diff --git a/R/chat.R b/R/chat.R index a8f53e4f..754c9500 100644 --- a/R/chat.R +++ b/R/chat.R @@ -133,18 +133,18 @@ Chat <- R6::R6Class("Chat", #' @description Extract structured data #' @param ... The input to send to the chatbot. Will typically include #' the phrase "extract structured data". - #' @param spec A type specification for the extracted data. Should be + #' @param type A type specification for the extracted data. Should be #' created with a [`type_()`][type_boolean] function. #' @param echo Whether to emit the response to stdout as it is received. #' Set to "text" to stream JSON data as it's generated (not supported by #' all providers). - extract_data = function(..., spec, echo = "none") { + extract_data = function(..., type, echo = "none") { turn <- user_turn(...) echo <- check_echo(echo %||% private$echo) coro::collect(private$submit_turns( turn, - spec = spec, + type = type, stream = echo != "none", echo = echo )) @@ -164,18 +164,18 @@ Chat <- R6::R6Class("Chat", #' that resolves to an object matching the type specification. #' @param ... The input to send to the chatbot. Will typically include #' the phrase "extract structured data". - #' @param spec A type specification for the extracted data. Should be + #' @param type A type specification for the extracted data. Should be #' created with a [`type_()`][type_boolean] function. #' @param echo Whether to emit the response to stdout as it is received. #' Set to "text" to stream JSON data as it's generated (not supported by #' all providers). - extract_data_async = function(..., spec, echo = "none") { + extract_data_async = function(..., type, echo = "none") { turn <- user_turn(...) echo <- check_echo(echo %||% private$echo) done <- coro::async_collect(private$submit_turns_async( turn, - spec = spec, + type = type, stream = echo != "none", echo = echo )) @@ -302,7 +302,7 @@ Chat <- R6::R6Class("Chat", # If stream = TRUE, yields completion deltas. If stream = FALSE, yields # complete assistant turns. - submit_turns = generator_method(function(self, private, user_turn, stream, echo, spec = NULL) { + submit_turns = generator_method(function(self, private, user_turn, stream, echo, type = NULL) { if (echo == "all") { cat_line(format(user_turn), prefix = "> ") @@ -313,7 +313,7 @@ Chat <- R6::R6Class("Chat", mode = if (stream) "stream" else "value", turns = c(private$.turns, list(user_turn)), tools = private$tools, - spec = spec + type = type ) emit <- emitter(echo) @@ -331,7 +331,7 @@ Chat <- R6::R6Class("Chat", result <- stream_merge_chunks(private$provider, result, chunk) } - turn <- value_turn(private$provider, result, has_spec = !is.null(spec)) + turn <- value_turn(private$provider, result, has_type = !is.null(type)) # Ensure turns always end in a newline if (any_text) { @@ -345,7 +345,7 @@ Chat <- R6::R6Class("Chat", cat_line(formatted, prefix = "< ") } } else { - turn <- value_turn(private$provider, response, has_spec = !is.null(spec)) + turn <- value_turn(private$provider, response, has_type = !is.null(type)) text <- turn@text if (!is.null(text)) { text <- paste0(text, "\n") @@ -364,13 +364,13 @@ Chat <- R6::R6Class("Chat", # If stream = TRUE, yields completion deltas. If stream = FALSE, yields # complete assistant turns. - submit_turns_async = async_generator_method(function(self, private, user_turn, stream, echo, spec = NULL) { + submit_turns_async = async_generator_method(function(self, private, user_turn, stream, echo, type = NULL) { response <- chat_perform( provider = private$provider, mode = if (stream) "async-stream" else "async-value", turns = c(private$.turns, list(user_turn)), tools = private$tools, - spec = spec + type = type ) emit <- emitter(echo) @@ -387,7 +387,7 @@ Chat <- R6::R6Class("Chat", result <- stream_merge_chunks(private$provider, result, chunk) } - turn <- value_turn(private$provider, result, has_spec = !is.null(spec)) + turn <- value_turn(private$provider, result, has_type = !is.null(type)) # Ensure turns always end in a newline if (any_text) { @@ -397,7 +397,7 @@ Chat <- R6::R6Class("Chat", } else { result <- await(response) - turn <- value_turn(private$provider, result, has_spec = !is.null(spec)) + turn <- value_turn(private$provider, result, has_type = !is.null(type)) text <- turn@text if (!is.null(text)) { text <- paste0(text, "\n") diff --git a/R/httr2.R b/R/httr2.R index 20e52b0e..b28cee01 100644 --- a/R/httr2.R +++ b/R/httr2.R @@ -5,7 +5,7 @@ chat_perform <- function(provider, mode = c("value", "stream", "async-stream", "async-value"), turns, tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { mode <- arg_match(mode) @@ -16,7 +16,7 @@ chat_perform <- function(provider, turns = turns, tools = tools, stream = stream, - spec = spec, + type = type, extra_args = extra_args ) diff --git a/R/provider-azure.R b/R/provider-azure.R index fa6c3b49..97bdc369 100644 --- a/R/provider-azure.R +++ b/R/provider-azure.R @@ -78,7 +78,7 @@ method(chat_request, ProviderAzure) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { req <- request(provider@base_url) @@ -95,12 +95,12 @@ method(chat_request, ProviderAzure) <- function(provider, tools <- as_json(provider, unname(tools)) extra_args <- utils::modifyList(provider@extra_args, extra_args) - if (!is.null(spec)) { + if (!is.null(type)) { response_format <- list( type = "json_schema", json_schema = list( name = "structured_data", - schema = as_json(provider, spec), + schema = as_json(provider, type), strict = TRUE ) ) diff --git a/R/provider-bedrock.R b/R/provider-bedrock.R index acea4c7a..9a779c23 100644 --- a/R/provider-bedrock.R +++ b/R/provider-bedrock.R @@ -58,7 +58,7 @@ method(chat_request, ProviderBedrock) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { req <- request(paste0( @@ -90,12 +90,12 @@ method(chat_request, ProviderBedrock) <- function(provider, messages <- compact(as_json(provider, turns)) - if (!is.null(spec)) { + if (!is.null(type)) { tool_def <- ToolDef( fun = function(...) {}, name = "structured_tool_call__", description = "Extract structured data", - arguments = type_object(data = spec) + arguments = type_object(data = type) ) tools[[tool_def@name]] <- tool_def tool_choice <- list(tool = list(name = tool_def@name)) @@ -188,12 +188,12 @@ method(stream_merge_chunks, ProviderBedrock) <- function(provider, result, chunk result } -method(value_turn, ProviderBedrock) <- function(provider, result, has_spec = FALSE) { +method(value_turn, ProviderBedrock) <- function(provider, result, has_type = FALSE) { contents <- lapply(result$output$message$content, function(content) { if (has_name(content, "text")) { ContentText(content$text) } else if (has_name(content, "toolUse")) { - if (has_spec) { + if (has_type) { ContentJson(content$toolUse$input$data) } else { ContentToolRequest( diff --git a/R/provider-claude.R b/R/provider-claude.R index 240f984b..a66b2726 100644 --- a/R/provider-claude.R +++ b/R/provider-claude.R @@ -69,7 +69,7 @@ method(chat_request, ProviderClaude) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { req <- request(provider@base_url) @@ -106,12 +106,12 @@ method(chat_request, ProviderClaude) <- function(provider, messages <- compact(as_json(provider, turns)) - if (!is.null(spec)) { + if (!is.null(type)) { tool_def <- ToolDef( fun = function(...) {}, name = "_structured_tool_call", description = "Extract structured data", - arguments = type_object(data = spec) + arguments = type_object(data = type) ) tools[[tool_def@name]] <- tool_def tool_choice <- list(type = "tool", name = tool_def@name) @@ -185,12 +185,12 @@ method(stream_merge_chunks, ProviderClaude) <- function(provider, result, chunk) result } -method(value_turn, ProviderClaude) <- function(provider, result, has_spec = FALSE) { +method(value_turn, ProviderClaude) <- function(provider, result, has_type = FALSE) { contents <- lapply(result$content, function(content) { if (content$type == "text") { ContentText(content$text) } else if (content$type == "tool_use") { - if (has_spec) { + if (has_type) { ContentJson(content$input$data) } else { if (is_string(content$input)) { diff --git a/R/provider-cortex.R b/R/provider-cortex.R index ae9c4da5..367381e5 100644 --- a/R/provider-cortex.R +++ b/R/provider-cortex.R @@ -99,12 +99,12 @@ method(chat_request, ProviderCortex) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { if (length(tools) != 0) { cli::cli_abort("Tools are not supported by Cortex.") } - if (!is.null(spec) != 0) { + if (!is.null(type) != 0) { cli::cli_abort("Structured data extraction is not supported by Cortex.") } @@ -234,7 +234,7 @@ cortex_chunk_to_message <- function(x) { } } -method(value_turn, ProviderCortex) <- function(provider, result, has_spec = FALSE) { +method(value_turn, ProviderCortex) <- function(provider, result, has_type = FALSE) { if (!is_named(result)) { # streaming role <- "assistant" content <- result diff --git a/R/provider-databricks.R b/R/provider-databricks.R index df602b16..9c5ed28b 100644 --- a/R/provider-databricks.R +++ b/R/provider-databricks.R @@ -72,7 +72,7 @@ method(chat_request, ProviderDatabricks) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { req <- request(provider@base_url) # Note: this API endpoint is undocumented and seems to exist primarily for @@ -95,12 +95,12 @@ method(chat_request, ProviderDatabricks) <- function(provider, tools <- as_json(provider, unname(tools)) extra_args <- utils::modifyList(provider@extra_args, extra_args) - if (!is.null(spec)) { + if (!is.null(type)) { response_format <- list( type = "json_schema", json_schema = list( name = "structured_data", - schema = as_json(provider, spec), + schema = as_json(provider, type), strict = TRUE ) ) diff --git a/R/provider-gemini.R b/R/provider-gemini.R index 4de26cd0..bbafcf37 100644 --- a/R/provider-gemini.R +++ b/R/provider-gemini.R @@ -50,7 +50,7 @@ method(chat_request, ProviderGemini) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { @@ -78,10 +78,10 @@ method(chat_request, ProviderGemini) <- function(provider, system <- list(parts = list(text = "")) } - if (!is.null(spec)) { + if (!is.null(type)) { generation_config <- list( response_mime_type = "application/json", - response_schema = as_json(provider, spec) + response_schema = as_json(provider, type) ) } else { generation_config <- NULL @@ -129,12 +129,12 @@ method(stream_merge_chunks, ProviderGemini) <- function(provider, result, chunk) merge_dicts(result, chunk) } } -method(value_turn, ProviderGemini) <- function(provider, result, has_spec = FALSE) { +method(value_turn, ProviderGemini) <- function(provider, result, has_type = FALSE) { message <- result$candidates[[1]]$content contents <- lapply(message$parts, function(content) { if (has_name(content, "text")) { - if (has_spec) { + if (has_type) { data <- jsonlite::parse_json(content$text) ContentJson(data) } else { diff --git a/R/provider-openai.R b/R/provider-openai.R index accecbab..7b275a96 100644 --- a/R/provider-openai.R +++ b/R/provider-openai.R @@ -96,7 +96,7 @@ method(chat_request, ProviderOpenAI) <- function(provider, stream = TRUE, turns = list(), tools = list(), - spec = NULL, + type = NULL, extra_args = list()) { req <- request(provider@base_url) @@ -113,12 +113,12 @@ method(chat_request, ProviderOpenAI) <- function(provider, tools <- as_json(provider, unname(tools)) extra_args <- utils::modifyList(provider@extra_args, extra_args) - if (!is.null(spec)) { + if (!is.null(type)) { response_format <- list( type = "json_schema", json_schema = list( name = "structured_data", - schema = as_json(provider, spec), + schema = as_json(provider, type), strict = TRUE ) ) @@ -169,14 +169,14 @@ method(stream_merge_chunks, ProviderOpenAI) <- function(provider, result, chunk) merge_dicts(result, chunk) } } -method(value_turn, ProviderOpenAI) <- function(provider, result, has_spec = FALSE) { +method(value_turn, ProviderOpenAI) <- function(provider, result, has_type = FALSE) { if (has_name(result$choices[[1]], "delta")) { # streaming message <- result$choices[[1]]$delta } else { message <- result$choices[[1]]$message } - if (has_spec) { + if (has_type) { json <- jsonlite::parse_json(message$content[[1]]) content <- list(ContentJson(json)) } else { diff --git a/R/provider.R b/R/provider.R index b247c4ad..62b3eaa9 100644 --- a/R/provider.R +++ b/R/provider.R @@ -26,7 +26,7 @@ Provider <- new_class( # Create a request------------------------------------ chat_request <- new_generic("chat_request", "provider", - function(provider, stream = TRUE, turns = list(), tools = list(), spec = NULL, extra_args = list()) { + function(provider, stream = TRUE, turns = list(), tools = list(), type = NULL, extra_args = list()) { S7_dispatch() } ) diff --git a/man/Chat.Rd b/man/Chat.Rd index e6538f31..7be08582 100644 --- a/man/Chat.Rd +++ b/man/Chat.Rd @@ -193,7 +193,7 @@ will be used.} \subsection{Method \code{extract_data()}}{ Extract structured data \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{Chat$extract_data(..., spec, echo = "none")}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{Chat$extract_data(..., type, echo = "none")}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -202,7 +202,7 @@ Extract structured data \item{\code{...}}{The input to send to the chatbot. Will typically include the phrase "extract structured data".} -\item{\code{spec}}{A type specification for the extracted data. Should be +\item{\code{type}}{A type specification for the extracted data. Should be created with a \code{\link[=type_boolean]{type_()}} function.} \item{\code{echo}}{Whether to emit the response to stdout as it is received. @@ -219,7 +219,7 @@ all providers).} Extract structured data, asynchronously. Returns a promise that resolves to an object matching the type specification. \subsection{Usage}{ -\if{html}{\out{
}}\preformatted{Chat$extract_data_async(..., spec, echo = "none")}\if{html}{\out{
}} +\if{html}{\out{
}}\preformatted{Chat$extract_data_async(..., type, echo = "none")}\if{html}{\out{
}} } \subsection{Arguments}{ @@ -228,7 +228,7 @@ that resolves to an object matching the type specification. \item{\code{...}}{The input to send to the chatbot. Will typically include the phrase "extract structured data".} -\item{\code{spec}}{A type specification for the extracted data. Should be +\item{\code{type}}{A type specification for the extracted data. Should be created with a \code{\link[=type_boolean]{type_()}} function.} \item{\code{echo}}{Whether to emit the response to stdout as it is received. diff --git a/tests/testthat/helper-provider.R b/tests/testthat/helper-provider.R index 5473bed8..232e3f59 100644 --- a/tests/testthat/helper-provider.R +++ b/tests/testthat/helper-provider.R @@ -128,11 +128,11 @@ test_data_extraction <- function(chat_fun) { " chat <- chat_fun() - data <- chat$extract_data(prompt, spec = article_summary) + data <- chat$extract_data(prompt, type = article_summary) expect_mapequal(data, list(title = "Apples are tasty", author = "Hadley Wickham")) # Check that we can do it again - data <- chat$extract_data(prompt, spec = article_summary) + data <- chat$extract_data(prompt, type = article_summary) expect_mapequal(data, list(title = "Apples are tasty", author = "Hadley Wickham")) } diff --git a/tests/testthat/test-chat.R b/tests/testthat/test-chat.R index 95219af5..95ce68dc 100644 --- a/tests/testthat/test-chat.R +++ b/tests/testthat/test-chat.R @@ -100,7 +100,7 @@ test_that("can extract structured data", { person <- type_object(name = type_string(), age = type_integer()) chat <- chat_openai() - data <- chat$extract_data("John, age 15, won first prize", spec = person) + data <- chat$extract_data("John, age 15, won first prize", type = person) expect_equal(data, list(name = "John", age = 15)) }) @@ -108,7 +108,7 @@ test_that("can extract structured data (async)", { person <- type_object(name = type_string(), age = type_integer()) chat <- chat_openai() - data <- sync(chat$extract_data_async("John, age 15, won first prize", spec = person)) + data <- sync(chat$extract_data_async("John, age 15, won first prize", type = person)) expect_equal(data, list(name = "John", age = 15)) }) diff --git a/vignettes/structured-data.Rmd b/vignettes/structured-data.Rmd index a857785b..1a963cd7 100644 --- a/vignettes/structured-data.Rmd +++ b/vignettes/structured-data.Rmd @@ -29,7 +29,7 @@ To extract structured data you call the `$extract_data()` method instead of the chat <- chat_openai() chat$extract_data( "My name is Susan and I'm 13 years old", - spec = type_object( + type = type_object( age = type_number(), name = type_string() ) @@ -41,14 +41,13 @@ The same basic idea works with images too: ```{r} chat$extract_data( content_image_url("https://www.r-project.org/Rlogo.png"), - spec = type_object( + type = type_object( primary_shape = type_string(), primary_colour = type_string() ) ) ``` - ## Data types basics To define your desired type specification (also known as a **schema**), you use the `type_()` functions. (You might already be familiar with these if you've done any function calling, as discussed in `vignette("function-calling")`). The type functions can be divided into three main groups: @@ -83,11 +82,14 @@ To define your desired type specification (also known as a **schema**), you use As well as the definition of the types, you need to provide the LLM with some information about what you actually want. This is the purpose of the first argument, `description`, which is a string that describes the data that you want. This is a good place to ask nicely for other attributes you'll like the value to possess (e.g. minimum or maximum values, date formats, ...). You aren't guaranteed that these requests will be honoured, but the LLM will usually make a best effort to do so. ```{r} -person <- type_object( +type_person <- type_object( "A person", name = type_string("Name"), age = type_integer("Age, in years."), - hobbies = type_array("List of hobbies. Should be exclusive and brief.", type_string()) + hobbies = type_array( + "List of hobbies. Should be exclusive and brief.", + items = type_string() + ) ) ``` @@ -105,7 +107,7 @@ text <- readLines(system.file("examples/third-party-testing.txt", package = "elm # html <- rvest::read_html(url) # text <- rvest::html_text2(rvest::html_element(html, "article")) -article_summary <- type_object( +type_summary <- type_object( "Summary of the article.", author = type_string("Name of the article author"), topics = type_array( @@ -118,7 +120,7 @@ article_summary <- type_object( ) chat <- chat_openai() -data <- chat$extract_data(text, spec = article_summary) +data <- chat$extract_data(text, type = type_summary) cat(data$summary) str(data) @@ -129,20 +131,18 @@ str(data) ```{r} text <- "John works at Google in New York. He met with Sarah, the CEO of Acme Inc., last week in San Francisco." -named_entities <- type_object( - "named entities", - entities = type_array( - "Array of named entities", - type_object( - name = type_string("The extracted entity name."), - type = type_enum("The entity type", c("person", "location", "organization")), - context = type_string("The context in which the entity appears in the text.") - ) - ) +type_named_entity <- type_object( + name = type_string("The extracted entity name."), + type = type_enum("The entity type", c("person", "location", "organization")), + context = type_string("The context in which the entity appears in the text.") +) + +type_named_entities <- type_object( + entities = type_array(items = type_named_entity) ) chat <- chat_openai() -str(chat$extract_data(text, spec = named_entities)) +str(chat$extract_data(text, type = type_named_entities)) ``` ### Example 3: Sentiment analysis @@ -150,7 +150,7 @@ str(chat$extract_data(text, spec = named_entities)) ```{r} text <- "The product was okay, but the customer service was terrible. I probably won't buy from them again." -sentiment <- type_object( +type_sentiment <- type_object( "Extract the sentiment scores of a given text. Sentiment scores should sum to 1.", positive_score = type_number("Positive sentiment score, ranging from 0.0 to 1.0."), negative_score = type_number("Negative sentiment score, ranging from 0.0 to 1.0."), @@ -158,7 +158,7 @@ sentiment <- type_object( ) chat <- chat_openai() -str(chat$extract_data(text, spec = sentiment)) +str(chat$extract_data(text, type = type_sentiment)) ``` Note that we've asked nicely for the scores to sum 1, and they do in this example (at least when I ran the code), but it's not guaranteed. @@ -168,7 +168,7 @@ Note that we've asked nicely for the scores to sum 1, and they do in this exampl ```{r} text <- "The new quantum computing breakthrough could revolutionize the tech industry." -classification <- type_array( +type_classification <- type_array( "Array of classification results. The scores should sum to 1.", type_object( name = type_enum( @@ -188,11 +188,11 @@ classification <- type_array( ) ) wrapper <- type_object( - classification = classification + classification = type_classification ) chat <- chat_openai() -data <- chat$extract_data(text, spec = wrapper) +data <- chat$extract_data(text, type = wrapper) do.call(rbind, lapply(data$classification, as.data.frame)) ``` @@ -200,14 +200,14 @@ Here we created a wrapper object because the OpenAI API requires all structured ```{r, eval = elmer:::anthropic_key_exists()} chat <- chat_claude() -data <- chat$extract_data(text, spec = classification) +data <- chat$extract_data(text, type = type_classification) do.call(rbind, lapply(data, as.data.frame)) ``` ### Example 5: Working with unknown keys ```{r, eval = elmer:::anthropic_key_exists()} -characteristics <- type_object( +type_characteristics <- type_object( "All characteristics", .additional_properties = TRUE ) @@ -221,7 +221,7 @@ prompt <- " " chat <- chat_claude() -str(chat$extract_data(prompt, spec = characteristics)) +str(chat$extract_data(prompt, type = type_characteristics)) ``` This examples only works with Claude, not GPT or Gemini, because only Claude @@ -238,7 +238,7 @@ The goal is to extract structured data from this screenshot: Even without any descriptions, ChatGPT does pretty well: ```{r} -asset <- type_object( +type_asset <- type_object( assert_name = type_string(), owner = type_string(), location = type_string(), @@ -249,13 +249,13 @@ asset <- type_object( income_high = type_integer(), tx_gt_1000 = type_boolean() ) -disclosure_report <- type_object( - assets = type_array(items = asset) +type_disclosure_report <- type_object( + assets = type_array(items = type_asset) ) chat <- chat_openai() image <- content_image_file("congressional-assets.png") -data <- chat$extract_data(image, spec = disclosure_report) +data <- chat$extract_data(image, type = type_disclosure_report) str(data) ``` @@ -270,7 +270,7 @@ By default, all components of an object are required. If you want to make some o For example, here the LLM hallucinates a date even though there isn't one in the text: ```{r} -article_spec <- type_object( +type_article <- type_object( "Information about an article written in markdown", title = type_string("Article title"), author = type_string("Name of the author"), @@ -289,7 +289,7 @@ prompt <- " " chat <- chat_openai() -chat$extract_data(prompt, spec = article_spec) +chat$extract_data(prompt, type = type_article) str(data) ``` @@ -298,13 +298,13 @@ Note that I've used more of an explict prompt here. For this example, I found th If let the LLM know that the fields are all optional, it'll instead return `NULL` for the missing fields: ```{r} -article_spec <- type_object( +type_article <- type_object( "Information about an article written in markdown", title = type_string("Article title", required = FALSE), author = type_string("Name of the author", required = FALSE), date = type_string("Date written in YYYY-MM-DD format.", required = FALSE) ) -chat$extract_data(prompt, spec = article_spec) +chat$extract_data(prompt, type = type_article) ``` ### Data frames @@ -312,7 +312,7 @@ chat$extract_data(prompt, spec = article_spec) If you want to define a data frame like object, you might be tempted to create a definition similar to what R uses: an object (i.e. a named list) containing multiple vectors (i.e. arrays): ```{r} -my_df_type <- type_object( +type_my_df <- type_object( name = type_array(items = type_string()), age = type_array(items = type_integer()), height = type_array(items = type_number()), @@ -323,7 +323,7 @@ my_df_type <- type_object( This however, is not quite right becuase there's no way to specify that each array should have the same length. Instead you need to turn the data structure "inside out", and instead create an array of objects: ```{r} -my_df_type <- type_array( +type_my_df <- type_array( items = type_object( name = type_string(), age = type_integer(),