Skip to contents

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.

The output shown comes from a real session with gpt-6-luna.

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:

# 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 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:

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:

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 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:

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
#>   <chr> <chr>      <int>
#> 1 FA    MIXED          1
#> 2 FA    PREVENTIVE     1
#> 3 PN    MIXED          1
#> 4 PN    PUNITIVE       2