Repository navigation
Conversation
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_*.
This was referenced Oct 26, 2026
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.
audreyt
added a commit
to audreyt/sensemaking-tools
that referenced
this pull request
Oct 27, 2026
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #120. This branch contains #120's commits; the change here is the last three commits (72d69b6, f3887b7, be1d869). Please review #120 first.
Summary
By default the generative model writes each quote and may lightly edit the participant's words in brackets. This adds an opt-in path where an extraction model marks the participant's own sentences instead, so every quote is verbatim text from the response.
The extraction model is pluggable behind one
Extractorinterface (src/models/extraction.py), with two backends:systemone: a System One host that answersextractquestions. Of the hosts tested, only Inception Mercury Decide does; local Clef, pplx-decider, Jev and Clef on Workers AI accept only choice, noul and score.clairic: Cloudflare's Clairic extraction model on Workers AI (@cf/cloudflare/clairic). A field that allows several spans is asked as a list of text; a single-span field as text.Both backends check every span against the request text before returning it and share the System One transport's bearer auth and retry policy. Quote extraction has its own settings, apart from
DECISION_*, so judgments can stay on a local model while quotes come from either backend:model_factory.get_extractor(prefix)reads{prefix}_ENDPOINT_TYPE, so other stages can take an extractor the same way.How it works:
SystemOneClientgainsExtractQuestionandExtractAnswer. Every returned span, from either backend, is checked against the request text (offsets count Unicode code points) and a mismatch is a parse error. Clairic answers without offsets are dropped; a field missing from Clairic's response, or offsets that are not a [start, end] pair, are parse errors rather than an empty answer.....<response>segments are joined first, as--skip_quote_extractiondoes.QUOTE_ENDPOINT_TYPEunset the generative path is unchanged, and--skip_quote_extractionstill takes precedence.Evaluation
A 28-response transit survey (38 statement-topic pairs, including multi-part
<response>answers and an emoji), comparing span quotes from Mercury with quotes from the existing generative path on local gemma4 (gemma4:26b-a4b-it):The four generated quotes that are not verbatim changed capitalization or punctuation without brackets (for example "It is" became "it is"). One pair got no generated quote because the endpoint errored. The judges are decision models that produced neither set; both show position bias (about half of the pairs flip with order), which is why every comparison is asked in both orders and averaged.
The instruction wording was chosen on this set: an earlier wording that did not mention "what they want done" kept only 3 of 10 sentences in which participants asked for something (for example "Please build protected bike lanes"); the final wording keeps all 10. The set is small, so treat the numbers as a sanity check rather than a benchmark.
Running the quote stage on the same 38 pairs through each backend: Clairic and Mercury both produced 38 quotes, all verbatim, with no
<response>fragments. Clairic marked nothing for 1 pair and Mercury for 3; those pairs got the whole response.Test plan
get_extractorfactory, extract parsing and span checks, sentence widening, the QUOTE_* factory and the span path in the quote stage<response>fragments and correct offsets after an emojiQUOTE_ENDPOINT_TYPE=clairicand with=systemone(Mercury): 38 of 38 quotes verbatim on each