Skip to content

Commit 8d794a6

Browse files
gadenbuiehadley
andauthored
feat: Complete dangling tool requests to avoid API errors (#840)
Co-authored-by: Hadley Wickham <[email protected]>
1 parent 89f25b0 commit 8d794a6

File tree

4 files changed

+598
-7
lines changed

4 files changed

+598
-7
lines changed

NEWS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
* `chat_openai_compatible()` replaces `chat_openai()` as the interface to use for OpenAI-compatible APIs, and `chat_openai()` is reserved for the official OpenAI API. Unlike previous versions of `chat_openai()`, the `base_url` parameter is now required (#801).
2222
* `chat_portkey()` now requires you to supply a model (#786).
2323
* `chat_portkey(virtual_key)` no longer needs to be supplied; instead Portkey recommends including the virtual key/provider in the `model` (#786).
24+
* `Chat$chat()`, `Chat$stream()`, and similar methods now add empty tool results when a the chat is interrupted during a tool call loop, allowing the conversation to be resumed without causing an API error (#840).
2425
* `Chat$chat_structured()` and friends now only warn if multiple JSON payloads found (instead of erroring) (@kbenoit, #732).
2526
* `Chat$get_tokens()` gives a brief description of the turn contents to make it easier to see which turn tokens are spent on (#618).
2627
* `Chat$get_tokens()` now also returns the cost, and returns one row for each assistant turn, better representing the underlying data received from LLM APIs. Similarly, the `print()` method now reports costs on each assistant turn, rather than trying to parse out individual costs (#824).

R/chat.R

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,9 @@ Chat <- R6::R6Class(
193193
#' `NULL`, then the value of `echo` set when the chat object was created
194194
#' will be used.
195195
chat = function(..., echo = NULL) {
196-
turn <- user_turn(...)
196+
finish_tools <- private$complete_dangling_tool_requests()
197+
198+
turn <- user_turn(!!!finish_tools, ...)
197199
echo <- check_echo(echo %||% private$echo)
198200

199201
# Returns a single turn (the final response from the assistant), even if
@@ -221,7 +223,9 @@ Chat <- R6::R6Class(
221223
#' using the schema. For example, this will turn arrays of objects into
222224
#' data frames and arrays of strings into a character vector.
223225
chat_structured = function(..., type, echo = "none", convert = TRUE) {
224-
turn <- user_turn(..., .check_empty = FALSE)
226+
finish_tools <- private$complete_dangling_tool_requests()
227+
228+
turn <- user_turn(!!!finish_tools, ..., .check_empty = FALSE)
225229
echo <- check_echo(echo %||% private$echo)
226230
check_bool(convert)
227231

@@ -252,7 +256,9 @@ Chat <- R6::R6Class(
252256
#' using the schema. For example, this will turn arrays of objects into
253257
#' data frames and arrays of strings into a character vector.
254258
chat_structured_async = function(..., type, echo = "none", convert = TRUE) {
255-
turn <- user_turn(..., .check_empty = FALSE)
259+
finish_tools <- private$complete_dangling_tool_requests()
260+
261+
turn <- user_turn(!!!finish_tools, ..., .check_empty = FALSE)
256262
echo <- check_echo(echo %||% private$echo)
257263
check_bool(convert)
258264

@@ -287,7 +293,9 @@ Chat <- R6::R6Class(
287293
#' an interactive user interface. Concurrent mode is the default and is
288294
#' best suited for automated scripts or non-interactive applications.
289295
chat_async = function(..., tool_mode = c("concurrent", "sequential")) {
290-
turn <- user_turn(...)
296+
finish_tools <- private$complete_dangling_tool_requests()
297+
298+
turn <- user_turn(!!!finish_tools, ...)
291299
tool_mode <- arg_match(tool_mode)
292300

293301
# Returns a single turn (the final response from the assistant), even if
@@ -315,7 +323,9 @@ Chat <- R6::R6Class(
315323
#' rich content types. When `stream = "content"`, `stream()` yields
316324
#' [Content] objects.
317325
stream = function(..., stream = c("text", "content")) {
318-
turn <- user_turn(...)
326+
finish_tools <- private$complete_dangling_tool_requests()
327+
328+
turn <- user_turn(!!!finish_tools, ...)
319329
stream <- arg_match(stream)
320330
private$chat_impl(
321331
turn,
@@ -343,7 +353,9 @@ Chat <- R6::R6Class(
343353
tool_mode = c("concurrent", "sequential"),
344354
stream = c("text", "content")
345355
) {
346-
turn <- user_turn(...)
356+
finish_tools <- private$complete_dangling_tool_requests()
357+
358+
turn <- user_turn(!!!finish_tools, ...)
347359
tool_mode <- arg_match(tool_mode)
348360
stream <- arg_match(stream)
349361
private$chat_impl_async(
@@ -735,6 +747,29 @@ Chat <- R6::R6Class(
735747

736748
has_system_prompt = function() {
737749
length(private$.turns) > 0 && is_system_turn(private$.turns[[1]])
750+
},
751+
752+
complete_dangling_tool_requests = function() {
753+
if (length(private$.turns) == 0) {
754+
return(NULL)
755+
}
756+
757+
last_turn <- private$.turns[[length(private$.turns)]]
758+
if (last_turn@role != "assistant") {
759+
return(NULL)
760+
}
761+
762+
tool_requests <- keep(last_turn@contents, is_tool_request)
763+
if (length(tool_requests) == 0) {
764+
return(NULL)
765+
}
766+
767+
lapply(tool_requests, function(req) {
768+
ContentToolResult(
769+
error = "Chat ended before the tool could be invoked.",
770+
request = req
771+
)
772+
})
738773
}
739774
)
740775
)
@@ -774,7 +809,7 @@ print.Chat <- function(x, ...) {
774809
turn_cost <- function(tokens, cost, prefix, suffix = "") {
775810
out <- paste0(prefix, "input=")
776811

777-
if (tokens[[3]] > 0) {
812+
if (!is.na(tokens[[3]]) && tokens[[3]] > 0) {
778813
out <- paste0(out, tokens[[1]], "+", tokens[[3]])
779814
} else {
780815
out <- paste0(out, tokens[[1]])

0 commit comments

Comments
 (0)