Skip to content

Describe participants from what they say about themselves (stacked on #124) - #125

Draft
audreyt wants to merge 11 commits into
Jigsaw-Code:mainfrom
audreyt:participant-context
Draft

audreyt wants to merge 11 commits into
Jigsaw-Code:mainfrom
audreyt:participant-context

Conversation

@audreyt

@audreyt audreyt commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #124 (which is stacked on #120). The change here is the commits after #124's (69fcfb2, 91f4d80, a02055b, f5223e7, the merge of #124 ca6f899, and 2849036). Please review #120 and #124 first.

Summary

Surveys often collect few demographics, yet responses carry context such as "I run a bakery" or "my elderly mother relies on paratransit". This adds src/participant_context, a stage that turns that context into columns, so opinions can be compared across groups.

For each configured attribute:

  1. An extraction model (CONTEXT_*, either clairic or systemone, through the Extractor interface from Extract quotes as exact spans with Mercury or Clairic (stacked on #120) #124) marks the participant's own words as evidence. Every span is checked against the response.
  2. The decision model (DECISION_*), when set, checks that evidence, seeing the whole response too. For an attribute with labels it picks one of them or none. For an attribute without labels it answers one yes/no question, whether the evidence states what the attribute asks (at DECISION_THRESHOLD), and rejected evidence is dropped.

When the extraction model marks nothing, the attribute gets no evidence and no further question is asked; an attribute with labels then gets the label none if a decision model is set. Marked evidence is only a candidate, as extraction models sometimes mark unrelated words, which is why the decision model checks it. Without a decision model, evidence is written unchecked and should be reviewed. A blank response reaches no model and is treated as one where nothing is marked, so a blank row no longer fails a run on hosts such as Mercury that reject empty text.

export CONTEXT_ENDPOINT_TYPE=clairic   # or systemone with Mercury Decide
export CLOUDFLARE_ACCOUNT_ID=<ACCOUNT_ID>
export CONTEXT_MODEL_API_KEY=<CLOUDFLARE_API_TOKEN>

python3 -m src.participant_context.participant_context_runner \
  --input_csv <OUTPUT_DIR>/processed.csv \
  --output_csv <OUTPUT_DIR>/processed_with_context.csv \
  --additional_context_file src/default-additional-context.md

The default attribute is role (business_owner, parent_or_caregiver, shift_worker, remote_worker). --attributes_file takes a JSON list of others, each with a name, the instructions for what to mark, and optional labels; the file is validated (unique identifier names, label keys without spaces, none reserved). The output adds <name>_evidence, <name>_not_stated, <name>_label and <name>_label_probability.

Evaluation

The 28-response transit survey from #124, with role labels written down before the first run (9 participants state a role, 19 do not). Labels came from local Clef in both runs; only the extraction backend differs.

Extraction backend Correct labels Evidence found (of 9) False role Evidence verbatim
Clairic 27 of 28 9 1 28 of 28
Mercury Decide 23 of 28 4 0 28 of 28
  • Clairic found evidence for all nine roles, usually as a short phrase ("kids", "shop", "elderly mother"). Its one error: a participant whose response begins "Bus." and who was "written up at work" for being late got the evidence "Bus", which Clef labeled shift_worker (probability 0.80, against 0.98 or more for the correct labels).
  • Mercury was precise but missed five roles stated indirectly (children, a daughter, an elderly mother, a son, a shop), reporting them as not stated with probability above 0.93.

With 28 responses this is a sanity check, not a benchmark; on this set Clairic is the better default for this stage.

A review run also tried an evidence-only attribute, tenure ("how long they have lived in the city"), which only one participant states ("30 years"). Without a check, Clairic also marked "older people", "last year", "suburban" and two other phrases in responses that never say. Neither span confidence (up to 0.94 on those) nor not_stated separated them from real evidence, so a threshold would not have fixed it. With the yes/no check (91f4d80) and local Clef, only "30 years" remains, in two Clairic runs and one Mercury run, and the role results are unchanged.

Test plan

  • pytest (342 tests), including fake-model tests for evidence and labels, the yes/no check on evidence without labels (rejected evidence dropped, silence never asked about), the none option, single-span fields, <response> joining, attribute-file validation and the runner's CSV columns; each of these was checked to fail when the behavior is removed
  • Live: the runner with CONTEXT_ENDPOINT_TYPE=clairic and with =systemone (Mercury), labels from local Clef: exit 0, results above
  • Live: the survey with two blank rows added (empty, and empty <response> segments), on Mercury and on Clairic with Clef: exit 0, both blank rows none, role results unchanged
  • Live: a custom attributes file with the evidence-only tenure attribute, with and without a decision model: results above

Quote extraction can now run on a System One model that answers extract
questions (Inception Mercury Decide does; Clef, Jev and the pplx deciders
answer only choice, noul and score). It is configured apart from the
decision model with QUOTE_ENDPOINT_TYPE, QUOTE_MODEL, QUOTE_MODEL_HOST and
QUOTE_MODEL_API_KEY, which share the DECISION_* factory, so judgments can
stay on local Clef while quotes come from Mercury.

- SystemOneClient gains ExtractQuestion and ExtractAnswer. Every returned
  span is checked against the request state (code-point offsets) and a
  mismatch is a parse error.
- For each topic one extract question marks the sentences where the
  participant gives a view on it. Spans are widened to whole sentences and
  non-adjacent sentences are joined with " ... ", so each piece of a quote
  is verbatim text of the response. A topic with nothing marked gets the
  whole response; host errors fail the stage.
- With QUOTE_ENDPOINT_TYPE unset the generative path is unchanged, and
  --skip_quote_extraction still takes precedence.
Adds src/models/extraction.py: an Extractor marks exact spans of a text for
named fields. Two backends implement it:

- SystemOneExtractor asks extract questions through a System One host
  (Inception Mercury Decide), one question per field.
- ClairicExtractor calls Cloudflare's Clairic on Workers AI, asking a field
  that allows several spans as a list of text and a single-span field as
  text. List items without offsets are dropped.

Both check every span against the text before returning, and both use the
System One transport's bearer auth and retry policy (the retry loop moves
into systemone.post_with_retry). model_factory.get_extractor(prefix) picks
the backend from {prefix}_ENDPOINT_TYPE=systemone|clairic; Clairic's host
defaults to the Workers AI URL for CLOUDFLARE_ACCOUNT_ID. The quote stage
now takes any extractor via QUOTE_*.
Adds src/participant_context: for each configured attribute, an extraction
model (CONTEXT_*, Clairic or a System One extract host such as Mercury
Decide) marks the participant's own words as evidence, and the decision
model (DECISION_*), when set, picks one of the attribute's labels or "none"
for that evidence. A participant who says nothing for an attribute gets no
evidence and the label "none" without a question, so nothing is inferred
from silence. Without a decision model only evidence is written.

The runner reads participant_id and survey_text from a CSV and adds
<name>_evidence, <name>_not_stated, <name>_label and
<name>_label_probability columns. The default attribute is role
(business_owner, parent_or_caregiver, shift_worker, remote_worker);
--attributes_file takes a JSON list of others.
Review of the participant-context stage found that an evidence-only
attribute kept whatever the extraction model marked: asked how long
participants had lived in the city, Clairic marked "older people",
"last year" and "suburban" in responses that never say. Neither span
confidence nor not_stated separated these from real evidence, so a
threshold would not fix it.

When a decision model is configured, evidence for an attribute without
labels is now checked with one yes/no question (whether the evidence
states what the attribute asks, at DECISION_THRESHOLD), and rejected
evidence is dropped. Attributes with labels were already checked, as the
decision model can answer "none". The README and docstrings no longer
claim that silence always yields no evidence: they say evidence is a
candidate, checked when a decision model is set and unchecked otherwise.
The gate review of 91f4d80 found the docs still promised the label "none"
whenever nothing is marked, which only holds for attributes with labels
and a decision model, and an AttributeResult docstring still said evidence
is None "when they said nothing". Documentation only.
Gate re-review: AttributeResult.evidence said the decision model keeps or
drops evidence whenever one is configured. For an attribute with labels the
evidence stays even when the label is "none". Documentation only.
Review found that ClairicExtractor read a field missing from the response
as an empty extraction and silently dropped offsets that were not a
[start, end] pair, so a malformed response could look like "nothing
found". Both are now parse errors. Answers whose offsets are null are
still dropped, as before and as documented, since Clairic returns those
for text it cannot place.
A blank survey_text reached the extraction model, and Mercury rejects an
empty state, so one blank row failed the whole run on Mercury but not on
Clairic. Blank responses, including ones made only of empty <response>
segments, now reach no model and are treated as saying nothing.

Inserting the participant-context section had also split the quote
paragraph of the README, leaving its last sentences (sentence widening,
the whole-response fallback, --skip_quote_extraction) under the wrong
heading. They are back where Jigsaw-Code#124 has them.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant