From a6975c2c6afd17f545880d61abc0aef9c973bea6 Mon Sep 17 00:00:00 2001 From: Duck Quang Date: Thu, 16 Jul 2026 05:38:27 +0700 Subject: [PATCH 1/2] Vary SIM prose, add domain-aware Python hypotheses, per-idea citations + thinking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mirrored keyless generators (frontend sim/content.ts, webapp/content.py) emitted fixed-skeleton text: the same self-critique/stress/review stitching every round, identical overview prose every session, and always-biomedical Python hypotheses. This de-templates them and threads simulated reasoning through. De-template (both runtimes, seeded phrase banks, headings/structure unchanged): - makeReview / make_review: 4 dimension paragraphs + assumption note now draw from seeded banks (scores drawn first so they stay deterministic). - makeSelfCritique / make_self_critique: the fixed reread/doubt/stakes stitching sentences now vary per round + target. - makeStressReport / make_stress_report: the break/claim/attack/feasibility/citation sentences vary per hyp + round (verdict token still seeded separately for report<->ranking consistency). - makeOverview / make_overview: every top-level section and per-proposal block drawn from banks, seeded per session (proposal ids) so two runs of the same goal read differently; per-proposal streams keep blocks distinct. Prose genericised so it fits any domain, not just wet-lab. - Match rationales varied (engine.ts, simulator.py, seed.py). Domain-aware Python hypotheses: ported the TS DOMAINS table / keyword extraction / title scaffolds into webapp/content.py so a non-biomedical goal (e.g. a reasoning method) yields on-topic hypotheses instead of drug/pathway mad-libs. make_plan now reports the inferred domain. Enriched both DOMAINS tables (reasoning/LLM terms → computing; inflammation/senescence → biomedicine). Per-idea citations: browser overviewRefs now consumes each proposal's real citations (was re-sampling random curated papers — a bug), deduped like the Python _overview_refs, and markers are spread across the claim / why / experiment sentences. Overview, drawer and per-proposal donut now cite the SAME papers. Framing/summary/landscape/comparative/recommended/open-questions stay UNCITED (Co-Scientist synthesis). Groq-prose path stays honestly uncited. Simulated thinking: each sim hypothesis gets varied seeded reasoning (distinct per hyp), threaded through engine.ts (PlanHyp/hypContent/addHyp/toHypothesis) and the Python store (hyp_thinking side table, read in get_hypothesis). Groq GenHyp gains a real `reasoning` field (prompt + parse) that flows into thinking for BYOK hyps. types.ts Hypothesis + PlanHyp gain optional thinking. Verified: npm run build green; pytest co_scientist/tests/unit -q (276 passed); ruff adds no new errors; server E2E — two same-goal runs differ in prose while headings match, per-proposal [n] resolve to References, framing uncited, hypotheses on-topic, hypotheses carry distinct thinking. Co-Authored-By: Claude Fable 5 --- frontend/src/lib/sim/content.ts | 347 ++++++++++--- frontend/src/lib/sim/engine.ts | 37 +- frontend/src/lib/sim/generate.ts | 7 +- frontend/src/types.ts | 4 + webapp/content.py | 828 +++++++++++++++++++++++++------ webapp/seed.py | 28 +- webapp/simulator.py | 22 +- webapp/store.py | 12 + 8 files changed, 1032 insertions(+), 253 deletions(-) diff --git a/frontend/src/lib/sim/content.ts b/frontend/src/lib/sim/content.ts index 0780d65..af15ff9 100644 --- a/frontend/src/lib/sim/content.ts +++ b/frontend/src/lib/sim/content.ts @@ -36,6 +36,10 @@ export interface SimHypothesisContent { full_text: string; citations: SimCitation[]; strategy: string; + /** Varied synthetic reasoning (2-3 sentences, seeded per hyp). Labelled + * SIMULATED by the UI. For Groq hyps the engine overrides this with the + * model's real `reasoning`. */ + thinking: string; } export interface SimReviewContent { @@ -122,7 +126,7 @@ const DOMAINS: DomainProfile[] = [ }, { id: "computing", - match: ["model", "algorithm", "software", "data", "network", "compute", "latency", "system", "code", "server", "database", "inference", "cache", "gpu", "throughput", "distributed", "spreadsheet", "layout", "app", "ui", "interface", "dashboard"], + match: ["model", "algorithm", "software", "data", "network", "compute", "latency", "system", "code", "server", "database", "inference", "cache", "gpu", "throughput", "distributed", "spreadsheet", "layout", "app", "ui", "interface", "dashboard", "reasoning", "prompt", "prompting", "llm", "transformer", "agent"], levers: ["an algorithmic redesign", "a caching layer", "a scheduling-policy change", "a model-architecture tweak", "a batching strategy"], metric: "end-to-end latency", unit: "%", methods: ["a benchmark with ablations", "an A/B experiment in staging", "a load test under production-like traffic"], @@ -152,7 +156,7 @@ const DOMAINS: DomainProfile[] = [ id: "biomedicine", // NB: "cell"/"cells" intentionally omitted — too ambiguous (spreadsheet / // battery / phone cell). Real bio prompts hit cancer/tumor/gene/organoid/etc. - match: ["gene", "genetic", "protein", "disease", "cancer", "drug", "tissue", "organoid", "microbiome", "patient", "clinical", "neuro", "neuroinflammation", "therapy", "therapeutic", "molecular", "immune", "blood", "brain", "metabolic", "tumor", "leukemia", "antibody", "biomarker"], + match: ["gene", "genetic", "protein", "disease", "cancer", "drug", "tissue", "organoid", "microbiome", "patient", "clinical", "neuro", "neuroinflammation", "therapy", "therapeutic", "molecular", "immune", "blood", "brain", "metabolic", "tumor", "leukemia", "antibody", "biomarker", "inflammation", "senescence", "fibrotic"], levers: ["a repurposed approved compound", "a targeted pathway inhibitor", "a genetic perturbation", "a combination regimen", "an epigenetic priming step"], metric: "the disease-signature score", unit: "%", methods: ["an in-vitro assay in a relevant model", "an isogenic knockdown experiment", "a dose-response study"], @@ -304,29 +308,84 @@ outcome is unchanged, giving a clean falsification. A dose- or intensity-dependent change in ${dom.metric}, concentrated where ${topic} is most acute — with no effect in the inert control arm. `; - return { title, summary, full_text, citations: makeCitations(r), strategy }; + // Varied synthetic reasoning — three seeded picks referencing THIS idea's own + // lever/topic/metric/method, so every hypothesis carries a distinct rationale. + const thinking = [ + r.choice([ + `Starting from the goal — ${aim} — I asked which single lever most plausibly moves ${dom.metric}.`, + `I worked backward from ${dom.metric}: what intervention on ${topic} shifts it most for the least cost?`, + `The prompt points at ${topic}; my instinct was to isolate one mechanism rather than bundle several.`, + `Before committing I weighed a few levers on ${topic} and kept the one with the cleanest read on ${dom.metric}.`, + ]), + r.choice([ + `${cap(lever)} stood out because its effect on ${dom.metric} should be direct, not mediated by a long causal chain.`, + `I favoured ${lever} since it acts close to the outcome, so a null result is informative rather than ambiguous.`, + `${cap(lever)} is concrete enough to specify precisely and cheap enough to falsify quickly.`, + `The appeal of ${lever} is that it fails loudly — if it does nothing to ${dom.metric}, that rules it out cleanly.`, + ]), + r.choice([ + `Tested with ${method}, a ≥${pct}${dom.unit} move would be decisive; anything less and I would drop it.`, + `I would run ${method} first — it gives a pass/fail on ${dom.metric} before any larger commitment.`, + `The plan is ${method}, kept small and pre-registered so the ${pct}${dom.unit} threshold actually means something.`, + ]), + ].join(" "); + return { title, summary, full_text, citations: makeCitations(r), strategy, thinking }; } export function makeReview(goal: string, hypTitle: string, kind: string): SimReviewContent { const r = makeRng(`${goal}|${hypTitle}|${kind}`); const verdict = r.choice(["neutral", "missing_piece", "already_explained", "other_more_likely"]); + // Draw the scores FIRST so they stay stable regardless of the prose banks + // below (the scorecard, self-critique and stress stages all read these). const scores = { novelty: round2(r.uniform(0.45, 0.95)), correctness: round2(r.uniform(0.5, 0.95)), testability: round2(r.uniform(0.55, 0.98)), feasibility: round2(r.uniform(0.4, 0.9)), }; + // Seeded prose banks — the four dimension paragraphs vary per hyp/session so + // no two reviews read alike, while the **Dimension (score).** labels stay fixed. + const nov = r.choice([ + "The proposed lever is under-explored for this goal; adjacent work exists but does not test this exact intervention.", + "A genuinely fresh angle — the surrounding literature circles the idea without landing on this specific move.", + "Not unprecedented, but the particular framing here has not been put to a direct test before.", + "The novelty is in how the pieces are combined rather than the pieces themselves; the specific claim is under-tested.", + ]); + const cor = r.choice([ + "Internally consistent — the causal chain from intervention to the primary outcome is plausible, though one upstream assumption (below) is load-bearing.", + "The logic holds together; the weakest link is a single upstream step, flagged below, that the design should pin down.", + "No obvious contradiction, and the mechanism is stated crisply enough to check — one assumption still carries most of the weight.", + "Reasoning is sound end-to-end, with the caveat that the effect depends on an intermediate step that is assumed rather than shown.", + ]); + const tes = r.choice([ + "Strong: the readout is quantitative and the proposed method yields a clear pass/fail against the stated threshold.", + "Highly testable — a pre-registered threshold on a measurable readout turns this into a decisive experiment.", + "The claim exposes itself to falsification: one clean measurement either clears the bar or sinks it.", + "Good — the outcome is numeric and the comparison is controlled, so the result won't be open to interpretation.", + ]); + const fea = r.choice([ + "Achievable with commonly available methods; the main risk is confounding, which the control arm is designed to absorb.", + "Within reach of a modest setup and timeline; the chief hazard is a confound the baseline arm has to neutralise.", + "Practical to run soon and cheaply — the open question is whether the control fully isolates the effect.", + "No exotic resources required; the sensitivity is to a confounder that the matched comparison is meant to rule out.", + ]); + const note = r.choice([ + "that the measured outcome actually reflects the mechanism, not a proxy", + "that the intervention reaches the regime where it can act at all", + "that the control arm removes the most likely alternative explanation", + "that the effect size survives outside the tidy conditions of the pilot", + ]); const body = `**Verdict:** ${verdict} -**Novelty (${scores.novelty}).** The proposed lever is under-explored for this goal; adjacent work exists but does not test this exact intervention. +**Novelty (${scores.novelty}).** ${nov} -**Correctness (${scores.correctness}).** Internally consistent — the causal chain from intervention to the primary outcome is plausible, though one upstream assumption (below) is load-bearing. +**Correctness (${scores.correctness}).** ${cor} -**Testability (${scores.testability}).** Strong: the readout is quantitative and the proposed method yields a clear pass/fail against the stated threshold. +**Testability (${scores.testability}).** ${tes} -**Feasibility (${scores.feasibility}).** Achievable with commonly available methods; the main risk is confounding, which the control arm is designed to absorb. +**Feasibility (${scores.feasibility}).** ${fea} -**Key assumption checked:** that the measured outcome actually reflects the mechanism, not a proxy. Rated *${r.choice(["plausible", "uncertain"])}*. +**Key assumption checked:** ${note}. Rated *${r.choice(["plausible", "uncertain"])}*. `; return { kind, verdict, scores, body }; } @@ -500,26 +559,34 @@ export function insertAfterHeading(md: string, n: number, block: string): string return md; } -interface OverviewRef { n: number; title: string; year: number; url: string } - -/** Pick one curated REAL paper per proposal (deterministic per proposal title). - * Returns numbered refs + per-proposal `[n]` markers, deduped by URL so repeated - * sources share a number. The real engine builds References from real data; the - * browser-LLM path passes null to referencesSection (honest, no sources). */ -function overviewRefs(goal: string, top: OverviewProposal[]): { refs: OverviewRef[]; markers: string[] } { +interface OverviewRef { n: number; title: string; year: number | null; url: string } + +/** Dedupe each proposal's OWN citations (real OpenAlex papers online, curated + * REAL_PAPERS offline) into a numbered reference list, and return a parallel + * per-proposal list of individual `[n]` marker strings so the caller can spread + * them across the sentences that lean on a source. Deduped by url (fallback doi) + * so a paper cited by two proposals shares one number — this makes the overview, + * the drawer and the per-proposal donut all cite the SAME papers. Mirrors + * webapp/content.py `_overview_refs`. The browser-LLM path never calls this; it + * passes null to referencesSection (honest, no sources). */ +function overviewRefs(top: OverviewProposal[]): { refs: OverviewRef[]; markers: string[][] } { const refs: OverviewRef[] = []; - const markers: string[] = []; - const urlToN = new Map(); + const markers: string[][] = []; + const keyToN = new Map(); for (const p of top) { - const paper = makeRng(`ref|${goal}|${p.title}`).choice(REAL_PAPERS); - const url = `https://doi.org/${paper.doi}`; - let n = urlToN.get(url); - if (n === undefined) { - n = refs.length + 1; - urlToN.set(url, n); - refs.push({ n, title: paper.title, year: paper.year, url }); + const ns: number[] = []; + for (const c of p.citations ?? []) { + const key = (c.url || c.doi || "").trim(); + if (!key) continue; + let n = keyToN.get(key); + if (n === undefined) { + n = refs.length + 1; + keyToN.set(key, n); + refs.push({ n, title: c.title, year: c.year, url: c.url }); + } + if (!ns.includes(n)) ns.push(n); } - markers.push(`[${n}]`); + markers.push(ns.sort((a, b) => a - b).map((n) => `[${n}]`)); } return { refs, markers }; } @@ -606,10 +673,21 @@ function proposalEloBody(id: string | undefined, title: string, n: number, figur export function makeOverview(goal: string, proposals: OverviewProposal[], figures?: OverviewFigures): string { const top = proposals.slice(0, 3); const lead = top[0]; - const { refs, markers } = overviewRefs(goal, top); + const { refs, markers } = overviewRefs(top); + // Seeded per SESSION (the proposal ids carry the session id) so two runs of the + // same goal read differently while a re-render of one run is stable. The + // top-level sections draw from `r`; each proposal block gets its own stream. + const seedTail = top.map((p) => p.id ?? p.title).join("|"); + const r = makeRng(`${goal}|overview|${seedTail}`); const sections = top.map((p, i) => { const elo = p.elo != null ? Math.round(p.elo) : "—"; + const pr = makeRng(`${goal}|proposal|${p.id ?? p.title}`); + // Spread this proposal's own citation markers across the sentences that + // lean on a source: the claim, why-it's-promising, and the experiment. + const cm = markers[i] ?? []; + const at = (k: number) => (cm[k] ? ` ${cm[k]}` : ""); + const expMk = cm.slice(2).length ? ` ${cm.slice(2).join("")}` : ""; // Per-proposal illustrations for the top-3: a compact score radar on the Elo // line, plus an experiment-pipeline mermaid, a cited-sources donut, and an // Elo sparkline at the END of the block. All UNNUMBERED (chart title only) @@ -626,27 +704,43 @@ export function makeOverview(goal: string, proposals: OverviewProposal[], figure if (eloFig) endFigs.push(eloFig); } const tail = endFigs.length ? `\n\n${endFigs.join("\n\n")}` : ""; + const why = pr.choice([ + "It survived repeated head-to-head debates against competing ideas, and reviewers scored it well on novelty and testability. The mechanism is specific enough to design a decisive experiment around.", + "It kept winning matches on the strength of its argument rather than its framing, and the reviewers' marks back that up. Crucially, it is concrete enough that one experiment can settle it.", + "Across the tournament it beat rivals that were vaguer or harder to test, and it carries a specific, falsifiable claim rather than a direction of travel.", + "The idea earned its rank by holding up under scrutiny, not by out-arguing softer competitors — and its core claim is sharp enough to design a clean test around.", + ]); + const exp = pr.choice([ + "Set up the smallest faithful version of the system, apply the intervention across a short range, and read out the primary measure alongside one orthogonal check. Include an untouched baseline and a plausibly-inert comparison so a positive result is interpretable.", + "Run a compact controlled trial: vary the lever over a few settings, measure the primary outcome plus a second independent signal, and hold a matched control so the effect can't be confused with drift.", + "Start with a cheap decisive experiment — the intervention at one or two intensities, a quantitative readout, and both a do-nothing baseline and an inert-looking control to keep the result unambiguous.", + "Build a minimal test bed, apply the intervention against a matched control, and track the primary measure together with an orthogonal one so a real effect and an artefact look different.", + ]); + const feas = pr.choice([ + "Achievable within a modest budget and a single cycle. The main risk is that the intervention never reaches the regime where it can act — worth a quick pilot to check that first.", + "Cheap and quick to run. The chief hazard is a hidden confounder producing the same reading, which the control arm is there to absorb.", + "No exotic resources needed and a short timeline. The open question is whether the effect survives outside the tidy conditions of the pilot.", + "Practical to stand up soon. The real exposure is that the lever's active range is narrower than the summary implies, so the pilot should probe that window.", + ]); + const fal = pr.choice([ + "No measurable shift in the primary readout when the intervention is applied at a realistic setting, or the effect reproduced by the inert control.", + "A flat primary measure across the intervention range, or a change that the matched control reproduces just as well.", + "The orthogonal check failing to move with the primary one, or the whole effect vanishing once a stricter control is added.", + "No dose- or intensity-dependent response where the phenomenon is most acute, or rescue by the plausibly-inert comparison arm.", + ]); return `### Proposal ${i + 1}. ${p.title} **Tournament Elo:** ${elo} · **Generation strategy:** \`${p.strategy}\`${radar} -**The hypothesis.** ${p.summary} +**The hypothesis.** ${p.summary}${at(0)} -**Why it's promising.** ${markers[i]} It survived repeated head-to-head debates against -competing ideas, and reviewers scored it well on novelty and testability. The -mechanism is specific enough to design a decisive experiment around. +**Why it's promising.**${at(1)} ${why} -**Proposed first experiment.** Stand up the relevant model system and apply the -intervention across a short dose range, reading out the primary phenotype with a -quantitative assay plus an orthogonal molecular signature. Include vehicle and a -mechanism-dead control so a positive result is interpretable. +**Proposed first experiment.** ${exp}${expMk} -**Feasibility and risks.** Achievable within a standard wet-lab budget and a -single quarter. The main risk is that the intervention does not reach an active -concentration in the relevant compartment — worth a pilot exposure check first. +**Feasibility and risks.** ${feas} -**What would falsify it.** No dose-dependent shift in the primary readout at a -clinically achievable exposure, or rescue by the mechanism-dead control.${tail}`; +**What would falsify it.** ${fal}${tail}`; }).join("\n\n---\n\n"); // Content figures woven into the relevant upper sections (empty strings when @@ -657,32 +751,67 @@ clinically achievable exposure, or rescue by the mechanism-dead control.${tail}` const compFigs = [figs.elo, figs.lineage].filter(Boolean).join("\n\n"); const comparative = compFigs ? `\n\n${compFigs}` : ""; + const framing = r.choice([ + "The goal above defines a question where a testable, mechanism-anchored answer would materially change what happens next. Across a multi-agent tournament, the system generated candidate hypotheses, critiqued them, and ranked them head-to-head so that only ideas surviving repeated scrutiny rose to the top. The proposals below are the survivors, ordered by tournament Elo.", + "Answering the goal well means turning it into something a team can actually test. The system spread the question across competing agents, let them argue and re-rank, and kept only the ideas that held up under pressure. What follows is that shortlist, ordered by tournament Elo.", + "The question above rewards a concrete, falsifiable answer over a plausible-sounding one. To find it, the system generated many candidate directions, pitted them against each other, and let repeated critique thin the field. The proposals below are what remained, ranked by Elo.", + "A useful answer here is one a lab can act on, not just agree with. The tournament produced candidate hypotheses, stress-tested them against rivals, and promoted the ones that kept winning on substance. Those survivors are listed below in Elo order.", + ]); + const exec = r.choice([ + `The tournament converged on ${top.length} strong candidate${top.length === 1 ? "" : "s"}, led by **${lead ? lead.title : "the top-ranked hypothesis"}**. The leading ideas share a bias toward interventions that are testable with what's already on hand and, where possible, reuse known levers to shorten the path from hypothesis to evidence.`, + `${top.length} candidate${top.length === 1 ? "" : "s"} rose above the rest, with **${lead ? lead.title : "the top-ranked hypothesis"}** in front. What unites the leaders is a preference for cheap, decisive tests over ambitious ones, and for building on established levers rather than inventing from scratch.`, + `After the dust settled, ${top.length} idea${top.length === 1 ? "" : "s"} stood out — **${lead ? lead.title : "the top-ranked hypothesis"}** most of all. The front-runners are linked less by topic than by temperament: each is specified tightly enough to falsify quickly and leans on existing methods to move fast.`, + `The field narrowed to ${top.length} serious contender${top.length === 1 ? "" : "s"}, headed by **${lead ? lead.title : "the top-ranked hypothesis"}**. The common thread among them is pragmatism — testable with current tools, and framed so a null result is as informative as a hit.`, + ]); + const landscape = r.choice([ + "Independent generation strategies (literature-grounded, debate-driven, combination, and out-of-box) were each given room to explore, then forced to compete. Where several strategies nominated the same mechanism, that convergence is treated as a robustness signal rather than redundancy.", + "Several strategies ran in parallel — grounded in prior work, argued out in debate, recombined, and deliberately unconventional — before being made to fight for rank. When different strategies landed on the same idea, we read that agreement as evidence, not repetition.", + "The candidates came from distinct angles: some read off the existing literature, some emerged from debate, some from recombining earlier ideas, and some from deliberately breaking the frame. Overlap between independent angles is counted in an idea's favour rather than pruned as duplication.", + "Generation was intentionally diverse — literature-anchored, adversarial, combinatorial, and contrarian lines all contributed — and then the tournament forced a reckoning. A mechanism that surfaced from more than one line is treated as corroborated, not redundant.", + ]); + const comparativeText = r.choice([ + "The top proposals are not interchangeable: some converge on a shared mechanism (mutually reinforcing evidence), while others are genuinely orthogonal bets worth running in parallel to hedge mechanism risk. Prefer starting with the highest-Elo idea that also has the cheapest decisive experiment.", + "These leaders are not variations on one theme — a few reinforce each other by pointing at the same mechanism, while others are independent wagers best run side by side. The pragmatic opening move is the top-ranked idea whose decisive experiment is also the cheapest.", + "Read together, the proposals split into overlapping bets and genuinely separate ones; the overlaps strengthen each other, the separations hedge against being wrong about the mechanism. Sequence them by starting where high rank meets a low-cost decisive test.", + "The shortlist mixes mutually supporting ideas with orthogonal ones, and both kinds earn their place — one for corroboration, the other for insurance. Begin with whichever high-Elo idea can be settled most cheaply.", + ]); + const rec1 = r.choice([ + "Run the single cheapest decisive experiment for the top proposal first.", + "Start with the top proposal's cheapest experiment that can actually settle it.", + "Spend the first dollar on the most decisive, lowest-cost test of the leader.", + ]); + const rec2 = r.choice([ + "If it clears, add the orthogonal runner-up to hedge mechanism risk.", + "If that holds up, bring in the most independent runner-up as a hedge.", + "Assuming a positive read, run the orthogonal runner-up next to cover the mechanism risk.", + ]); + const rec3 = r.choice([ + "Pre-register every falsification threshold before any hands-on work begins.", + "Fix and record each pass/fail threshold up front, before collecting data.", + "Lock in the falsification criteria in advance so a near-miss can't be argued away.", + ]); + const open = r.choice([ + "Where the evidence was thin, reviewer confidence is lower and a domain expert is most likely to disagree — treat those proposals as exploratory. The tournament optimizes for debate-survivability, not ground truth, so a high Elo is a strong prior, not a proof.", + "The proposals resting on the least support are exactly where an expert would push back hardest; hold them loosely. Remember the ranking rewards ideas that survive argument, which is correlated with being right but is not the same thing.", + "Confidence should track the underlying support, which is uneven — the thinner cases are best read as leads rather than conclusions. A high Elo says an idea withstood scrutiny, not that it is true.", + "Some of these stand on firmer ground than others, and the shakier ones deserve a skeptic's eye before any commitment. The tournament measures how well an idea defends itself, so treat rank as a prior to update, not a verdict.", + ]); + return `# Research proposal **Research goal.** ${goal} ## Problem framing and significance -The goal above defines a question where a testable, mechanism-anchored answer -would materially change what a lab does next. Across a multi-agent tournament, -the system generated candidate hypotheses, critiqued them, and ranked them -head-to-head so that only ideas surviving repeated scrutiny rose to the top. The -proposals below are the survivors, ordered by tournament Elo. +${framing} ## Executive summary -The tournament converged on ${top.length} strong candidate${top.length === 1 ? "" : "s"}, -led by **${lead ? lead.title : "the top-ranked hypothesis"}**. The leading ideas -share a bias toward interventions that are testable with existing models and, -where possible, repurpose known agents to shorten the path from hypothesis to -evidence. +${exec} ## The approach landscape -Independent generation strategies (literature-grounded, debate-driven, -combination, and out-of-box) were each given room to explore, then forced to -compete. Where several strategies nominated the same mechanism, that convergence -is treated as a robustness signal rather than redundancy.${donut} +${landscape}${donut} ## Ranked proposals @@ -690,23 +819,17 @@ ${scores}${sections} ## Comparative assessment -The top proposals are not interchangeable: some converge on a shared pathway -(mutually reinforcing evidence), while others are genuinely orthogonal bets worth -running in parallel to hedge mechanism risk. Prefer starting with the highest-Elo -idea that also has the cheapest decisive experiment.${comparative} +${comparativeText}${comparative} ## Recommended path and sequencing -1. Run the single cheapest decisive experiment for the top proposal first. -2. If it clears, add the orthogonal runner-up to hedge mechanism risk. -3. Pre-register every falsification threshold before wet-lab work begins. +1. ${rec1} +2. ${rec2} +3. ${rec3} ## Open questions and limitations -Where the literature was thin, reviewer confidence is lower and a domain expert -is most likely to disagree — treat those proposals as exploratory. The tournament -optimizes for debate-survivability, not ground truth, so a high Elo is a strong -prior, not a proof. +${open} ## Analysis @@ -870,24 +993,52 @@ export function makeSelfCritique(goal: string, roundNo: number, top: CritiqueHyp const opener = CRITIQUE_OPENERS[(roundNo - 1) % CRITIQUE_OPENERS.length]; const closer = CRITIQUE_CLOSERS[(roundNo - 1) % CRITIQUE_CLOSERS.length]; + // Seeded stitching banks so the connective sentences vary by round + target, + // instead of being identical every session. The angle/opener/closer already + // rotate; this varies the prose that links them. + const cr = makeRng(`${goal}|selfcritique|${roundNo}|${title}`); + const lowStr = sc[lowDim].toFixed(2); + const eTxt = eloTxt(target.elo); let priorRef: string; if (roundNo > 1) { const prev = list[(roundNo - 2) % list.length]; const prevAngle = CRITIQUE_ANGLES[(roundNo - 2) % CRITIQUE_ANGLES.length]; - priorRef = `Round ${roundNo - 1} probed the ${prevAngle.name} in **${(prev.title || "an untitled idea").trim()}**; this round I turn to the ${angle.name} in **${title}**.`; + const prevTitle = (prev.title || "an untitled idea").trim(); + priorRef = cr.choice([ + `Round ${roundNo - 1} probed the ${prevAngle.name} in **${prevTitle}**; this round I turn to the ${angle.name} in **${title}**.`, + `Last round it was the ${prevAngle.name} in **${prevTitle}**. Now I switch targets to **${title}** and press on its ${angle.name}.`, + `Having leaned on the ${prevAngle.name} of **${prevTitle}** in round ${roundNo - 1}, I move to a different idea and a different axis: the ${angle.name} in **${title}**.`, + ]); } else { - priorRef = `This is the first critique pass, so I start by attacking the current leader's ${angle.name}.`; + priorRef = cr.choice([ + `This is the first critique pass, so I start by attacking the current leader's ${angle.name}.`, + `First pass — I open on the leader and go straight at its ${angle.name}.`, + `Nothing to compare against yet, so I begin where the leader looks softest: its ${angle.name}.`, + ]); } + const reread = cr.choice([ + `I re-read **${title}** (${eTxt}) — its last review landed at ${scoreLine}, verdict *${rv.verdict}*. The softest mark is **${lowDim}** (${lowStr}), and that is exactly where a ${angle.name} problem would bite.`, + `Back to **${title}** (${eTxt}). The scorecard reads ${scoreLine}, verdict *${rv.verdict}*; **${lowDim}** (${lowStr}) is the weakest line, and a ${angle.name} flaw would land right there.`, + `Looking again at **${title}** (${eTxt}): review scores ${scoreLine}, verdict *${rv.verdict}*. Its low mark is **${lowDim}** (${lowStr}) — the same place a ${angle.name} problem would do the most damage.`, + ]); const thinking = `Round ${roundNo}. ${priorRef}\n\n` + - `I re-read **${title}** (${eloTxt(target.elo)}) — its last review landed at ${scoreLine}, verdict *${rv.verdict}*. The softest mark is **${lowDim}** (${sc[lowDim].toFixed(2)}), and that is exactly where a ${angle.name} problem would bite.\n\n` + + `${reread}\n\n` + angle.probes.map((p, i) => `${i + 1}. ${p}`).join("\n"); - const critique = - `${opener} Looking hard at **${title}**, I am not convinced. The weak axis this round is **${angle.name}**: ${angle.body}.\n\n` + - `Its ${lowDim} score (${sc[lowDim].toFixed(2)}) is the softest on its scorecard, so ${angle.threat}. If that holds, the verdict of *${rv.verdict}* is generous and the ${eloTxt(target.elo)} gap to the field is doing more work than the evidence supports.\n\n` + - `${closer}`; + + const doubt = cr.choice([ + `Looking hard at **${title}**, I am not convinced. The weak axis this round is **${angle.name}**: ${angle.body}.`, + `I read **${title}** against the grain and it does not fully hold up. The exposed axis is **${angle.name}** — ${angle.body}.`, + `Pressing on **${title}**, my doubt sharpens rather than fades. It turns on **${angle.name}**: ${angle.body}.`, + ]); + const stakes = cr.choice([ + `Its ${lowDim} score (${lowStr}) is the softest on its scorecard, so ${angle.threat}. If that holds, the verdict of *${rv.verdict}* is generous and the ${eTxt} gap to the field is doing more work than the evidence supports.`, + `With ${lowDim} already the lowest mark (${lowStr}), ${angle.threat}. Should that be right, *${rv.verdict}* flatters it, and its ${eTxt} lead is resting on argument more than proof.`, + `The ${lowStr} on ${lowDim} is where it is thinnest, which means ${angle.threat}. If so, calling it *${rv.verdict}* is charitable and the ${eTxt} margin overstates the case.`, + ]); + const critique = `${opener} ${doubt}\n\n${stakes}\n\n${closer}`; return `## Thinking\n\n${thinking}\n\n## Self-critique\n\n${critique}`; } @@ -997,26 +1148,51 @@ export function makeStressReport(goal: string, hyp: StressHyp, roundInfo: { roun }; const scoreRow = REVIEW_DIMS.map((d) => `${d} ${sc[d].toFixed(2)} → ${after[d].toFixed(2)}`).join(" · "); + // Seeded prose banks (drawn AFTER the numeric picks so those stay stable). + // Only the connective sentences vary; the report's bold section frame is fixed. + const citeTail = r.choice([ + `on re-reading, it backs a ~${haircut}% smaller effect than the summary implies once a stricter control is added`, + `read closely, it supports an effect about ${haircut}% weaker than the claim, and only before the stricter control`, + `the actual result is ~${haircut}% below what the summary leans on it for once you tighten the control`, + ]); const citeTitles = [...new Set(cites.map((c) => (c.title || "untitled source").trim()))]; const citationLine = citeTitles.length - ? citeTitles.slice(0, 2).map((t) => - `- *${t}* — on re-reading, it backs a ~${haircut}% smaller effect than the summary implies once a stricter control is added.`, - ).join("\n") + ? citeTitles.slice(0, 2).map((t) => `- *${t}* — ${citeTail}.`).join("\n") : "- No sources were attached — flagging the citation gap as a finding: the claim currently rests on uncited reasoning."; + const breakLine = r.choice([ + `Stress round ${roundInfo.round}/${roundInfo.of}. I am trying to *break* **${title}**, not defend it.`, + `Stress round ${roundInfo.round}/${roundInfo.of}. My job here is to falsify **${title}**, not to make its case.`, + `Stress round ${roundInfo.round}/${roundInfo.of}. I approach **${title}** as an adversary looking for the crack, not an advocate.`, + ]); + const claimLine = r.choice([ + `Its core claim: “${gist}”. That lever is what I have to falsify.`, + `The claim under fire: “${gist}”. If it is wrong, that is where it breaks.`, + `What it asserts: “${gist}”. This is the load-bearing lever I need to knock over.`, + ]); + const attackLead = r.choice([ + `**What I attacked.** I targeted the idea's core claim — “${gist}” — and ${probe.attack}.`, + `**What I attacked.** Going straight at the central claim — “${gist}” — I ${probe.attack}.`, + `**What I attacked.** I took aim at the load-bearing claim — “${gist}” — and ${probe.attack}.`, + ]); + const feasLine = r.choice([ + `**Feasibility numbers.** At a realistic exposure the predicted effect is ~${effect}% of the outcome measure — above noise, but the margin is thin, so any pilot must be powered for it.`, + `**Feasibility numbers.** Under realistic conditions the effect works out to ~${effect}% of the outcome — it clears noise, but only just, so a pilot needs real statistical power.`, + `**Feasibility numbers.** The back-of-envelope effect is ~${effect}% of the measure at a plausible setting — detectable, yet close enough to noise that an underpowered pilot would miss it.`, + ]); const thinking = - `Stress round ${roundInfo.round}/${roundInfo.of}. I am trying to *break* **${title}**, not defend it.\n\n` + - `Its core claim: “${gist}”. That lever is what I have to falsify.\n\n` + + `${breakLine}\n\n` + + `${claimLine}\n\n` + `1. Adversarial search: what published result, if it exists, would kill this specific claim?\n` + `2. Citation audit: ${nCites ? `re-open each of the ${nCites} supporting reference(s) and ask whether it shows *this* effect or an adjacent one` : "there are no attached sources, so the absence of evidence is itself the first finding"}.\n` + `3. Feasibility math: put rough numbers on the lever to see if the claimed effect is plausible at a realistic dose/setting.\n` + `4. Design the cheapest experiment that could falsify it at prototype scale — before anyone commits real resources.`; const report = `${token} — ${driver}.\n\n` + - `**What I attacked.** I targeted the idea's core claim — “${gist}” — and ${probe.attack}.\n\n` + + `${attackLead}\n\n` + `**Found evidence.**\n${citationLine}\n\n` + `**Scores before → after fix.** ${scoreRow}.\n\n` + - `**Feasibility numbers.** At a realistic exposure the predicted effect is ~${effect}% of the outcome measure — above noise, but the margin is thin, so any pilot must be powered for it.\n\n` + + `${feasLine}\n\n` + `**Prototype-scale pilot (run this BEFORE scaling).**\n` + `- *Model:* the smallest faithful test bed for “${title.slice(0, 60)}”.\n` + `- *Intervention:* the hypothesis's own lever, a single dose/setting.\n` + @@ -1028,13 +1204,26 @@ export function makeStressReport(goal: string, hyp: StressHyp, roundInfo: { roun /** Title + summary for the stress-hardened fix child. Shared contract with * webapp/content.py `make_stress_fix`. Deterministic (seeded by hyp id). */ -export function makeStressFix(hyp: { id: string; title: string }): { title: string; summary: string } { +export function makeStressFix(hyp: { id: string; title: string }): { title: string; summary: string; thinking: string } { const title = (hyp.title || "an untitled idea").trim(); const r = makeRng(`fix|${hyp.id}`); const fix = r.choice(STRESS_FIXES); + const thinking = [ + r.choice([ + `The stress test on “${title}” found a real but bounded weakness, so I kept the mechanism and redesigned around the failure mode.`, + `Rather than abandon “${title}”, I isolated the one place the stress test broke it and closed that gap specifically.`, + `“${title}” survived scrutiny except at a single seam; this revision targets exactly that seam and nothing else.`, + ]), + r.choice([ + "The change is deliberately conservative — narrow the claim to what the evidence defends and add the control the test showed was load-bearing.", + "I resisted broadening the idea; the fix only removes the failure the test exposed, so the comparison to the parent stays clean.", + "Keeping the edit minimal means a re-rank measures the fix, not a wholesale rewrite.", + ]), + ].join(" "); return { title: `${title} — hardened`, summary: `A stress-hardened revision of “${title}” that ${fix}. Same core mechanism, but the failure mode the stress test surfaced is now designed out before scaling.`, + thinking, }; } diff --git a/frontend/src/lib/sim/engine.ts b/frontend/src/lib/sim/engine.ts index 2a16072..8c723f4 100644 --- a/frontend/src/lib/sim/engine.ts +++ b/frontend/src/lib/sim/engine.ts @@ -93,7 +93,25 @@ interface PlanHyp { title: string; summary: string; full_text: string; citations: { title: string; url: string; excerpt: string | null; doi: string | null; year: number | null }[]; review: HypReview; + thinking: string; // varied synthetic reasoning (sim) / model reasoning (groq) } + +/** Varied match rationales so the tournament feed doesn't repeat one sentence. + * Picked per match off the plan RNG (deterministic per session). */ +const MATCH_RATIONALES = [ + "gave a sharper falsification criterion", + "offered a cleaner causal mechanism", + "proposed a more decisive experiment", + "held up better under cross-examination", + "rested on stronger, more direct evidence", + "made a more specific, testable claim", +]; +const RERANK_RATIONALES = [ + "held up under re-examination", + "survived a second look", + "kept its edge on the low-K rematch", + "did not wobble when re-scored", +]; interface PlanMatch { id: string; t: number; hyp_a: string; hyp_b: string; mode: string; winner: "a" | "b"; elo_a_before: number; elo_b_before: number; @@ -269,6 +287,9 @@ function buildPlan(rec: SimRecord): Plan { scores: { novelty: g.novelty, correctness: g.correctness, testability: g.testability, feasibility: g.feasibility }, body: g.critique, } as HypReview, + // Groq hyps carry the model's own reasoning (a genuine trace); template + // hyps get the varied synthetic thinking below. + thinking: g.reasoning, }; } const c = makeHypothesis(goal, idx, strategy); @@ -278,6 +299,7 @@ function buildPlan(rec: SimRecord): Plan { // REAL_PAPERS fallback so the zero-backend offline demo still cites papers. title: c.title, summary: c.summary, full_text: c.full_text, citations: real ?? c.citations, review: { verdict: rv.verdict, scores: rv.scores, body: rv.body } as HypReview, + thinking: c.thinking, }; }; /** Win probability from each idea's fixed quality anchor (seed Elo, elo0), not @@ -302,6 +324,7 @@ function buildPlan(rec: SimRecord): Plan { id: hypId(idx), idx, strategy, created_by: createdBy, parents, tCreate: t, tReview: t, elo0: seedElo(hypId(idx), idx, parents), title: c.title, summary: c.summary, full_text: c.full_text, citations: c.citations, review: c.review, + thinking: c.thinking, }; hyps.push(h); elo.set(h.id, h.elo0); @@ -327,7 +350,7 @@ function buildPlan(rec: SimRecord): Plan { matches.push({ id: mid, t, hyp_a: a.id, hyp_b: b.id, mode, winner, elo_a_before: ea, elo_b_before: eb, elo_a_after: ra, elo_b_after: rb, - rationale: `Idea ${winner.toUpperCase()} gave a sharper falsification criterion.`, + rationale: `Idea ${winner.toUpperCase()} ${r.choice(MATCH_RATIONALES)}.`, similarity: round2(r.uniform(0.05, 0.4)), }); elo.set(a.id, ra); elo.set(b.id, rb); @@ -416,7 +439,7 @@ function buildPlan(rec: SimRecord): Plan { matches.push({ id: mid, t, hyp_a: a.id, hyp_b: b.id, mode, winner, elo_a_before: ea, elo_b_before: eb, elo_a_after: ra, elo_b_after: rb, - rationale: `Idea ${winner.toUpperCase()} held up under re-examination.`, + rationale: `Idea ${winner.toUpperCase()} ${r.choice(RERANK_RATIONALES)}.`, similarity: round2(r.uniform(0.05, 0.4)), }); elo.set(a.id, ra); elo.set(b.id, rb); @@ -469,6 +492,7 @@ function buildPlan(rec: SimRecord): Plan { full_text: `## Hardening\n\n${fix.summary}`, citations: h.citations, review: { verdict: rv.verdict, scores: rv.scores, body: rv.body }, + thinking: fix.thinking, }; hyps.push(child); elo.set(child.id, childElo); @@ -496,10 +520,14 @@ function buildPlan(rec: SimRecord): Plan { for (let bi = 0; bi < 2 && hyps.length >= 2; bi++) { const [a, b] = r.sample(hyps, 2); const w: "a" | "b" = r.random() < pWinA(a, b) ? "a" : "b"; - stressMatch(a, b, w, 6, `Idea held up under re-examination.`); + stressMatch(a, b, w, 6, `Idea ${w.toUpperCase()} ${r.choice(RERANK_RATIONALES)}.`); } for (const { parent, child } of pairs) { - stressMatch(child, parent, "a", 16, "Hardened revision beat its parent under stress re-test."); + stressMatch(child, parent, "a", 16, r.choice([ + "Hardened revision beat its parent under stress re-test.", + "The stress-hardened child outscored the original once re-tested.", + "Fix child overtook its parent after the stress re-rank.", + ])); } // stress_ranking summary, ordered by the fix children's final Elo. t += 0.8; @@ -752,6 +780,7 @@ function toHypothesis(s: Snapshot, h: PlanHyp): Hypothesis { dedup_cluster: clusterId(h.idx, s.rec.n_initial), n_reviews: reviewed ? 1 : 0, scores: reviewed ? h.review.scores : {}, + thinking: h.thinking || undefined, }; } diff --git a/frontend/src/lib/sim/generate.ts b/frontend/src/lib/sim/generate.ts index 185dd97..f094ae0 100644 --- a/frontend/src/lib/sim/generate.ts +++ b/frontend/src/lib/sim/generate.ts @@ -27,6 +27,9 @@ export interface GenHyp { testability: number; feasibility: number; critique: string; + /** The model's own short rationale for proposing this — flows into the + * hypothesis `thinking` field (a genuine reasoning trace for BYOK hyps). */ + reasoning: string; } export interface GeneratedContent { @@ -85,7 +88,8 @@ export async function generateSession( ` "predicted_outcome": "1 sentence on the expected result if the hypothesis holds",\n` + ` "verdict": "one of: well_grounded, promising, needs_work, speculative",\n` + ` "novelty": 0.0-1.0, "correctness": 0.0-1.0, "testability": 0.0-1.0, "feasibility": 0.0-1.0,\n` + - ` "critique": "2-3 sentence critical review naming the key risk or assumption"\n` + + ` "critique": "2-3 sentence critical review naming the key risk or assumption",\n` + + ` "reasoning": "2-3 sentences of first-person reasoning: WHY you proposed this and what you weighed (a genuine thought trace, distinct from the critique)"\n` + ` }\n` + ` ],\n` + ` "overview": "200-320 word markdown overview organized as these three '## ' sections, in order and with these exact headings: '## Executive summary' (what the hypotheses converge on and the leading direction), '## Proposed directions' (the cross-cutting themes ACROSS these specific hypotheses and the recommended next experiments, referencing the actual hypotheses), and '## Limitations' (the key risks or assumptions). Do NOT invent citations, papers, DOIs, or URLs — a References note is appended automatically stating that no sources were retrieved in this mode."\n` + @@ -118,6 +122,7 @@ export async function generateSession( testability: clamp01(h?.testability), feasibility: clamp01(h?.feasibility), critique: str(h?.critique), + reasoning: str(h?.reasoning), })); return { hyps, overview: str(data?.overview), source: provider }; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index c08ff32..672d2e7 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -89,6 +89,10 @@ export interface Hypothesis { citations?: Citation[]; reviews?: Review[]; elo_history?: { t: string; elo: number }[]; + /** Varied synthetic reasoning for sim hyps (labelled SIMULATED in the UI); the + * model's real rationale for BYOK/Groq hyps. Optional — populated by the sim + * engine. */ + thinking?: string; } export interface Citation { diff --git a/webapp/content.py b/webapp/content.py index a65d102..a21093e 100644 --- a/webapp/content.py +++ b/webapp/content.py @@ -27,43 +27,221 @@ "supervisor": "claude-sonnet-4-6", } -# Templated mechanism fragments — combined to make distinct hypothesis bodies. -_MECHANISMS = [ - ("Repurposing {drug} via {pathway} modulation", - "{drug} is a clinically approved agent whose off-target inhibition of " - "{pathway} may suppress the disease-driving program identified in the goal."), - ("{pathway} blockade reverses the {phenotype} phenotype", - "Sustained {pathway} signaling maintains {phenotype}; pharmacological " - "blockade should collapse the feed-forward loop and restore homeostasis."), - ("Synthetic-lethal targeting of {gene} in {context}", - "Cells in {context} become dependent on {gene}; a selective inhibitor " - "exploits this dependency while sparing normal tissue."), - ("{microbe}-derived metabolites drive {phenotype}", - "Host exposure to {microbe} metabolites rewires {pathway}, providing a " - "tractable, diet-modifiable lever over {phenotype}."), - ("Combination of {drug} and {pathway} inhibition is synergistic", - "Each agent alone is sub-therapeutic, but co-inhibition closes a " - "compensatory escape route, predicting a strong synergy index."), - ("Epigenetic priming sensitizes {context} to {drug}", - "Low-dose epigenetic priming reopens silenced loci, restoring " - "{drug} sensitivity in otherwise refractory {context}."), +# --------------------------------------------------------------------------- # +# Prompt understanding (deterministic, no network) — mirrors sim/content.ts so a +# NON-biomedical goal (e.g. "a reasoning method better than chain-of-thought") +# yields on-topic hypotheses instead of drug/pathway mad-libs. Domain is inferred +# from the goal's own vocabulary; biomedicine is one domain among several. +# --------------------------------------------------------------------------- # + +_STOPWORDS = { + "the", "a", "an", "and", "or", "of", "to", "in", "on", "for", "with", "by", + "at", "from", "into", "as", "is", "are", "be", "will", "can", "could", "would", + "that", "this", "these", "those", "it", "its", "their", "our", "your", "we", + "how", "what", "why", "which", "using", "use", "via", "based", "new", "novel", + "testable", "strategies", "strategy", "mechanisms", "mechanism", "ways", "way", + "approach", "approaches", "study", "research", "goal", "propose", "proposing", + "find", "finding", "identify", "identifying", "generate", "generating", + "discover", "explore", "investigate", "develop", "improve", "improving", + "reduce", "reducing", "increase", "increasing", "extend", "extending", + "overcome", "overcoming", "between", "linking", "across", "within", +} + +_ACTION_VERBS = [ + "reduce", "lower", "cut", "decrease", "minimize", "minimise", "prevent", + "eliminate", "improve", "increase", "boost", "enhance", "raise", "maximize", + "maximise", "extend", "expand", "accelerate", "optimize", "optimise", + "strengthen", "overcome", "restore", "stabilize", "stabilise", +] + +_TOKEN_RE = re.compile(r"[A-Za-z][A-Za-z0-9+–-]*") # noqa: RUF001 — en dash matches hyphenated terms (mirrors sim/content.ts) + + +def _extract_keywords(goal: str) -> tuple[list[str], list[str]]: + """(unigrams, topics): content unigrams + contiguous content-word phrases, + longest/most-specific first. Mirrors sim/content.ts extractKeywords.""" + tokens = _TOKEN_RE.findall(goal) + unigrams: list[str] = [] + phrases: list[str] = [] + run: list[str] = [] + + def flush() -> None: + if run: + phrases.append(" ".join(run)) + run.clear() + + for tok in tokens: + low = tok.lower() + if len(tok) > 2 and low not in _STOPWORDS: + run.append(tok) + unigrams.append(low) + else: + flush() + flush() + topics = list(dict.fromkeys(" ".join(p.split()[:4]) for p in phrases)) + topics.sort(key=lambda p: (-len(p.split()), -len(p))) + return unigrams, topics + + +_DOMAINS = [ + { + "id": "transportation", + "match": ["traffic", "congestion", "transit", "road", "commute", "vehicle", + "mobility", "urban", "city", "parking", "transport", "bus", "rail", + "driving", "highway", "pedestrian"], + "levers": ["dynamic congestion pricing", "adaptive signal control", + "dedicated priority lanes", "demand-responsive routing", + "a mode-shift incentive", "real-time rerouting"], + "metric": "average travel time", "unit": "%", + "methods": ["a calibrated traffic microsimulation", + "a before-after field study on a corridor", + "a staggered rollout across zones"], + }, + { + "id": "energy-materials", + "match": ["battery", "batteries", "lithium", "lithium-ion", "ion", "energy", + "solar", "wind", "grid", "turbine", "storage", "material", + "photovoltaic", "fuel", "hydrogen", "electrode", "electrolyte", + "power", "thermal", "capacity", "charge"], + "levers": ["a protective interface coating", "a tuned operating-temperature window", + "a materials substitution", "a smart charge controller", + "an electrolyte additive"], + "metric": "cycle-life retention", "unit": "%", + "methods": ["accelerated cycling on a test bench", "a controlled bench experiment", + "a paired A/B hardware trial"], + }, + { + "id": "computing", + "match": ["model", "algorithm", "software", "data", "network", "compute", + "latency", "system", "code", "server", "database", "inference", + "cache", "gpu", "throughput", "distributed", "spreadsheet", + "layout", "app", "ui", "interface", "dashboard", "reasoning", + "prompt", "prompting", "llm", "transformer", "agent"], + "levers": ["an algorithmic redesign", "a caching layer", + "a scheduling-policy change", "a model-architecture tweak", + "a batching strategy"], + "metric": "end-to-end latency", "unit": "%", + "methods": ["a benchmark with ablations", "an A/B experiment in staging", + "a load test under production-like traffic"], + }, + { + "id": "education-social", + "match": ["student", "students", "learning", "education", "retention", + "teach", "school", "college", "training", "course", "curriculum", + "literacy", "classroom", "tutor", "graduation"], + "levers": ["a structured mentoring program", "a low-cost behavioral nudge", + "a curriculum redesign", "an early-warning outreach", + "a peer-support cohort"], + "metric": "retention rate", "unit": "%", + "methods": ["a randomized controlled trial", "a difference-in-differences study", + "a stepped-wedge pilot"], + }, + { + "id": "economics-business", + "match": ["market", "price", "pricing", "cost", "revenue", "customer", + "supply", "demand", "business", "retail", "sales", "inventory", + "logistics", "churn", "profit", "supermarket", "supermarkets", + "food", "grocery", "perishable", "spoilage", "stock"], + "levers": ["dynamic pricing", "a demand-forecasting model", + "a process redesign", "a targeted incentive", + "an inventory-routing change"], + "metric": "unit cost", "unit": "%", + "methods": ["an A/B pricing experiment", "a controlled pilot in selected sites", + "a holdout-group trial"], + }, + { + "id": "climate-environment", + "match": ["climate", "carbon", "emission", "emissions", "pollution", + "pollutant", "air", "smog", "aqi", "ecosystem", "water", + "sustainability", "sustainable", "recycling", "biodiversity", + "greenhouse", "renewable"], + "levers": ["a deployment incentive", "a behavioral nudge", + "a process electrification", "a monitoring-and-feedback loop", + "a policy instrument"], + "metric": "emissions intensity", "unit": "%", + "methods": ["a field trial with matched controls", "a monitored pilot deployment", + "a scenario simulation"], + }, + { + "id": "biomedicine", + # NB: "cell"/"cells" intentionally omitted — too ambiguous (spreadsheet / + # battery / phone cell). Real bio prompts hit cancer/tumor/gene/organoid/etc. + "match": ["gene", "genetic", "protein", "disease", "cancer", "drug", "tissue", + "organoid", "microbiome", "patient", "clinical", "neuro", + "neuroinflammation", "therapy", "therapeutic", "molecular", + "immune", "blood", "brain", "metabolic", "tumor", "leukemia", + "antibody", "biomarker", "inflammation", "senescence", "fibrotic"], + "levers": ["a repurposed approved compound", "a targeted pathway inhibitor", + "a genetic perturbation", "a combination regimen", + "an epigenetic priming step"], + "metric": "the disease-signature score", "unit": "%", + "methods": ["an in-vitro assay in a relevant model", + "an isogenic knockdown experiment", "a dose-response study"], + }, ] -_DRUGS = ["Nanvuranlat", "KIRA6", "Leflunomide", "Binimetinib", "Pacritinib", - "Cerivastatin", "Dimethyl fumarate", "Metformin", "Disulfiram", - "Niclosamide", "Auranofin", "Itraconazole"] -_PATHWAYS = ["IRE1α–XBP1", "DHODH", "MEK/ERK", "JAK2/STAT5", "mevalonate", - "NRF2–KEAP1", "Wnt/β-catenin", "mTORC1", "ferroptosis", "cGAS–STING"] -_GENES = ["WRN", "PRMT5", "MAT2A", "USP1", "POLQ", "WEE1", "ATR"] -_PHENOTYPES = ["chronic inflammation", "drug tolerance", "stemness", - "immune evasion", "fibrotic remodeling", "metabolic rewiring"] -_MICROBES = ["Akkermansia muciniphila", "Faecalibacterium prausnitzii", - "Bacteroides fragilis", "segmented filamentous bacteria"] -_CONTEXTS = ["AML blasts", "senescent fibroblasts", "exhausted CD8 T cells", - "drug-tolerant persister cells", "the inflamed gut epithelium"] - -_JOURNALS = ["Nature", "Cell", "Science", "Nature Medicine", "Cell Metabolism", - "Immunity", "Nature Cancer", "PNAS", "eLife", "Blood"] +_GENERIC = { + "id": "generic", "match": [], + "levers": ["a targeted intervention", "a structural redesign", + "a data-driven policy", "an automated feedback controller", + "an incentive realignment", "an early screening step"], + "metric": "the primary outcome measure", "unit": "%", + "methods": ["a controlled pilot study", "a randomized experiment", + "a simulation calibrated to real data", + "a field trial with matched controls"], +} + + +def _infer_domain(unigrams: list[str]) -> dict: + """Pick the domain whose vocabulary best matches the prompt (else generic). + Tolerates simple plurals; counts DISTINCT stems. Mirrors sim/content.ts.""" + stems = set() + for u in unigrams: + stems.add(u) + if u.endswith("s") and len(u) > 3: + stems.add(u[:-1]) + best, best_score = _GENERIC, 0 + for d in _DOMAINS: + hits = set() + for m in d["match"]: + stem = m[:-1] if m.endswith("s") and len(m) > 3 else m + if m in stems or stem in stems: + hits.add(stem) + if len(hits) > best_score: + best, best_score = d, len(hits) + return best + + +def _cap(s: str) -> str: + return s[0].upper() + s[1:] if s else s + + +def _clip(s: str, n: int) -> str: + if len(s) <= n: + return s + return re.sub(r"\s+\S*$", "", s[:n]) + "…" + + +def _goal_aim(goal: str) -> str: + """A short, prompt-grounded 'aim' clause (uses the prompt's own words).""" + lower = goal.lower() + idx = -1 + for v in _ACTION_VERBS: + m = re.search(rf"\b{v}\b", lower) + if m and (idx < 0 or m.start() < idx): + idx = m.start() + clause = goal[idx:] if idx >= 0 else goal + return _clip(re.sub(r"[.?!]+$", "", clause).strip().lower(), 90) + + +_TITLE_SCAFFOLDS = [ + lambda t, lv, lv2, m, me: f"{_cap(lv)} improves {m} in {t}", + lambda t, lv, lv2, m, me: f"{_cap(lv)} as a lever for {t}", + lambda t, lv, lv2, m, me: f"Combining {lv} and {lv2} in {t}", + lambda t, lv, lv2, m, me: f"{_cap(lv)} for {t}, tested via {me}", + lambda t, lv, lv2, m, me: f"Introducing {lv} early reduces failure in {t}", + lambda t, lv, lv2, m, me: f"{_cap(lv)} shifts {m} in {t}", +] # Curated pool of REAL, landmark papers. Every DOI was verified to resolve to a # live publisher page (curl -sI -L → HTTP 200; transcript in the PR). The keyless @@ -140,53 +318,90 @@ def _rng(seed_text: str) -> random.Random: def make_hypothesis(goal: str, idx: int, strategy: str) -> dict: r = _rng(f"{goal}|{idx}|{strategy}") - mech_title, mech_body = r.choice(_MECHANISMS) - fill = { - "drug": r.choice(_DRUGS), - "pathway": r.choice(_PATHWAYS), - "gene": r.choice(_GENES), - "phenotype": r.choice(_PHENOTYPES), - "microbe": r.choice(_MICROBES), - "context": r.choice(_CONTEXTS), - } - title = mech_title.format(**fill) + unigrams, topics = _extract_keywords(goal) + dom = _infer_domain(unigrams) + aim = _goal_aim(goal) + # Rotate through the prompt's own noun phrases so hypotheses cover its facets. + pool = topics or [_clip(goal, 48)] + topic = pool[idx % len(pool)] or _clip(goal, 48) + lever = r.choice(dom["levers"]) + lever2 = r.choice(dom["levers"]) + if lever2 == lever: + lever2 = dom["levers"][(dom["levers"].index(lever) + 1) % len(dom["levers"])] + method = r.choice(dom["methods"]) + pct = r.randint(15, 45) + + scaffold = _TITLE_SCAFFOLDS[idx % len(_TITLE_SCAFFOLDS)] + # Deep mode can ask for many hypotheses but there are only a few scaffolds — + # past the first rotation, tag a variant number so titles stay distinct. + cycle = idx // len(_TITLE_SCAFFOLDS) + base = _clip(scaffold(topic, lever, lever2, dom["metric"], method), + 96 if cycle > 0 else 110) + title = f"{base} — variant {cycle + 1}" if cycle > 0 else base summary = ( - mech_body.format(**fill) + " The hypothesis is directly testable in " - f"existing {fill['context']} models with a clear, quantitative readout." + f"{_cap(lever)} is a plausible lever to {aim}. The effect should appear as " + f"a measurable change in {dom['metric']}, making it directly testable via " + f"{method} against a pre-registered threshold." ) + combine_note = f" together with {lever2}" if strategy == "combine" else "" full_text = f"""## Mechanism -{mech_body.format(**fill)} - -We propose that **{fill['pathway']}** acts as the central node linking the -upstream stimulus to **{fill['phenotype']}** observed in {fill['context']}. - -## Rationale - -1. Genetic perturbation of {fill['gene']} phenocopies the proposed intervention. -2. {fill['drug']} is already approved, de-risking translation and toxicity. -3. The pathway is druggable with sub-micromolar tool compounds. +We hypothesise that **{lever}** acts on the core driver of {topic}, and that this +propagates to a measurable shift in **{dom['metric']}**. The link to the stated +goal — *{aim}* — is direct: if the lever works, the outcome moves; if it does +not, the outcome is unchanged, giving a clean falsification. ## Proposed experiment -- **Model:** {fill['context']} (primary + isogenic line). -- **Intervention:** titrate {fill['drug']} ± {fill['pathway']} inhibitor. -- **Primary readout:** reversal of {fill['phenotype']} signature (RNA-seq + functional assay). -- **Controls:** vehicle, isotype, and a pathway-dead mutant rescue. -- **Success criterion:** >50% reduction in the {fill['phenotype']} score at a - clinically achievable exposure. +- **Method:** {_cap(method)}. +- **Intervention:** apply {lever}{combine_note}. +- **Primary readout:** {dom['metric']} (with a matched control condition). +- **Controls:** a no-intervention baseline and a plausibly-inert comparison. +- **Success criterion:** a ≥{pct}{dom['unit']} improvement in {dom['metric']} versus control. ## Predicted outcome -A dose-dependent collapse of the {fill['phenotype']} program with an -EC50 within the approved therapeutic window of {fill['drug']}. +A dose- or intensity-dependent change in {dom['metric']}, concentrated where +{topic} is most acute — with no effect in the inert control arm. """ + # Varied synthetic reasoning — three seeded picks referencing THIS idea's own + # lever/topic/metric/method, so every hypothesis carries a distinct rationale. + thinking = " ".join([ + r.choice([ + f"Starting from the goal — {aim} — I asked which single lever most " + f"plausibly moves {dom['metric']}.", + f"I worked backward from {dom['metric']}: what intervention on {topic} " + f"shifts it most for the least cost?", + f"The prompt points at {topic}; my instinct was to isolate one " + f"mechanism rather than bundle several.", + f"Before committing I weighed a few levers on {topic} and kept the one " + f"with the cleanest read on {dom['metric']}.", + ]), + r.choice([ + f"{_cap(lever)} stood out because its effect on {dom['metric']} should " + f"be direct, not mediated by a long causal chain.", + f"I favoured {lever} since it acts close to the outcome, so a null " + f"result is informative rather than ambiguous.", + f"{_cap(lever)} is concrete enough to specify precisely and cheap " + f"enough to falsify quickly.", + f"The appeal of {lever} is that it fails loudly — if it does nothing " + f"to {dom['metric']}, that rules it out cleanly.", + ]), + r.choice([ + f"Tested with {method}, a ≥{pct}{dom['unit']} move would be decisive; " + f"anything less and I would drop it.", + f"I would run {method} first — it gives a pass/fail on {dom['metric']} " + f"before any larger commitment.", + f"The plan is {method}, kept small and pre-registered so the " + f"{pct}{dom['unit']} threshold actually means something.", + ]), + ]) # Sample distinct REAL landmark papers (verified-resolving DOIs) so the demo's # citations always link to a paper that exists — no fabricated/random DOIs. citations = [_paper_citation(p) for p in r.sample(_REAL_PAPERS, r.randint(2, 4))] return { "title": title, "summary": summary, "full_text": full_text, - "citations": citations, "strategy": strategy, + "citations": citations, "strategy": strategy, "thinking": thinking, } @@ -195,29 +410,73 @@ def make_review(goal: str, hyp_title: str, kind: str) -> dict: verdict = r.choice( ["neutral", "missing_piece", "already_explained", "other_more_likely"] ) + # Draw the scores FIRST so they stay stable regardless of the prose banks + # below (the scorecard, self-critique and stress stages all read these). scores = { "novelty": round(r.uniform(0.45, 0.95), 2), "correctness": round(r.uniform(0.5, 0.95), 2), "testability": round(r.uniform(0.55, 0.98), 2), "feasibility": round(r.uniform(0.4, 0.9), 2), } + # Seeded prose banks — the four dimension paragraphs vary per hyp/session so + # no two reviews read alike, while the **Dimension (score).** labels stay fixed. + nov = r.choice([ + "The proposed lever is under-explored for this goal; adjacent work exists " + "but does not test this exact intervention.", + "A genuinely fresh angle — the surrounding literature circles the idea " + "without landing on this specific move.", + "Not unprecedented, but the particular framing here has not been put to a " + "direct test before.", + "The novelty is in how the pieces are combined rather than the pieces " + "themselves; the specific claim is under-tested.", + ]) + cor = r.choice([ + "Internally consistent — the causal chain from intervention to the primary " + "outcome is plausible, though one upstream assumption (below) is load-bearing.", + "The logic holds together; the weakest link is a single upstream step, " + "flagged below, that the design should pin down.", + "No obvious contradiction, and the mechanism is stated crisply enough to " + "check — one assumption still carries most of the weight.", + "Reasoning is sound end-to-end, with the caveat that the effect depends on " + "an intermediate step that is assumed rather than shown.", + ]) + tes = r.choice([ + "Strong: the readout is quantitative and the proposed method yields a clear " + "pass/fail against the stated threshold.", + "Highly testable — a pre-registered threshold on a measurable readout turns " + "this into a decisive experiment.", + "The claim exposes itself to falsification: one clean measurement either " + "clears the bar or sinks it.", + "Good — the outcome is numeric and the comparison is controlled, so the " + "result won't be open to interpretation.", + ]) + fea = r.choice([ + "Achievable with commonly available methods; the main risk is confounding, " + "which the control arm is designed to absorb.", + "Within reach of a modest setup and timeline; the chief hazard is a confound " + "the baseline arm has to neutralise.", + "Practical to run soon and cheaply — the open question is whether the " + "control fully isolates the effect.", + "No exotic resources required; the sensitivity is to a confounder that the " + "matched comparison is meant to rule out.", + ]) + note = r.choice([ + "that the measured outcome actually reflects the mechanism, not a proxy", + "that the intervention reaches the regime where it can act at all", + "that the control arm removes the most likely alternative explanation", + "that the effect size survives outside the tidy conditions of the pilot", + ]) body = f"""**Verdict:** {verdict} -**Novelty ({scores['novelty']}).** The mechanistic link is under-explored; a -handful of adjacent papers exist but none test this exact intervention. +**Novelty ({scores['novelty']}).** {nov} -**Correctness ({scores['correctness']}).** Internally consistent. The proposed -causal chain is plausible given the cited evidence, though one upstream step -relies on an assumption flagged below. +**Correctness ({scores['correctness']}).** {cor} -**Testability ({scores['testability']}).** Strong — the readout is quantitative -and the reagents are commercially available. +**Testability ({scores['testability']}).** {tes} -**Feasibility ({scores['feasibility']}).** Achievable within a standard wet-lab -budget; the main risk is compound exposure in the relevant compartment. +**Feasibility ({scores['feasibility']}).** {fea} -**Key assumption checked:** that the approved agent reaches the target tissue at -an active concentration. Rated *{r.choice(['plausible', 'uncertain'])}*. +**Key assumption checked:** {note}. Rated *{r.choice(['plausible', 'uncertain'])}*. """ return {"kind": kind, "verdict": verdict, "scores": scores, "body": body} @@ -369,13 +628,15 @@ def cap(body: str | None, text: str) -> str: } -def _overview_refs(top: list[dict]) -> tuple[list[dict], list[str]]: - """Dedupe the proposals' citation objects (curated real papers) into a - numbered reference list, and return a parallel list of per-proposal inline - `[n]` marker strings. Deduped by URL (fallback DOI) so repeated sources share - a number. Consumes the citation objects as-is — nothing extra invented here.""" +def _overview_refs(top: list[dict]) -> tuple[list[dict], list[list[str]]]: + """Dedupe the proposals' OWN citation objects (curated real papers online, or + OpenAlex) into a numbered reference list, and return a parallel per-proposal + list of individual `[n]` marker strings so the caller can spread them across + the sentences that lean on a source. Deduped by URL (fallback DOI) so a paper + cited by two proposals shares one number — the overview, drawer and + per-proposal donut all cite the SAME papers. Mirrors sim/content.ts.""" refs: list[dict] = [] - markers: list[str] = [] + markers: list[list[str]] = [] key_to_n: dict[str, int] = {} for p in top: ns: list[int] = [] @@ -388,8 +649,9 @@ def _overview_refs(top: list[dict]) -> tuple[list[dict], list[str]]: n = len(refs) + 1 key_to_n[key] = n refs.append({"n": n, **c}) - ns.append(n) - markers.append("".join(f"[{n}]" for n in sorted(set(ns)))) + if n not in ns: + ns.append(n) + markers.append([f"[{n}]" for n in sorted(ns)]) return refs, markers @@ -471,11 +733,22 @@ def make_overview(goal: str, proposals: list[dict]) -> str: f"{c.get('url') or ('https://doi.org/' + c['doi'] if c.get('doi') else '')}".rstrip() for c in refs ) or "No verifiable citations were gathered." + # Seeded per SESSION (the proposal ids carry the session id) so two runs of + # the same goal read differently while a re-render is stable. Top-level + # sections draw from `r`; each proposal block gets its own stream. + seed_tail = "|".join(p.get("id") or p.get("title", "") for p in top) + r = _rng(f"{goal}|overview|{seed_tail}") sections = [] for i, p in enumerate(top): elo = round(p["elo"]) if p.get("elo") is not None else "—" - mk = f" {markers[i]}" if markers[i] else "" + pr = _rng(f"{goal}|proposal|{p.get('id') or p.get('title')}") + # Spread this proposal's own citation markers across the sentences that + # lean on a source: the claim, why-it's-promising, and the experiment. + cm = markers[i] + at0 = f" {cm[0]}" if len(cm) > 0 else "" + at1 = f" {cm[1]}" if len(cm) > 1 else "" + exp_mk = f" {''.join(cm[2:])}" if len(cm) > 2 else "" # Per-proposal illustrations for the top-3: a compact score radar on the # Elo line, plus an experiment-pipeline mermaid and a citations mini-donut # at the END of the block. All UNNUMBERED (chart title only) so the @@ -491,27 +764,71 @@ def make_overview(goal: str, proposals: list[dict]) -> str: if donut: end_figs.append(donut) tail = ("\n\n" + "\n\n".join(end_figs)) if end_figs else "" + why = pr.choice([ + "It survived repeated head-to-head debates against competing ideas, and " + "reviewers scored it well on novelty and testability. The mechanism is " + "specific enough to design a decisive experiment around.", + "It kept winning matches on the strength of its argument rather than its " + "framing, and the reviewers' marks back that up. Crucially, it is " + "concrete enough that one experiment can settle it.", + "Across the tournament it beat rivals that were vaguer or harder to " + "test, and it carries a specific, falsifiable claim rather than a " + "direction of travel.", + "The idea earned its rank by holding up under scrutiny, not by " + "out-arguing softer competitors — and its core claim is sharp enough to " + "design a clean test around.", + ]) + exp = pr.choice([ + "Set up the smallest faithful version of the system, apply the " + "intervention across a short range, and read out the primary measure " + "alongside one orthogonal check. Include an untouched baseline and a " + "plausibly-inert comparison so a positive result is interpretable.", + "Run a compact controlled trial: vary the lever over a few settings, " + "measure the primary outcome plus a second independent signal, and hold " + "a matched control so the effect can't be confused with drift.", + "Start with a cheap decisive experiment — the intervention at one or two " + "intensities, a quantitative readout, and both a do-nothing baseline and " + "an inert-looking control to keep the result unambiguous.", + "Build a minimal test bed, apply the intervention against a matched " + "control, and track the primary measure together with an orthogonal one " + "so a real effect and an artefact look different.", + ]) + feas = pr.choice([ + "Achievable within a modest budget and a single cycle. The main risk is " + "that the intervention never reaches the regime where it can act — worth " + "a quick pilot to check that first.", + "Cheap and quick to run. The chief hazard is a hidden confounder " + "producing the same reading, which the control arm is there to absorb.", + "No exotic resources needed and a short timeline. The open question is " + "whether the effect survives outside the tidy conditions of the pilot.", + "Practical to stand up soon. The real exposure is that the lever's " + "active range is narrower than the summary implies, so the pilot should " + "probe that window.", + ]) + fal = pr.choice([ + "No measurable shift in the primary readout when the intervention is " + "applied at a realistic setting, or the effect reproduced by the inert " + "control.", + "A flat primary measure across the intervention range, or a change that " + "the matched control reproduces just as well.", + "The orthogonal check failing to move with the primary one, or the whole " + "effect vanishing once a stricter control is added.", + "No dose- or intensity-dependent response where the phenomenon is most " + "acute, or rescue by the plausibly-inert comparison arm.", + ]) sections.append(f"""### Proposal {i+1}. {p['title']} **Tournament Elo:** {elo} · **Generation strategy:** `{p.get('strategy', 'literature')}`{radar} -**The hypothesis.** {p.get('summary', '').strip()} +**The hypothesis.** {p.get('summary', '').strip()}{at0} -**Why it's promising.**{mk} It survived repeated head-to-head debates against -competing ideas, and reviewers scored it well on novelty and testability. The -mechanism is specific enough to design a decisive experiment around. +**Why it's promising.**{at1} {why} -**Proposed first experiment.** Stand up the relevant model system and apply the -intervention across a short dose range, reading out the primary phenotype with a -quantitative assay plus an orthogonal molecular signature. Include vehicle and a -mechanism-dead control so a positive result is interpretable. +**Proposed first experiment.** {exp}{exp_mk} -**Feasibility and risks.** Achievable within a standard wet-lab budget and a -single quarter. The main risk is that the intervention does not reach an active -concentration in the relevant compartment — worth a pilot exposure check first. +**Feasibility and risks.** {feas} -**What would falsify it.** No dose-dependent shift in the primary readout at a -clinically achievable exposure, or rescue by the mechanism-dead control.{tail}""") +**What would falsify it.** {fal}{tail}""") body = "\n\n---\n\n".join(sections) # Content figures woven into the relevant upper sections (empty strings when @@ -521,31 +838,131 @@ def make_overview(goal: str, proposals: list[dict]) -> str: scores = f"{figs['scores']}\n\n" if figs["scores"] else "" lineage = f"\n\n{figs['lineage']}" if figs["lineage"] else "" + framing = r.choice([ + "The goal above defines a question where a testable, mechanism-anchored " + "answer would materially change what happens next. Across a multi-agent " + "tournament, the system generated candidate hypotheses, critiqued them, and " + "ranked them head-to-head so that only ideas surviving repeated scrutiny " + "rose to the top. The proposals below are the survivors, ordered by " + "tournament Elo.", + "Answering the goal well means turning it into something a team can " + "actually test. The system spread the question across competing agents, let " + "them argue and re-rank, and kept only the ideas that held up under " + "pressure. What follows is that shortlist, ordered by tournament Elo.", + "The question above rewards a concrete, falsifiable answer over a " + "plausible-sounding one. To find it, the system generated many candidate " + "directions, pitted them against each other, and let repeated critique thin " + "the field. The proposals below are what remained, ranked by Elo.", + "A useful answer here is one a lab can act on, not just agree with. The " + "tournament produced candidate hypotheses, stress-tested them against " + "rivals, and promoted the ones that kept winning on substance. Those " + "survivors are listed below in Elo order.", + ]) + exec_summary = r.choice([ + f"The tournament converged on {len(top)} strong " + f"candidate{'' if len(top) == 1 else 's'}, led by **{lead_title}**. The " + "leading ideas share a bias toward interventions that are testable with " + "what's already on hand and, where possible, reuse known levers to shorten " + "the path from hypothesis to evidence.", + f"{len(top)} candidate{'' if len(top) == 1 else 's'} rose above the rest, " + f"with **{lead_title}** in front. What unites the leaders is a preference " + "for cheap, decisive tests over ambitious ones, and for building on " + "established levers rather than inventing from scratch.", + f"After the dust settled, {len(top)} idea{'' if len(top) == 1 else 's'} " + f"stood out — **{lead_title}** most of all. The front-runners are linked " + "less by topic than by temperament: each is specified tightly enough to " + "falsify quickly and leans on existing methods to move fast.", + f"The field narrowed to {len(top)} serious " + f"contender{'' if len(top) == 1 else 's'}, headed by **{lead_title}**. The " + "common thread among them is pragmatism — testable with current tools, and " + "framed so a null result is as informative as a hit.", + ]) + landscape = r.choice([ + "Independent generation strategies (literature-grounded, debate-driven, " + "combination, and out-of-box) were each given room to explore, then forced " + "to compete. Where several strategies nominated the same mechanism, that " + "convergence is treated as a robustness signal rather than redundancy.", + "Several strategies ran in parallel — grounded in prior work, argued out in " + "debate, recombined, and deliberately unconventional — before being made to " + "fight for rank. When different strategies landed on the same idea, we read " + "that agreement as evidence, not repetition.", + "The candidates came from distinct angles: some read off the existing " + "literature, some emerged from debate, some from recombining earlier ideas, " + "and some from deliberately breaking the frame. Overlap between independent " + "angles is counted in an idea's favour rather than pruned as duplication.", + "Generation was intentionally diverse — literature-anchored, adversarial, " + "combinatorial, and contrarian lines all contributed — and then the " + "tournament forced a reckoning. A mechanism that surfaced from more than one " + "line is treated as corroborated, not redundant.", + ]) + comparative = r.choice([ + "The top proposals are not interchangeable: some converge on a shared " + "mechanism (mutually reinforcing evidence), while others are genuinely " + "orthogonal bets worth running in parallel to hedge mechanism risk. Prefer " + "starting with the highest-Elo idea that also has the cheapest decisive " + "experiment.", + "These leaders are not variations on one theme — a few reinforce each other " + "by pointing at the same mechanism, while others are independent wagers best " + "run side by side. The pragmatic opening move is the top-ranked idea whose " + "decisive experiment is also the cheapest.", + "Read together, the proposals split into overlapping bets and genuinely " + "separate ones; the overlaps strengthen each other, the separations hedge " + "against being wrong about the mechanism. Sequence them by starting where " + "high rank meets a low-cost decisive test.", + "The shortlist mixes mutually supporting ideas with orthogonal ones, and " + "both kinds earn their place — one for corroboration, the other for " + "insurance. Begin with whichever high-Elo idea can be settled most cheaply.", + ]) + rec1 = r.choice([ + "Run the single cheapest decisive experiment for the top proposal first.", + "Start with the top proposal's cheapest experiment that can actually settle it.", + "Spend the first dollar on the most decisive, lowest-cost test of the leader.", + ]) + rec2 = r.choice([ + "If it clears, add the orthogonal runner-up to hedge mechanism risk.", + "If that holds up, bring in the most independent runner-up as a hedge.", + "Assuming a positive read, run the orthogonal runner-up next to cover the " + "mechanism risk.", + ]) + rec3 = r.choice([ + "Pre-register every falsification threshold before any hands-on work begins.", + "Fix and record each pass/fail threshold up front, before collecting data.", + "Lock in the falsification criteria in advance so a near-miss can't be " + "argued away.", + ]) + open_q = r.choice([ + "Where the evidence was thin, reviewer confidence is lower and a domain " + "expert is most likely to disagree — treat those proposals as exploratory. " + "The tournament optimizes for debate-survivability, not ground truth, so a " + "high Elo is a strong prior, not a proof.", + "The proposals resting on the least support are exactly where an expert " + "would push back hardest; hold them loosely. Remember the ranking rewards " + "ideas that survive argument, which is correlated with being right but is " + "not the same thing.", + "Confidence should track the underlying support, which is uneven — the " + "thinner cases are best read as leads rather than conclusions. A high Elo " + "says an idea withstood scrutiny, not that it is true.", + "Some of these stand on firmer ground than others, and the shakier ones " + "deserve a skeptic's eye before any commitment. The tournament measures how " + "well an idea defends itself, so treat rank as a prior to update, not a " + "verdict.", + ]) + return f"""# Research proposal **Research goal.** {goal} ## Problem framing and significance -The goal above defines a question where a testable, mechanism-anchored answer -would materially change what a lab does next. Across a multi-agent tournament, -the system generated candidate hypotheses, critiqued them, and ranked them -head-to-head so that only ideas surviving repeated scrutiny rose to the top. The -proposals below are the survivors, ordered by tournament Elo. +{framing} ## Executive summary -The tournament converged on {len(top)} strong candidate{'' if len(top) == 1 else 's'}, -led by **{lead_title}**. The leading ideas share a bias toward interventions that -are testable with existing models and, where possible, repurpose known agents to -shorten the path from hypothesis to evidence. +{exec_summary} ## The approach landscape -Independent generation strategies (literature-grounded, debate-driven, -combination, and out-of-box) were each given room to explore, then forced to -compete. Where several strategies nominated the same mechanism, that convergence -is treated as a robustness signal rather than redundancy.{donut} +{landscape}{donut} ## Ranked proposals @@ -553,23 +970,17 @@ def make_overview(goal: str, proposals: list[dict]) -> str: ## Comparative assessment -The top proposals are not interchangeable: some converge on a shared pathway -(mutually reinforcing evidence), while others are genuinely orthogonal bets worth -running in parallel to hedge mechanism risk. Prefer starting with the highest-Elo -idea that also has the cheapest decisive experiment.{lineage} +{comparative}{lineage} ## Recommended path and sequencing -1. Run the single cheapest decisive experiment for the top proposal first. -2. If it clears, add the orthogonal runner-up to hedge mechanism risk. -3. Pre-register every falsification threshold before wet-lab work begins. +1. {rec1} +2. {rec2} +3. {rec3} ## Open questions and limitations -Where the literature was thin, reviewer confidence is lower and a domain expert -is most likely to disagree — treat those proposals as exploratory. The tournament -optimizes for debate-survivability, not ground truth, so a high Elo is a strong -prior, not a proof. +{open_q} ## Analysis @@ -585,17 +996,18 @@ def make_overview(goal: str, proposals: list[dict]) -> str: def make_plan(goal: str) -> dict: r = _rng(goal) + dom = _infer_domain(_extract_keywords(goal)[0]) return { "objective": goal, "preferences": r.sample( - ["prioritize testable mechanisms", "favor drug repurposing", + ["prioritize testable mechanisms", "favor low-cost interventions", "emphasize novelty", "require quantitative readouts", - "avoid CBRN-adjacent directions"], 3), + "prefer reversible/ethical directions"], 3), "constraints": r.sample( - ["existing models only", "clinically approved agents preferred", - "budget-bounded wet-lab", "no human-subjects work"], 2), + ["use existing methods where possible", "bounded budget", + "clear falsification criteria required", "no high-risk directions"], 2), "idea_attributes": ["mechanistic", "testable", "novel", "feasible"], - "domain_hint": "biomedicine", + "domain_hint": dom["id"], "notes": "Auto-parsed research plan.", } @@ -863,40 +1275,72 @@ def make_self_critique(goal: str, round_no: int, top: list[dict]) -> str: opener = _CRITIQUE_OPENERS[(round_no - 1) % len(_CRITIQUE_OPENERS)] closer = _CRITIQUE_CLOSERS[(round_no - 1) % len(_CRITIQUE_CLOSERS)] + # Seeded stitching banks so the connective sentences vary by round + target, + # instead of being identical every session. The angle/opener/closer already + # rotate; this varies the prose that links them. + cr = _rng(f"{goal}|selfcritique|{round_no}|{title}") + low_str = f"{sc[low_dim]:.2f}" + e_txt = _elo_txt(target.get("elo")) if round_no > 1: prev = lst[(round_no - 2) % len(lst)] prev_angle = _CRITIQUE_ANGLES[(round_no - 2) % len(_CRITIQUE_ANGLES)] prev_title = (prev.get("title") or "an untitled idea").strip() - prior_ref = ( + prior_ref = cr.choice([ f"Round {round_no - 1} probed the {prev_angle['name']} in " f"**{prev_title}**; this round I turn to the {angle['name']} in " - f"**{title}**." - ) + f"**{title}**.", + f"Last round it was the {prev_angle['name']} in **{prev_title}**. Now I " + f"switch targets to **{title}** and press on its {angle['name']}.", + f"Having leaned on the {prev_angle['name']} of **{prev_title}** in round " + f"{round_no - 1}, I move to a different idea and a different axis: the " + f"{angle['name']} in **{title}**.", + ]) else: - prior_ref = ( - f"This is the first critique pass, so I start by attacking the " - f"current leader's {angle['name']}." - ) - + prior_ref = cr.choice([ + f"This is the first critique pass, so I start by attacking the current " + f"leader's {angle['name']}.", + f"First pass — I open on the leader and go straight at its " + f"{angle['name']}.", + f"Nothing to compare against yet, so I begin where the leader looks " + f"softest: its {angle['name']}.", + ]) + + reread = cr.choice([ + f"I re-read **{title}** ({e_txt}) — its last review landed at {score_line}, " + f"verdict *{rv['verdict']}*. The softest mark is **{low_dim}** ({low_str}), " + f"and that is exactly where a {angle['name']} problem would bite.", + f"Back to **{title}** ({e_txt}). The scorecard reads {score_line}, verdict " + f"*{rv['verdict']}*; **{low_dim}** ({low_str}) is the weakest line, and a " + f"{angle['name']} flaw would land right there.", + f"Looking again at **{title}** ({e_txt}): review scores {score_line}, " + f"verdict *{rv['verdict']}*. Its low mark is **{low_dim}** ({low_str}) — the " + f"same place a {angle['name']} problem would do the most damage.", + ]) probe_lines = "\n".join(f"{i + 1}. {p}" for i, p in enumerate(angle["probes"])) - thinking = ( - f"Round {round_no}. {prior_ref}\n\n" - f"I re-read **{title}** ({_elo_txt(target.get('elo'))}) — its last review " - f"landed at {score_line}, verdict *{rv['verdict']}*. The softest mark is " - f"**{low_dim}** ({sc[low_dim]:.2f}), and that is exactly where a " - f"{angle['name']} problem would bite.\n\n" - f"{probe_lines}" - ) - critique = ( - f"{opener} Looking hard at **{title}**, I am not convinced. The weak axis " - f"this round is **{angle['name']}**: {angle['body']}.\n\n" - f"Its {low_dim} score ({sc[low_dim]:.2f}) is the softest on its " - f"scorecard, so {angle['threat']}. If that holds, the verdict of " - f"*{rv['verdict']}* is generous and the {_elo_txt(target.get('elo'))} gap " - f"to the field is doing more work than the evidence supports.\n\n" - f"{closer}" - ) + thinking = f"Round {round_no}. {prior_ref}\n\n{reread}\n\n{probe_lines}" + + doubt = cr.choice([ + f"Looking hard at **{title}**, I am not convinced. The weak axis this round " + f"is **{angle['name']}**: {angle['body']}.", + f"I read **{title}** against the grain and it does not fully hold up. The " + f"exposed axis is **{angle['name']}** — {angle['body']}.", + f"Pressing on **{title}**, my doubt sharpens rather than fades. It turns on " + f"**{angle['name']}**: {angle['body']}.", + ]) + stakes = cr.choice([ + f"Its {low_dim} score ({low_str}) is the softest on its scorecard, so " + f"{angle['threat']}. If that holds, the verdict of *{rv['verdict']}* is " + f"generous and the {e_txt} gap to the field is doing more work than the " + f"evidence supports.", + f"With {low_dim} already the lowest mark ({low_str}), {angle['threat']}. " + f"Should that be right, *{rv['verdict']}* flatters it, and its {e_txt} lead " + f"is resting on argument more than proof.", + f"The {low_str} on {low_dim} is where it is thinnest, which means " + f"{angle['threat']}. If so, calling it *{rv['verdict']}* is charitable and " + f"the {e_txt} margin overstates the case.", + ]) + critique = f"{opener} {doubt}\n\n{stakes}\n\n{closer}" return f"## Thinking\n\n{thinking}\n\n## Self-critique\n\n{critique}" @@ -1019,24 +1463,64 @@ def make_stress_report(goal: str, hyp: dict, round_info: dict) -> str: } score_row = " · ".join(f"{d} {sc[d]:.2f} → {after[d]:.2f}" for d in _REVIEW_DIMS) + # Seeded prose banks (drawn AFTER the numeric picks so those stay stable). + # Only the connective sentences vary; the report's bold section frame is fixed. + cite_tail = r.choice([ + f"on re-reading, it backs a ~{haircut}% smaller effect than the summary " + f"implies once a stricter control is added", + f"read closely, it supports an effect about {haircut}% weaker than the " + f"claim, and only before the stricter control", + f"the actual result is ~{haircut}% below what the summary leans on it for " + f"once you tighten the control", + ]) cite_titles = list(dict.fromkeys( (c.get("title") or "untitled source").strip() for c in cites)) if cite_titles: citation_line = "\n".join( - f"- *{t}* — on re-reading, it backs a ~{haircut}% smaller effect than " - f"the summary implies once a stricter control is added." - for t in cite_titles[:2] + f"- *{t}* — {cite_tail}." for t in cite_titles[:2] ) else: citation_line = ( "- No sources were attached — flagging the citation gap as a finding: " "the claim currently rests on uncited reasoning." ) + break_line = r.choice([ + f"Stress round {round_no}/{of}. I am trying to *break* **{title}**, not " + f"defend it.", + f"Stress round {round_no}/{of}. My job here is to falsify **{title}**, not " + f"to make its case.", + f"Stress round {round_no}/{of}. I approach **{title}** as an adversary " + f"looking for the crack, not an advocate.", + ]) + claim_line = r.choice([ + f"Its core claim: “{gist}”. That lever is what I have to falsify.", + f"The claim under fire: “{gist}”. If it is wrong, that is where it breaks.", + f"What it asserts: “{gist}”. This is the load-bearing lever I need to knock " + f"over.", + ]) + attack_lead = r.choice([ + f"**What I attacked.** I targeted the idea's core claim — “{gist}” — and " + f"{probe['attack']}.", + f"**What I attacked.** Going straight at the central claim — “{gist}” — I " + f"{probe['attack']}.", + f"**What I attacked.** I took aim at the load-bearing claim — “{gist}” — and " + f"{probe['attack']}.", + ]) + feas_line = r.choice([ + f"**Feasibility numbers.** At a realistic exposure the predicted effect is " + f"~{effect}% of the outcome measure — above noise, but the margin is thin, " + f"so any pilot must be powered for it.", + f"**Feasibility numbers.** Under realistic conditions the effect works out " + f"to ~{effect}% of the outcome — it clears noise, but only just, so a pilot " + f"needs real statistical power.", + f"**Feasibility numbers.** The back-of-envelope effect is ~{effect}% of the " + f"measure at a plausible setting — detectable, yet close enough to noise " + f"that an underpowered pilot would miss it.", + ]) thinking = ( - f"Stress round {round_no}/{of}. I am trying to *break* **{title}**, not " - f"defend it.\n\n" - f"Its core claim: “{gist}”. That lever is what I have to falsify.\n\n" + f"{break_line}\n\n" + f"{claim_line}\n\n" f"1. Adversarial search: what published result, if it exists, would kill " f"this specific claim?\n" f"2. Citation audit: " @@ -1053,13 +1537,10 @@ def make_stress_report(goal: str, hyp: dict, round_info: dict) -> str: ) report = ( f"{token} — {driver}.\n\n" - f"**What I attacked.** I targeted the idea's core claim — “{gist}” — and " - f"{probe['attack']}.\n\n" + f"{attack_lead}\n\n" f"**Found evidence.**\n{citation_line}\n\n" f"**Scores before → after fix.** {score_row}.\n\n" - f"**Feasibility numbers.** At a realistic exposure the predicted effect is " - f"~{effect}% of the outcome measure — above noise, but the margin is thin, " - f"so any pilot must be powered for it.\n\n" + f"{feas_line}\n\n" f"**Prototype-scale pilot (run this BEFORE scaling).**\n" f"- *Model:* the smallest faithful test bed for “{title[:60]}”.\n" f"- *Intervention:* the hypothesis's own lever, a single dose/setting.\n" @@ -1080,6 +1561,24 @@ def make_stress_fix(hyp: dict) -> dict: title = (hyp.get("title") or "an untitled idea").strip() r = _rng(f"fix|{hyp.get('id')}") fix = r.choice(_STRESS_FIXES) + thinking = " ".join([ + r.choice([ + f"The stress test on “{title}” found a real but bounded weakness, so I " + f"kept the mechanism and redesigned around the failure mode.", + f"Rather than abandon “{title}”, I isolated the one place the stress " + f"test broke it and closed that gap specifically.", + f"“{title}” survived scrutiny except at a single seam; this revision " + f"targets exactly that seam and nothing else.", + ]), + r.choice([ + "The change is deliberately conservative — narrow the claim to what the " + "evidence defends and add the control the test showed was load-bearing.", + "I resisted broadening the idea; the fix only removes the failure the " + "test exposed, so the comparison to the parent stays clean.", + "Keeping the edit minimal means a re-rank measures the fix, not a " + "wholesale rewrite.", + ]), + ]) return { "title": f"{title} — hardened", "summary": ( @@ -1087,6 +1586,7 @@ def make_stress_fix(hyp: dict) -> dict: f"core mechanism, but the failure mode the stress test surfaced is now " f"designed out before scaling." ), + "thinking": thinking, } diff --git a/webapp/seed.py b/webapp/seed.py index 071666c..b818dd1 100644 --- a/webapp/seed.py +++ b/webapp/seed.py @@ -27,6 +27,13 @@ ); CREATE INDEX IF NOT EXISTS web_cit_hyp ON web_citations(hypothesis_id); +-- Varied synthetic reasoning per hypothesis (labelled SIMULATED in the UI). +-- A side table so the shared co_scientist hypotheses schema is untouched. +CREATE TABLE IF NOT EXISTS hyp_thinking ( + hypothesis_id TEXT PRIMARY KEY, + thinking TEXT +); + CREATE TABLE IF NOT EXISTS chat_messages ( id TEXT PRIMARY KEY, session_id TEXT NOT NULL, @@ -41,6 +48,15 @@ DEMO_TAG = "demo::" # session ids are prefixed so --reset only nukes demo data +# Varied match rationales so the demo tournament feed doesn't repeat one sentence. +_SEED_MATCH_RATIONALES = [ + "offered a sharper falsification criterion and stronger mechanistic grounding", + "made a more specific, testable claim with cleaner controls", + "held up better under cross-examination", + "proposed a more decisive, lower-cost experiment", + "rested on stronger, more direct evidence", +] + def _ts(dt: datetime) -> str: return dt.isoformat() @@ -132,7 +148,8 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, "id": hid, "created_by": created_by, "strategy": strat, "parent_ids": list({p for p in parent_ids}), "title": c["title"], "summary": c["summary"], "full_text": c["full_text"], - "citations": c["citations"], "cluster": f"clu_{i % n_clusters}", + "citations": c["citations"], "thinking": c["thinking"], + "cluster": f"clu_{i % n_clusters}", "elo": seed_elo, "elo0": seed_elo, # elo0 = fixed quality anchor "matches": 0, "created_at": start + timedelta(minutes=2 + i * 3), }) @@ -161,8 +178,8 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, "winner": winner, "ea": a["elo"], "eb": b["elo"], "ea2": ra, "eb2": rb, "created_at": match_t, "similarity": round(r.uniform(0.05, 0.4), 2), - "rationale": f"Under {mode}, idea {winner.upper()} offered a sharper " - "falsification criterion and stronger mechanistic grounding.", + "rationale": f"Under {mode}, idea {winner.upper()} " + f"{r.choice(_SEED_MATCH_RATIONALES)}.", }) a["elo"], b["elo"] = ra, rb a["matches"] += 1 @@ -200,6 +217,10 @@ def build_session(conn: sqlite3.Connection, *, goal: str, status: str, VALUES (?,?,?,?,?,?)""", (h["id"], cit["title"], cit["url"], cit["excerpt"], cit["doi"], cit["year"]), ) + conn.execute( + "INSERT OR REPLACE INTO hyp_thinking (hypothesis_id, thinking) VALUES (?,?)", + (h["id"], h.get("thinking", "")), + ) if h["state"] in ("draft",): continue for kind in (["full", "verification"] if h["state"] != "reviewed" else ["full"]): @@ -338,6 +359,7 @@ def seed(db=DEFAULT_DB, reset: bool = False) -> list[str]: conn.execute("DELETE FROM elo_journal WHERE match_id=?", (mid,)) for hid in hids: conn.execute("DELETE FROM web_citations WHERE hypothesis_id=?", (hid,)) + conn.execute("DELETE FROM hyp_thinking WHERE hypothesis_id=?", (hid,)) conn.execute("DELETE FROM events WHERE session_id=?", (sid,)) # FK ON DELETE CASCADE clears hypotheses/reviews/matches/transcripts/ # system_feedback/tasks when the parent session is removed. diff --git a/webapp/simulator.py b/webapp/simulator.py index 71f390e..fb62d56 100644 --- a/webapp/simulator.py +++ b/webapp/simulator.py @@ -43,6 +43,16 @@ ELO_SEED_BASE = 1000.0 ELO_SEED_SPAN = 800.0 +# Varied match rationales so the tournament feed doesn't repeat one sentence. +_MATCH_RATIONALES = [ + "gave a sharper falsification criterion", + "offered a cleaner causal mechanism", + "proposed a more decisive experiment", + "held up better under cross-examination", + "rested on stronger, more direct evidence", + "made a more specific, testable claim", +] + def _now() -> datetime: return datetime.now(UTC) @@ -172,6 +182,9 @@ def _add_hyp(self, conn, i, strat, created_by, parents): "INSERT INTO web_citations (hypothesis_id, title, url, excerpt, doi, year)" " VALUES (?,?,?,?,?,?)", (hid, cit["title"], cit["url"], cit["excerpt"], cit["doi"], cit["year"])) + conn.execute( + "INSERT OR REPLACE INTO hyp_thinking (hypothesis_id, thinking) VALUES (?,?)", + (hid, c.get("thinking", ""))) cost = round(self.r.uniform(0.04, 0.2), 4) _transcript(conn, self.sid, created_by, f"{created_by}.{strat}", content.MODELS[created_by], now, cost) @@ -228,6 +241,9 @@ def _apply_match(self, conn, a, b, mode: str, winner: str, k: int = 48): mid = "mat_" + hashlib.sha256( f"{self.sid}{a['id']}{b['id']}{time.time()}".encode()).hexdigest()[:16] now = _now() + rationale = ( + f"Idea {winner.upper()} {self.r.choice(_MATCH_RATIONALES)}." + ) conn.execute( """INSERT INTO tournament_matches (id, session_id, created_at, hyp_a, hyp_b, mode, winner, @@ -235,8 +251,7 @@ def _apply_match(self, conn, a, b, mode: str, winner: str, k: int = 48): rationale, transcript_id, similarity) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)""", (mid, self.sid, _ts(now), a["id"], b["id"], mode, winner, - a["elo"], b["elo"], ra, rb, - f"Idea {winner.upper()} gave a sharper falsification criterion.", + a["elo"], b["elo"], ra, rb, rationale, None, round(self.r.uniform(0.05, 0.4), 2))) conn.execute( """INSERT OR IGNORE INTO elo_journal @@ -375,6 +390,9 @@ def _add_fix_child(self, conn, i, parent, fix): "INSERT INTO web_citations (hypothesis_id, title, url, excerpt, doi, year)" " VALUES (?,?,?,?,?,?)", (hid, cit["title"], cit["url"], cit["excerpt"], cit["doi"], cit["year"])) + conn.execute( + "INSERT OR REPLACE INTO hyp_thinking (hypothesis_id, thinking) VALUES (?,?)", + (hid, fix.get("thinking", ""))) cost = round(self.r.uniform(0.04, 0.2), 4) _transcript(conn, self.sid, "evolution", "evolution.feedback_driven", content.MODELS["evolution"], now, cost) diff --git a/webapp/store.py b/webapp/store.py index 4b8c7ad..d94a58d 100644 --- a/webapp/store.py +++ b/webapp/store.py @@ -154,9 +154,21 @@ def get_hypothesis(conn: sqlite3.Connection, hid: str) -> dict | None: h["reviews"] = list_reviews(conn, hid) h["scores"] = _avg_scores(conn, hid) h["elo_history"] = elo_history_for(conn, hid) + h["thinking"] = _hypothesis_thinking(conn, hid) return h +def _hypothesis_thinking(conn: sqlite3.Connection, hid: str) -> str | None: + """Varied synthetic reasoning stashed by the demo seeder / live simulator in a + side table (see webapp/seed.py EXTRA_TABLES). None for real-engine DBs that + have no such table.""" + try: + row = _row(conn, "SELECT thinking FROM hyp_thinking WHERE hypothesis_id=?", (hid,)) + return row["thinking"] if row else None + except sqlite3.OperationalError: + return None + + def _hypothesis_citations(conn: sqlite3.Connection, hid: str) -> list[dict]: # Citations live in the on-disk artifact JSON in the real engine; the demo # seeder stashes them in a side table if present, else returns []. From 71c5cdba4af1158ac20d987c78eedea6672bf167 Mon Sep 17 00:00:00 2001 From: Duck Quang Date: Thu, 16 Jul 2026 05:44:52 +0700 Subject: [PATCH 2/2] Surface thinking from the hypotheses column too, not only the sim side table When this lands alongside the engine-thinking-capture work (which stores thinking as a column on the hypotheses table), get_hypothesis must prefer that column and fall back to the sim's hyp_thinking side table, so both real-engine and simulated reasoning reach the drawer. Co-Authored-By: Claude Fable 5 --- webapp/store.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/webapp/store.py b/webapp/store.py index d94a58d..43f5059 100644 --- a/webapp/store.py +++ b/webapp/store.py @@ -154,7 +154,10 @@ def get_hypothesis(conn: sqlite3.Connection, hid: str) -> dict | None: h["reviews"] = list_reviews(conn, hid) h["scores"] = _avg_scores(conn, hid) h["elo_history"] = elo_history_for(conn, hid) - h["thinking"] = _hypothesis_thinking(conn, hid) + # Real-engine DBs store thinking as a column on `hypotheses` (loaded by + # SELECT *); the demo/sim stashes it in the hyp_thinking side table. Prefer + # the column, fall back to the side table. + h["thinking"] = h.get("thinking") or _hypothesis_thinking(conn, hid) return h