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.