--- title: "Analyzing text" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Analyzing text} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", eval = FALSE) ``` > *Versión en español:* `vignette("text-analysis-es", package = "ellmercodex")`. Language models are useful in research when they return something R can work with, not just prose. This article uses speeches from a Uruguayan parliamentary debate on cannabis regulation to show two `ellmer` features that work with `ellmercodex`: structured data and tool calling. The examples are inspired by the [blog post about `ellmercodex`](https://simxnherrera.github.io/blog/ellmercodex/). The output shown comes from a real session with `gpt-6-luna`. ```{r} library(ellmercodex) chat <- chat_codex(model = "gpt-6-luna", effort = "low") ``` ## Classify an excerpt With `$chat_structured()` you describe the result you want with `ellmer`'s type system, and the model has to answer in that shape. Here we ask for a stance on drug policy, from a fixed set of codes, plus a short rationale: ```{r} # A real excerpt from the debate excerpt <- paste( "seguimos pensando que además de la represión y del combate al", "narcotráfico que debe seguir con firmeza y decisión como sociedad", "debemos apostar a dos o tres pilares fundamentales la educación", "la prevención y la rehabilitación" ) result <- chat$chat_structured( paste( "Classify the excerpt as PREVENTIVE, PUNITIVE, MIXED, or NO_CLEAR_STANCE.", "Use MIXED when it argues for preventive and punitive responses at the", "same time. Answer in English.", "\n\nExcerpt:", excerpt ), type = ellmer::type_object( stance = ellmer::type_enum(c( "PREVENTIVE", "PUNITIVE", "MIXED", "NO_CLEAR_STANCE" )), rationale = ellmer::type_string() ) ) result #> $stance #> [1] "MIXED" #> #> $rationale #> [1] "The excerpt supports firm repression and combating drug trafficking, #> alongside education, prevention, and rehabilitation." ``` The result is an R list, so you can put it in a `data.frame`, validate it, or analyze it alongside codes from human coders. `ellmer`'s [structured data](https://ellmer.tidyverse.org/articles/structured-data.html) article covers the type system in depth, including how to extract several rows at once. ## Let the model call your R functions Suppose you have already classified many speeches: ```{r} speeches <- data.frame( party = c("FA", "FA", "PN", "PN", "PN"), stance = c("PREVENTIVE", "MIXED", "PUNITIVE", "PUNITIVE", "MIXED") ) ``` Instead of pasting the data into the prompt, you can give the model a tool: an R function it may call when it needs information. This one counts the stances for one party and returns JSON, which is what `ellmer` expects from tools: ```{r} summarize_party <- function(party_name) { speeches |> dplyr::filter(party == party_name) |> dplyr::count(stance) |> jsonlite::toJSON() } summary_tool <- ellmer::tool( summarize_party, name = "summarize_party", description = "Count the policy stances in a party's speeches.", arguments = list(party_name = ellmer::type_string()) ) chat$register_tool(summary_tool) chat$chat("Compare the policy stances of FA and PN. Use the available tool.") #> FA has one **MIXED** stance and one **PREVENTIVE** stance. PN has one #> **MIXED** stance and two **PUNITIVE** stances. Both have a mixed stance, #> while FA's other stance is preventive and PN's are punitive. ``` The model decided to call `summarize_party()` for each party, received the results computed by R, and used them in its answer. A tool could just as well query a database, search a corpus, or call an API. R always runs the functions: the model can only use the tools you register, and it decides when to call them and with which arguments. `ellmer`'s [tool calling](https://ellmer.tidyverse.org/articles/tool-calling.html) article explains how to design good tools. ## Combine the two `$chat_structured()` does not use registered tools, as in `ellmer`. When you need both, gather the context with `$chat()` first, then extract the result with `$chat_structured()` in the same conversation: ```{r} chat$chat("Use the tool to get the stance counts for FA and PN.") counts <- chat$chat_structured( "Return the counts you just retrieved.", type = ellmer::type_array( ellmer::type_object( party = ellmer::type_string(), stance = ellmer::type_string(), n = ellmer::type_integer() ) ) ) counts #> # A tibble: 4 x 3 #> party stance n #> #> 1 FA MIXED 1 #> 2 FA PREVENTIVE 1 #> 3 PN MIXED 1 #> 4 PN PUNITIVE 2 ```