Skip to contents

Returns the lines of a session file: one JSON object per line, each one row of a table of the result, so that writeLines(format(result), "session.fg.jsonl") saves the session and focus_group_from_transcript() reads it back.

Usage

# S3 method for class 'focus_group_result'
format(x, ...)

# S3 method for class 'FocusGroup'
format(x, ...)

# S3 method for class 'focus_group_arms'
format(x, ...)

Arguments

x

A focus_group_result, or a FocusGroup.

...

Unused.

Value

A character vector of JSON lines, one per row, in the order session, participants, items, messages, routing, probes, injections, calls.

Details

Each line has a type, and its other fields are the columns of the table it comes from:

session

One line, first: the fields of metadata (topic, purpose, the turn-taking rule and its parameters, the settings, the status, the versions), with summary and usage, and format and version, which name the file format. metadata$handles and metadata$guide are written in the lines below. A field of metadata the format does not define, such as a researcher's metadata$condition, is written with the prefix x_ ("x_condition"), and the reader restores it under its name.

participant

One line per row of participants, the moderator's included.

item

One line per guide item, in order: item (its number) and the item's phase, text, and verbatim, where set; any other field of the item, such as a label, with the prefix x_.

message

One line per row of transcript.

routing, probe, injection, call

One line per row of routing, probes, injections, and calls. A call line leaves out messages, the messages the call sent, which grow with the square of the session's length and are not needed to read or continue it.

The writing is canonical, so that a file read back and written again is the same file. A missing value is null. A whole number stored as a double is written with a decimal point (160.0) and an integer without one, and every double is written with as many digits as it needs to be read back exactly. A named vector, such as the desire scores of a pick, is an object; candidates and tied are always arrays. An empty list is {}. Times are written in UTC to the microsecond ("2026-10-24T14:30:00.25Z"). Every character outside ASCII is written as a \uXXXX escape, so the file is plain ASCII.

The file holds no model configuration and no runner, and so no key or endpoint from a configuration: metadata holds none, and the recorded call settings are the allowlisted model parameters. The message of a failed call is written as recorded, except that key-like values in it (the values of query parameters such as key=, bearer tokens, the values of x-api-key: and api-key: headers and of JSON fields such as "api_key", "apiKey", "token", and "secret", and strings of the forms of OpenAI, Groq, xAI, Hugging Face, and Google API keys) are replaced by <redacted>. saveRDS() of the result, by contrast, saves focus_group, which holds the configuration (and so any literal key or private endpoint in it) and the runner; set result$focus_group <- NULL first to save the record alone. analyze_focus_group() and the Studio rebuild the session object of such a result from its tables.

For a result saved by FocusGroup 0.5.5 or earlier, the guide, the flow parameters, and the communication styles, which it lacks, are read from the fields of its focus_group, when it has one; without one, focus_group_from_transcript() rebuilds them from the record as its help page describes.

format() of a FocusGroup, such as one read by focus_group_from_transcript(), gives the lines of its session file in the same way, from its log and records. A focus_group_arms object holds several sessions and has no one file: format an arm, format(x$arms[["name"]]), or the shared prefix, format(x$prefix).

Examples

cfg <- LLMR::llm_config("groq", "openai/gpt-oss-20b",
                        api_key = LLMR::llm_api_key_env("GROQ_API_KEY"))
scripted <- function(experiments, ...) {
  experiments$response_text <- "Evening hours matter most to me."
  experiments
}
result <- run_focus_group("Library hours", cfg, n_participants = 2,
                          guide = list(Engagement = "Which hours matter?",
                                       Closing = "Close the session."),
                          flow = "round_robin", verbose = FALSE,
                          .runner = scripted)
path <- tempfile(fileext = ".fg.jsonl")
writeLines(format(result), path)
substr(readLines(path)[1:3], 1, 70)
#> [1] "{\"type\":\"session\",\"format\":\"fg-session\",\"version\":1,\"topic\":\"Library h"    
#> [2] "{\"type\":\"participant\",\"id\":\"P1\",\"handle\":\"P1\",\"role\":\"participant\",\"is"
#> [3] "{\"type\":\"participant\",\"id\":\"P2\",\"handle\":\"P2\",\"role\":\"participant\",\"is"
fg <- focus_group_from_transcript(path)
fg$conversation_log[[3]]$text
#> [1] "Evening hours matter most to me."