---
title: "FocusGroup Architecture"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{FocusGroup Architecture}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, include=FALSE}
knitr::opts_chunk$set(eval = FALSE)
```

# Architecture

FocusGroup supports the design and examination of moderated discussions before
fieldwork. Researchers can compare moderator guides, participant sources, and
speaker-selection rules while retaining one transcript format for analysis. A
generated transcript describes the specified session and does not estimate
population attitudes.

Most studies can begin with `run_focus_group()`. This article explains the
underlying objects for researchers who need direct construction, offline
execution, or custom turn-taking rules.

`FGAgent` represents a participant or moderator and generates that agent's
turns. Its model configuration is stored in the `config` field.
`ConversationFlow` is the base class for custom turn-taking rules.
`FocusGroup` advances the moderator guide and records each message. LLMR
supplies model configurations and provider calls.

## Main functions

`run_focus_group()` is the single high-level session runner. It requires an
explicit `config`, constructs the roster and flow, performs a call-count
preflight, runs the guide, and returns a `focus_group_result`.

Its main arguments are consistent with the other package entry points:

- `n_participants` is the participant count, excluding the moderator.
- `guide` is a named vector or list of phase counts or moderator scripts.
- `flow` selects the speaker-selection rule.
- `message_mode` selects role-flipped or flat message construction.
- `config` is the model configuration, and `admin_config` is the group-level
  configuration on direct `FocusGroup` construction.
- `.runner`, when supplied, is an optional function used instead of live model
  calls.

The package supplies three built-in flow modes: `"round_robin"`,
`"probabilistic"`, and `"desire_based"`. Construct them with
`create_conversation_flow()`. Their implementation classes are internal.
`ConversationFlow` remains public so packages and researchers can define new
selection rules.

`create_agents()`, `create_agents_from_data()`, and
`create_agents_from_survey()` return agent-ID-keyed lists that include the
moderator `MOD`. Each requires an explicit `config` because the returned agents
can generate model output.

`focus_group_from_transcript()` creates a `FocusGroup` from an existing
transcript without generating new turns. `moderator_id` identifies the
moderator when known. If it is omitted, a speaker named exactly `mod`,
`moderator`, `facilitator`, or `interviewer` (in any case) is the moderator.
When no speaker matches, the importer says so and analyzes every speaker as a
participant.

## Offline execution and replay

The `.runner` argument accepts an optional function for offline tests or replay
that uses the request and response data-frame format used across LLMR. The
package calls `.runner(experiments, ...)`, where `experiments` is a data frame
with `config` and `messages` list-columns. The function returns those rows with
at least `response_text`; it may also return `response_id`, token counts,
`success`, and related diagnostics.

The function handles moderator, participant, desire-scoring, and summary
requests. This format permits an archive replay function to execute a full
session without a live provider. A configuration remains explicit and is
recorded with the experiment so readers can see how it was run.

## Running a session

`FocusGroup$run_simulation()` first records the participant roster as a
`System` message. It then reads `question_script` in order. For each entry, the
moderator generates one message. Question phases call
`select_next_speaker()` and collect no more than
`max_participant_responses` participant messages, followed at a call-on point
by the replies of the participants the moderator calls on. Opening and closing
phases do not collect participant responses.

The round-robin mode cycles through participants. The probabilistic mode
samples from speaking propensities. The desire-based mode asks eligible
participants for model-produced scores and selects among eligible candidates;
it passes over the participant who spoke just before, and a moderator line in
between ends that exclusion, so whoever closed one question may open the next.
A score is read from the reply's final answer, never from reasoning the
model returns with it. A desire score that cannot be obtained, after one retry
with a larger budget, stops the session with an error of class
`focusgroup_desire_failure`. With `on_desire_failure = "random"` the session
continues instead: the speaker is chosen among the participants with valid
scores by the same rule, or by a uniform draw when none has one, and the
failure is recorded in the message metadata. A failure is never recorded as a
score of zero.

Two kinds of turn are given without the flow. When a moderator line puts its
last question to one participant by handle ("P3, do you agree?", "Could you
say more, P3?"), that participant takes the next turn. When the discussion of
a call-on item ends (`call_on_silent`, by default the first, middle, and last
question), the moderator calls on each participant who has not spoken since
the previous such point, in roster order: a fixed line, delivered with no model
call, then the participant's reply, with no desire score. Krueger (2002)
describes the opening question of a focus group as a round robin; the first
point gives every participant a turn during the first question. Both kinds of
turn count as participant turns, for `max_participant_responses` (an
addressed turn) and for `fg_before_turn()`, and the flow's state is updated
after them as after its own picks. Round robin keeps its place in the
rotation, and passes over the participant who has just spoken when the
rotation comes to them, so that no one speaks twice in a row.

The loop stops when the script is exhausted, `num_rounds` is reached, or the
last closing entry is processed. It stores a final summary in `final_summary`,
unless the session runs with `final_summary = FALSE`.
The summary is not appended to `conversation_log`.

The agents see each other under handles: participants as `P1`, `P2`, ... in
roster order and the moderator as `Moderator` by default. Every prompt, the
desire scores, probes, and the interim summary among them, is built from one
view of the log in which each speaker appears under its handle, and each
participant's system message names its own. The log keeps the agent IDs.

Krueger, R. A. (2002). *Designing and Conducting Focus Group Interviews*.
University of Minnesota.

A `FocusGroup` object runs once. Calling `run_simulation()` on an object that
has already run raises an error. Construct a new object for another full
session. To continue a recorded session under other histories, pass it to
`run_focus_group_arms()`, as described below; the continuation experiment in
`run_focus_studio()` compares one next message.

A message can also be placed into a running session: `fg_injection()`
describes it and the point where it goes, after the moderator's line for a
guide item (`fg_after_item()`), before a participant turn
(`fg_before_turn()`), or before the closing line (`"before_closing"`). `run_focus_group_arms()` runs the session to that point
once, copies the whole session there, and continues each copy to the end
under its own arm, so the arms share every message before the injection.
`fg_probe()` asks each participant a private question at stated points; the
answers are saved apart from the transcript and never enter the discussion.
With `scale = c(1, 7)`, each answer is also read as one whole number from 1 to
7 into the `value` column of `probes`; an answer from which no single number
on the scale can be read, such as "3 or 4" or "6/10", is re-asked once, and
stays `NA`, with a warning, when the re-ask gives none either.

An injection from a participant's ID or handle at `fg_before_turn(t)` is that
participant's turn `t`, said as written with no model call. It is recorded
with the routing rule `"injected"` and takes the place of the turn the session
was about to give: the flow's pick, a turn the moderator's question addressed,
or the reply to a call-on line.

`run_focus_group_arms()` also continues a recorded session. Given a
`focus_group_result` or a `FocusGroup` as its first argument, and arms that
branch at `fg_before_turn(t)`, it reads the record into a new object, sets the
engine to the state it was in just before turn `t` (the guide item and its
stage, the turns given on it, the call-on queue, an addressed participant, the
question and the interim summary in force, and the turn-taking rule's state),
and runs each arm from there with the configuration supplied. The messages
before turn `t` are the shared prefix. An arm that is `NULL` regenerates turn
`t` and what follows; under round robin, with a model that answers the same
messages the same way, it repeats the record. A human transcript read with
`focus_group_from_transcript()` can be continued once its participants have
personas; its later moderator lines are delivered as recorded.

## Messages and prompts

The default `message_mode = "roleflip"` presents an agent's prior messages as
assistant messages and labels other speakers' messages as user messages.
`message_mode = "flat"` places the transcript in one user message. The selected
mode is recorded with the session.

`get_default_prompt_templates()` returns only templates supported by package
operations. Pass named replacements through `prompt_templates` in
`FocusGroup$new()`. A custom participant template containing
`{{conversation_history}}` or `{{persona_description}}` uses the flat message
path.

## Stored data

`conversation_log` is an ordered list of message records. Each record has a
unique integer `message_id`, the moderator cycle in `round`, the guide item
in `item`, `phase`, the agent ID in `speaker_id`, and the label the agents saw
in `handle`. Moderator and participant messages responding to the same
question share a round. A generated message also records what the provider returned about the
call, such as its response identifier, finish reason, and token counts, with
the provider and model that produced it.

`run_focus_group()` exposes this log as the `transcript` data frame in its
result. A `focus_group_result` always has these fields:

- `focus_group`: the underlying R6 object.
- `transcript`: one row per logged message.
- `summary`: the final model summary.
- `participants`: a data frame describing the roster.
- `usage`: token use in LLMR's `sent`, `rec`, and `total` vocabulary, with
  `unknown`, the number of calls whose usage was incomplete. Unreported counts
  are unknown rather than zero, so the totals are lower bounds when `unknown`
  is positive; `unknown` is `NA` when that number cannot be determined, as
  after a call that raised an error.
- `metadata`: `topic`, `purpose`, `flow`, `message_mode`, `n_participants`,
  `estimated_calls`, `provider`, `model`, `complete`, and `failure`, with no
  credentials. `complete` is `FALSE` for a session that stopped early, and
  `failure` then records the phase and the error message.

When a model call fails during `run_simulation()`, `run_focus_group()` raises
an error of class `focusgroup_incomplete_session`. Its `partial` field is a
`focus_group_result` built from the messages logged before the failure, its
`phase` field names the phase that was running, and its `parent` field holds
the original error. A failed final summary is the exception: it produces a
warning and a complete result whose `summary` is `NA`.

The class has a concise print method. `FocusGroup` also has a concise S3 print
method for direct R6 objects.

`format(result)` writes the result as the lines of a session file, one JSON
object per line, each one row of a table of the result:
`writeLines(format(result), "session.fg.jsonl")`. The file holds no model
configuration, key, endpoint, or runner, and `focus_group_from_transcript()`
reads it back into a new `FocusGroup` with every setting and record, so that
it can be analyzed, replayed with `fg_player()`, or continued. `saveRDS()` of
a result, by contrast, keeps `focus_group`, with its configuration and runner.
The file `system.file("fg-session-spec.md", package = "FocusGroup")` describes
the format for converting a transcript by hand or with a language model.

## Analysis

`FocusGroup$analyze()` and the descriptive text methods operate on the stored
log. `analyze_focus_group()` collects the principal analyses in a
`focus_group_analysis` with these returned components: `basic_stats`, `topics`,
`tfidf`, `readability`, `themes`, `model_summary`, `plots`, and `issues`.

Missing optional packages and unsuitable data produce empty components with
consistent columns where the result is tabular, and an explanatory row in
`issues`. `plots` is an empty list when ggplot2 is unavailable. Descriptive
analyses run offline. Thematic analysis and model summaries run only when
`analyze_focus_group()` receives an explicit `config`. A requested model
analysis that encounters a provider failure raises the provider condition.

## Extending the package

To add a turn-taking rule, subclass `ConversationFlow` and implement
`select_next_speaker()`. Override `update_state_post_selection()` when the rule
keeps state, and `note_moderator_turn()` when a moderator line should change
it. An extension can construct its class directly; built-in modes
remain available through `create_conversation_flow()`.

See the function and class help pages for argument defaults and return values.
