-
Notifications
You must be signed in to change notification settings - Fork 79
feat: Record and replay Turn objects and its dependencies #503
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
schloerke
wants to merge
38
commits into
tidyverse:main
Choose a base branch
from
schloerke:turns_record_replay
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
38 commits
Select commit
Hold shift + click to select a range
a9dae0a
Ignore _dev folder
schloerke 8c0ccb9
Init pass at contents_record / contents_replay
schloerke 60e9a70
Drop chat state during serialization / unserialization. Instead, pass…
schloerke 4ba8b91
Merge branch 'main' into turns_record_replay
schloerke 17d41eb
Relocate expect_record_replay
schloerke 8a3905d
Test with registered tool
schloerke 540ff71
Store more information in a ToolDef record. On restore, make a new To…
schloerke f423c07
document
schloerke 3654460
Remove commented code
schloerke 2c3673c
Add pre/post checks when recording
schloerke 2795f45
Rename dispatch method
schloerke 8cc0574
Fixed bug in tooldef args
schloerke 618290b
Don't record completed field
schloerke 59e0b55
Get the S7 class name in the traceback!
schloerke adf7bac
Complete some TODOs
schloerke e084919
Update content-replay.R
schloerke 66b9649
Allow for objects to be non-pkg S7 classes
schloerke 0dd25a4
Error when S7 classes can't be found
schloerke a7b24cb
Update test-content-replay.R
schloerke f268b96
Move test helper to helper file. Rename it
schloerke 3f46ee6
Merge branch 'main' into turns_record_replay
schloerke e9cba40
Add Barret as author
schloerke f51229e
Update _pkgdown.yml
schloerke ae4dc7e
Add news entry
schloerke 2227382
bump version
schloerke a011e7b
Loop over list elements when recording or replaying
schloerke 4493487
clean up code and add test for unknown tool
schloerke 9206ce5
Merge branch 'main' into turns_record_replay
schloerke 4a80ad0
Switch tests to use chatgpt
hadley 6feb541
Tweak docs/style; unexport `contents_replay_class`
hadley 94b7fc3
Merge branch 'main' into turns_record_replay
schloerke 20b961e
doc update
schloerke f41cc3c
Remove prefix of `rlang::`
schloerke 81dbaa6
Remove unnecessary arg to `cli_abort()`. Add snapshot of error.
schloerke 9bbab27
Move `cls` assertions inside `get_cls_constructor()`
schloerke 9f52544
Remove non-exported param docs
schloerke 3465cb4
Only support (and enforce) ellmer s7 objects.
schloerke eefd37f
Update test for (now unsupported) local s7 class
schloerke File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,4 +14,5 @@ _cache/ | |
^[\.]?air\.toml$ | ||
^\.vscode$ | ||
^data-raw$ | ||
^_dev$ | ||
^revdep$ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,4 @@ docs | |
inst/doc | ||
|
||
/.quarto/ | ||
_dev/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,15 @@ | ||
Package: ellmer | ||
Title: Chat with Large Language Models | ||
Version: 0.2.1.9000 | ||
Version: 0.2.1.9001 | ||
Authors@R: c( | ||
person("Hadley", "Wickham", , "[email protected]", role = c("aut", "cre"), | ||
comment = c(ORCID = "0000-0003-4757-117X")), | ||
person("Joe", "Cheng", role = "aut"), | ||
person("Aaron", "Jacobs", role = "aut"), | ||
person("Garrick", "Aden-Buie", , "[email protected]", role = "aut", | ||
comment = c(ORCID = "0000-0002-7111-0077")), | ||
person("Barret", "Schloerke", , "[email protected]", role = "aut", | ||
comment = c(ORCID = "0000-0001-9986-114X")), | ||
person("Posit Software, PBC", role = c("cph", "fnd"), | ||
comment = c(ROR = "03wc8by49")) | ||
) | ||
|
@@ -58,6 +60,7 @@ RoxygenNote: 7.3.2 | |
Collate: | ||
'utils-S7.R' | ||
'types.R' | ||
'ellmer-package.R' | ||
'tools-def.R' | ||
'content.R' | ||
'provider.R' | ||
|
@@ -70,9 +73,9 @@ Collate: | |
'content-image.R' | ||
'content-pdf.R' | ||
'turns.R' | ||
'content-replay.R' | ||
'content-tools.R' | ||
'deprecated.R' | ||
'ellmer-package.R' | ||
'httr2.R' | ||
'import-standalone-obj-type.R' | ||
'import-standalone-purrr.R' | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,231 @@ | ||
#' @include utils-S7.R | ||
#' @include turns.R | ||
#' @include tools-def.R | ||
#' @include content.R | ||
NULL | ||
|
||
#' Save and restore content | ||
#' | ||
#' @description | ||
#' These generic functions can be use to convert [Turn]/[Content] objects | ||
#' into easily serializable representations. | ||
#' | ||
#' * `contents_record()` accept a [Turn] or [Content] and return a simple list. | ||
#' * `contents_replay()` will accept a simple list (from `contents_record()`) | ||
#' and return a [Turn] or [Content] object. | ||
#' | ||
#' @param content A [Turn] or [Content] object to serialize. | ||
#' @param obj A basic list to desierialize. | ||
#' @param chat A [Chat] object to be used for context. | ||
#' @param ... Not used. | ||
#' | ||
#' @examplesIf has_credentials("openai") | ||
#' chat <- chat_openai(model = "gpt-4.1-nano") | ||
#' chat$chat("Where is the capital of France?") | ||
#' | ||
#' # Serialize to a simple list | ||
#' turn_recorded <- contents_record(chat$get_turns(), chat = chat) | ||
#' str(turn_recorded) | ||
#' | ||
#' # Deserialize back to S7 objects | ||
#' turn_replayed <- contents_replay(turn_recorded, chat = chat) | ||
#' turn_replayed | ||
#' @export | ||
#' @rdname contents_record | ||
contents_record <- | ||
new_generic( | ||
"contents_record", | ||
"content", | ||
function(content, ..., chat) { | ||
check_chat(chat, call = caller_env()) | ||
|
||
recorded <- S7::S7_dispatch() | ||
|
||
if (!is_recorded_object(recorded)) { | ||
cli::cli_abort( | ||
"Expected the recorded object to be a list with at least names 'version', 'class', and 'props'." | ||
) | ||
} | ||
|
||
if ( | ||
!is.character(recorded$class) || | ||
length(recorded$class) != 1 | ||
) { | ||
cli::cli_abort( | ||
"Expected the recorded object to have a single $class name, containing `::` if the class is from a package." | ||
) | ||
} | ||
|
||
if (!grepl("ellmer::", recorded$class, fixed = TRUE)) { | ||
cli::cli_abort( | ||
"Only S7 classes from the `ellmer` package are currently supported. Received: {.val {recorded$class}}." | ||
) | ||
} | ||
|
||
recorded | ||
} | ||
) | ||
|
||
method(contents_record, S7::S7_object) <- function(content, ..., chat) { | ||
class_name <- class(content)[1] | ||
|
||
# Remove read-only props | ||
cls_props <- S7::S7_class(content)@properties | ||
prop_names <- names(cls_props)[!map_lgl(cls_props, prop_is_read_only)] | ||
|
||
recorded_props <- setNames( | ||
lapply(prop_names, function(prop_name) { | ||
prop_value <- S7::prop(prop_name, object = content) | ||
if (S7_inherits(prop_value)) { | ||
# Recursive record for S7 objects | ||
contents_record(prop_value, chat = chat) | ||
} else if (is_list_of_s7_objects(prop_value)) { | ||
# Make record of each item in list | ||
lapply(prop_value, contents_record, chat = chat) | ||
} else { | ||
prop_value | ||
} | ||
}), | ||
prop_names | ||
) | ||
|
||
# Remove non-serializable properties | ||
recorded_props <- Filter(function(x) !is.function(x), recorded_props) | ||
|
||
list( | ||
version = 1, | ||
class = class_name, | ||
props = recorded_props | ||
) | ||
} | ||
|
||
|
||
#' @rdname contents_record | ||
#' @export | ||
# Holy "Holy Trait" dispatching, Batman! | ||
contents_replay <- function(obj, ..., chat) { | ||
check_chat(chat, call = caller_env()) | ||
|
||
# Find any reason to not believe `obj` is a recorded object. | ||
# If not a recorded object, return it as is. | ||
# If it is a recorded s7 object, dispatch on the discovered class. | ||
|
||
if (!is_recorded_object(obj)) { | ||
cli::cli_abort( | ||
"Expected the object to be a list with at least names 'version', 'class', and 'props'." | ||
) | ||
} | ||
|
||
class_name <- obj$class | ||
if (!(is.character(class_name) && length(class_name) == 1)) { | ||
cli::cli_abort( | ||
"Expected the replay object's `'class'` value to be a single character." | ||
) | ||
} | ||
|
||
cls_name <- strsplit(class_name, "::")[[1]][2] | ||
if (!grepl("ellmer::", class_name, fixed = TRUE)) { | ||
cli::cli_abort( | ||
"Only S7 classes from the `ellmer` package are currently supported." | ||
) | ||
} | ||
|
||
cls <- pkg_env("ellmer")[[cls_name]] | ||
|
||
if (is.null(cls)) { | ||
cli::cli_abort("Unable to find the S7 class: {.val {class_name}}.") | ||
} | ||
|
||
if (!S7_inherits(cls)) { | ||
schloerke marked this conversation as resolved.
Show resolved
Hide resolved
|
||
cli::cli_abort( | ||
"The object returned for {.val {class_name}} is not an S7 class." | ||
) | ||
} | ||
|
||
# Manually retrieve the handler for the class as we dispatch on the class itself, | ||
# not on an instance | ||
# An error will be thrown if a method is not found, | ||
# however we have a fallback for the `S7::S7_object` (the root base class) | ||
handler <- S7::method(contents_replay_class, cls) | ||
handler(cls, obj, chat = chat) | ||
} | ||
|
||
contents_replay_class <- new_generic( | ||
"contents_replay_class", | ||
"cls", | ||
function(cls, obj, ..., chat) { | ||
S7::S7_dispatch() | ||
} | ||
) | ||
|
||
|
||
method(contents_replay_class, S7::S7_object) <- function( | ||
cls, | ||
obj, | ||
..., | ||
chat | ||
) { | ||
stopifnot(obj$version == 1) | ||
|
||
obj_props <- map(obj$props, function(prop_value) { | ||
if (is_list_of_recorded_objects(prop_value)) { | ||
# If the prop is a list of recorded objects, replay each one | ||
map(prop_value, contents_replay, chat = chat) | ||
} else if (is_recorded_object(prop_value)) { | ||
# If the prop is a recorded object, replay it | ||
contents_replay(prop_value, chat = chat) | ||
} else { | ||
prop_value | ||
} | ||
}) | ||
|
||
class_name <- obj$class[1] | ||
cls_name <- strsplit(class_name, "::")[[1]][2] | ||
# While this seems like a bit of extra work, the tracebacks are accurate | ||
# vs referencing an unrelated parameter name in the traceback | ||
exec(cls_name, !!!obj_props, .env = ns_env("ellmer")) | ||
} | ||
|
||
method(contents_replay_class, ToolDef) <- function( | ||
cls, | ||
obj, | ||
..., | ||
chat | ||
) { | ||
if (obj$version != 1) { | ||
cli::cli_abort( | ||
"Unsupported version {.val {obj$version}}." | ||
) | ||
} | ||
|
||
tools <- chat$get_tools() | ||
matched_tool <- tools[[obj$props$name]] | ||
|
||
if (!is.null(matched_tool)) { | ||
return(matched_tool) | ||
} | ||
|
||
# If no tool is found, return placeholder tool containing the metadata | ||
ret <- contents_replay_class( | ||
super(cls, S7::S7_object), | ||
obj, | ||
chat = chat | ||
) | ||
ret | ||
} | ||
|
||
prop_is_read_only <- function(prop) { | ||
is.function(prop$getter) && !is.function(prop$setter) | ||
} | ||
|
||
is_recorded_object <- function(x) { | ||
is.list(x) && all(c("version", "class", "props") %in% names(x)) | ||
} | ||
|
||
is_list_of_s7_objects <- function(x) { | ||
is.list(x) && all(map_lgl(x, S7_inherits)) | ||
} | ||
|
||
is_list_of_recorded_objects <- function(x) { | ||
is.list(x) && all(map_lgl(x, is_recorded_object)) | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.