Mining insights
Transcript → code-computed charts + steering signals.
Endpoint: POST /api/v1/tools/mine_insights. Pure + deterministic by default (0 credits); the LLM layer (useLlm:true) runs the smallest model (Haiku) and meters a few credits.
What goes in
| Field | Type | What it does |
|---|---|---|
turns (required) | array | The transcript: [{ speaker, text }]. Comes straight from a focus group, chat, debate, etc. |
maxSignals | integer | Cap on steering signals returned (default 2) — keeps it to the strongest. |
minStrength | number 0–1 | Drop signals weaker than this (default ~0.5). |
useLlm | boolean | Add the smallest-model analysis layer (subtext the keyword scan misses). |
llmMode | mine·identify·find | Which question the LLM layer answers (below). |
topic | string | Focus hint for the LLM layer. |
{ "turns": [
{ "speaker": "Jamie", "text": "At $29 I'd switch today." },
{ "speaker": "Riley", "text": "For my 12-person team that price worries me — cheap won't scale." },
{ "speaker": "Sam", "text": "I got burned at tax season once. I need to trust the numbers." }
],
"maxSignals": 3, "minStrength": 0.4 }What comes back — two layers, clearly separated
1. Charts (code-computed, basis: "code-computed-from-transcript")
| kind | Shows | How it's computed |
|---|---|---|
share_bar (theme convergence) | Share of speakers who raised each theme. | Theme detection over the text, counted across participants. |
diverging_bar (polarization) | For-vs-against split on contested themes. | Stance scoring per speaker per theme. |
sentiment_arc | Sentiment trajectory across the conversation. | Per-turn sentiment, ordered. |
theme_distribution | How themes spread across the panel. | Theme frequency distribution. |
"charts": [
{ "kind": "share_bar", "title": "Theme convergence",
"series": [ {"label":"Price","value":0.67}, {"label":"Trust","value":0.33}, {"label":"Scale","value":0.33} ],
"computedFrom": "3 speakers, 3 turns" }
]Charts are never drawn from model-invented numbers. The model may caption or theme, but the values come from counting/scoring the real transcript. That's the bright line.
2. Steering signals (rate-limited, ranked by strength)
| kind | Meaning |
|---|---|
steer | The single highest-value follow-up question to ask next. |
contradiction | A speaker contradicted themselves — worth probing. |
divergence | A genuine split in the room. Carries a ready-to-run branchPrompt you can hand to /api/v1/insights/branch. |
branch | An explicit suggestion to fork a sub-conversation. |
"signals": [
{ "kind": "divergence", "strength": 0.82,
"summary": "Price reads as 'great value' to Jamie but 'won't scale' to Riley — opposite meanings of the same $29.",
"branchPrompt": "Riley, you said $29 feels too cheap to scale — what price would actually signal it's built for a team your size?" }
]The LLM layer — useLlm: true with three modes
The code miner is fast and deterministic but keyword-bounded. Flip useLlm:true and the smallest model reads the transcript for what a scan can't see. The output is returned under llm with basis: "model-analyzed" — kept strictly separate from the code charts.
llmMode: "mine" — deeper signals (subtext)
Surfaces implied objections, unprobed threads, and emotional undercurrents.
{ "useLlm": true, "llmMode": "mine", "topic": "$29 finance app" }
# → llm.signals: [
# { kind:"steer", text:"Sam's tax-season burn is an unprobed trust wound — ask what evidence would rebuild it." } ]llmMode: "identify" — where opinions diverge NOW
Maps the real fault lines present in this transcript, with a targeted probe per split.
{ "useLlm": true, "llmMode": "identify" }
# → llm.signals: [
# { kind:"divergence", text:"Jamie optimizes price, Riley optimizes scale-fit, Sam optimizes trust — three different value hierarchies." } ]llmMode: "find" — what would FLIP an opinion (hypotheses)
Generates counterfactual conditions to test. Stamped hypothesized: true — these are hypotheses to verify, not facts.
{ "useLlm": true, "llmMode": "find" }
# → llm: { basis:"model-analyzed", mode:"find", hypothesized:true, signals:[
# { kind:"condition", text:"If there were a $99 team tier with SLAs, would Riley's 'won't scale' objection disappear?" } ] }A find result is a question for the world, not an answer. The natural next step is to test it live: feed it to the flip-test loop, which branches the conversation, introduces the condition, and measures whether the opinion actually moved. See Branching & flip-tests.
The full loop: converse → mine → branch → verify
# 1. Run a focus group (or chat) → grab data.transcript
POST /api/v1/tools/focus_group { "audienceId":"aud_f1_1", "topic":"$29 finance app", "stimulus":"...", "panelSize":5, "rounds":2 }
# 2. Mine it for divergences (code + LLM "identify")
POST /api/v1/tools/mine_insights { "turns":[...transcript...], "useLlm":true, "llmMode":"identify" }
# → a divergence signal with a branchPrompt
# 3. Explore that split live
POST /api/v1/insights/branch { "audienceId":"aud_f1_1", "branchPrompt":"<from step 2>", "depth":2 }
# 4. Or hypothesize + verify a flip-condition end to end
POST /api/v1/insights/flip-test { "audienceId":"aud_f1_1", "turns":[...], "topic":"price vs scale" }
# → verdict: flipped | softened | unchanged | hardened (MEASURED from real stance text)Why two layers instead of one
The code layer gives you defensible numbers (you can show a client the chart and explain exactly how it was computed). The LLM layer gives you interpretation and direction (what to ask next, what might change minds). Keeping them separate — with explicit basis labels — means you always know which is which, and you never accidentally present a model's guess as a measured result.