/** * Demo conversation engine with Plan Mode and Action Mode. * * Plan Mode: Deep structured interview for drug hunters — asks the critical * questions that determine whether a molecule will succeed in the clinic. * * Action Mode: AI takes the lead — minimal questions, proactive exploration, * autonomous pipeline decisions. */ import { BENCHMARKS, MODELS, TARGETS } from "./data"; import type { StreamEvent, PlanProposal, TraceEntry, Artifact, Citation } from "./types"; import { createPhyloCampaign, submitPhyloJob, getPhyloJobStatus, estimateToolCost, } from "./api"; import { resolveToolEndpoint } from "./tool-mapping"; export type AgentMode = "plan" | "action"; type PlanState = | "greeting" | "ask_target" | "ask_indication" | "ask_mechanism" | "ask_format" | "ask_epitope" | "ask_affinity" | "ask_species" | "ask_developability" | "ask_route" | "ask_competition" | "ask_seed" | "ask_scale" | "show_plan" | "confirmed" | "free_chat"; type ActionState = "greeting" | "ask_target" | "running" | "free_chat"; interface DemoConversation { id: string; mode: AgentMode; planState: PlanState; actionState: ActionState; target: string; indication: string; mechanism: string; format: string; epitope: string; affinity: string; species: string; developability: string; route: string; competition: string; seed: string; scale: string; messageCount: number; } let _conv: DemoConversation | null = null; /** Optional scientist-provided input file (PDB/FASTA). Set from the UI. */ let _inputFile: File | null = null; /** Set the scientist-provided input file for use in executePlan. */ export function demoSetInputFile(file: File | null): void { _inputFile = file; } export function demoCreateConversation(): string { const id = "demo-" + Math.random().toString(36).slice(2, 10); _conv = { id, mode: "plan", planState: "greeting", actionState: "greeting", target: "", indication: "", mechanism: "", format: "", epitope: "", affinity: "", species: "", developability: "", route: "", competition: "", seed: "", scale: "", messageCount: 0, }; return id; } export function demoSetMode(mode: AgentMode): void { if (_conv) _conv.mode = mode; } export function demoGetMode(): AgentMode { return _conv?.mode ?? "plan"; } export function demoGetDesignBrief(): Record { if (!_conv) return {}; const b: Record = {}; if (_conv.target) b["Target"] = _conv.target; if (_conv.indication) b["Indication"] = _conv.indication; if (_conv.mechanism) b["Mechanism"] = _conv.mechanism; if (_conv.format) b["Format"] = _conv.format; if (_conv.epitope) b["Epitope"] = _conv.epitope; if (_conv.affinity) b["Affinity"] = _conv.affinity; if (_conv.species) b["Species"] = _conv.species; if (_conv.developability) b["Developability"] = _conv.developability; if (_conv.route) b["Route"] = _conv.route; if (_conv.competition) b["Competition"] = _conv.competition; if (_conv.seed) b["Starting point"] = _conv.seed; if (_conv.scale) b["Designs"] = _conv.scale; return b; } export async function demoSendMessage( message: string, onEvent: (ev: StreamEvent) => void ): Promise { if (!_conv) return; _conv.messageCount++; const msg = message.toLowerCase().trim(); // Explore queries work in any mode/state if (isExploreQuery(msg)) { await handleExploreQuery(msg, onEvent); return; } if (_conv.mode === "plan") { await handlePlanMode(msg, onEvent); } else { await handleActionMode(msg, onEvent); } } // ============================================================ // PLAN MODE — Deep drug design interview // ============================================================ async function handlePlanMode(msg: string, onEvent: (ev: StreamEvent) => void) { switch (_conv!.planState) { case "greeting": { const target = detectTarget(msg); if (target) { _conv!.target = target; await emitToolCall("lookup_target", { target_name: target }, getTargetData(target), onEvent); await streamText( `**${target}** — ${getTargetNotes(target)}.\n\nWhat therapeutic indication are you pursuing?\n\n` + `- **Oncology** (e.g., solid tumors, hematologic malignancies)\n` + `- **Autoimmune / Inflammation**\n` + `- **Infectious disease**\n` + `- **Other**`, onEvent ); _conv!.planState = "ask_indication"; } else { await streamText( `Welcome to **Plan Mode**. I will walk you through a structured design brief to make sure we capture everything that matters for your molecule.\n\n` + `What is your **target protein**?`, onEvent ); _conv!.planState = "ask_target"; } break; } case "ask_target": { const target = detectTarget(msg) || msg.replace(/[^a-zA-Z0-9\-\s]/g, "").trim().toUpperCase(); _conv!.target = target; const known = TARGETS.find((t) => t.name.toLowerCase() === target.toLowerCase()); if (known) { await emitToolCall("lookup_target", { target_name: target }, getTargetData(target), onEvent); await streamText( `**${target}** — ${known.notes}\n\nWhat therapeutic indication?\n\n- **Oncology**\n- **Autoimmune / Inflammation**\n- **Infectious disease**\n- **Other**`, onEvent ); } else { await streamText( `Noted: **${target}**. No benchmark data yet, but we can design against it.\n\nWhat therapeutic indication?\n\n- **Oncology**\n- **Autoimmune / Inflammation**\n- **Infectious disease**\n- **Other**`, onEvent ); } _conv!.planState = "ask_indication"; break; } case "ask_indication": { _conv!.indication = extractChoice(msg, ["oncology", "autoimmune", "inflammation", "infectious", "other"]) || msg.slice(0, 60); await streamText( `What is the desired **mechanism of action**?\n\n` + `- **Blocking / Neutralization** — prevent ligand-receptor interaction\n` + `- **Receptor agonism** — activate signaling\n` + `- **Internalization / ADC** — deliver payload into cell\n` + `- **Cell depletion** — ADCC/CDC to kill target-expressing cells\n` + `- **Not sure** — I can advise based on target biology`, onEvent ); _conv!.planState = "ask_mechanism"; break; } case "ask_mechanism": { _conv!.mechanism = extractChoice(msg, ["blocking", "neutralization", "agonism", "internalization", "adc", "depletion", "adcc", "cdc"]) || msg.slice(0, 60); const needsFc = ["depletion", "adcc", "cdc", "internalization", "adc"].some((k) => _conv!.mechanism.includes(k)); await streamText( `What **antibody format**?${needsFc ? " (Your mechanism likely needs Fc effector functions.)" : ""}\n\n` + `- **VHH** (nanobody) — 15 kDa, great tissue penetration, no Fc\n` + `- **scFv** — 27 kDa, no Fc\n` + `- **mAb (IgG1)** — full Fc, enables ADCC/CDC\n` + `- **mAb (IgG4)** — Fc-silent, blocking only\n` + `- **Bispecific** — dual-target engagement`, onEvent ); _conv!.planState = "ask_format"; break; } case "ask_format": { if (msg.includes("vhh") || msg.includes("nanobody")) _conv!.format = "VHH"; else if (msg.includes("scfv")) _conv!.format = "scFv"; else if (msg.includes("igg1") || msg.includes("mab")) _conv!.format = "mAb (IgG1)"; else if (msg.includes("igg4")) _conv!.format = "mAb (IgG4)"; else if (msg.includes("bispecific")) _conv!.format = "Bispecific"; else _conv!.format = msg.slice(0, 20); await streamText( `Do you have a **preferred epitope** or binding site on ${_conv!.target}?\n\n` + `- **Orthosteric** — ligand binding site\n` + `- **Allosteric** — away from active site\n` + `- **Specific residues** — describe them\n` + `- **No preference** — let the model explore`, onEvent ); _conv!.planState = "ask_epitope"; break; } case "ask_epitope": { _conv!.epitope = msg.includes("no preference") || msg.includes("no pref") || msg.includes("explore") ? "no preference" : msg.slice(0, 80); await streamText( `What **affinity** are you targeting?\n\n` + `- **< 1 nM** — ultra-high (typical for therapeutic mAbs)\n` + `- **1-10 nM** — high (standard for most programs)\n` + `- **10-100 nM** — moderate (acceptable for some mechanisms)\n` + `- **Any binder first** — optimize affinity later`, onEvent ); _conv!.planState = "ask_affinity"; break; } case "ask_affinity": { _conv!.affinity = extractChoice(msg, ["< 1", "1-10", "10-100", "any binder"]) || msg.slice(0, 30); await streamText( `What **species cross-reactivity** do you need?\n\n` + `- **Human only** — sufficient for early discovery\n` + `- **Human + cynomolgus** — needed for tox studies\n` + `- **Human + cyno + mouse** — enables mouse efficacy models\n` + `- **Not sure**`, onEvent ); _conv!.planState = "ask_species"; break; } case "ask_species": { _conv!.species = extractChoice(msg, ["human only", "cyno", "mouse", "not sure"]) || msg.slice(0, 40); await streamText( `**Developability** priorities? Select the most critical:\n\n` + `- **Low aggregation** — Agmata score < 0.3\n` + `- **High expression** — > 1 g/L in CHO\n` + `- **Thermostability** — Tm > 70C\n` + `- **Low immunogenicity** — high humanness score\n` + `- **Balanced** — optimize all equally`, onEvent ); _conv!.planState = "ask_developability"; break; } case "ask_developability": { _conv!.developability = extractChoice(msg, ["aggregation", "expression", "thermostab", "immunogenicity", "balanced"]) || msg.slice(0, 40); await streamText( `Intended **route of administration**?\n\n` + `- **IV** (intravenous) — standard for oncology\n` + `- **SC** (subcutaneous) — preferred for chronic dosing\n` + `- **Intrathecal** — CNS delivery\n` + `- **Inhaled** — respiratory targets\n` + `- **Not decided**`, onEvent ); _conv!.planState = "ask_route"; break; } case "ask_route": { _conv!.route = extractChoice(msg, ["iv", "sc", "intrathecal", "inhaled", "not decided"]) || msg.slice(0, 30); await streamText( `Are there **existing antibodies** against ${_conv!.target} you want to benchmark against or differentiate from?\n\n` + `- Name specific antibodies (e.g., trastuzumab, pembrolizumab)\n` + `- **None known**\n` + `- **Yes, but undisclosed**`, onEvent ); _conv!.planState = "ask_competition"; break; } case "ask_competition": { _conv!.competition = msg.includes("none") ? "none known" : msg.slice(0, 80); await streamText( `Starting point?\n\n` + `- **De novo** — design from scratch against the target structure\n` + `- **Seed sequence** — optimize an existing antibody\n` + `- **Humanize** — humanize a non-human lead`, onEvent ); _conv!.planState = "ask_seed"; break; } case "ask_seed": { _conv!.seed = msg.includes("novo") || msg.includes("scratch") ? "de novo" : msg.includes("humanize") ? "humanization" : "seed optimization"; await streamText( `How many designs to generate?\n\n- **10** — quick pilot\n- **50** — standard campaign\n- **100+** — comprehensive`, onEvent ); _conv!.planState = "ask_scale"; break; } case "ask_scale": { const match = msg.match(/(\d+)/); _conv!.scale = match ? match[1] : "50"; await showDesignBrief(onEvent); _conv!.planState = "show_plan"; break; } case "show_plan": { if (msg.includes("run") || msg.includes("go") || msg.includes("yes") || msg.includes("ready") || msg.includes("confirm") || msg.includes("start")) { await executePlan(onEvent); _conv!.planState = "free_chat"; } else { await streamText("What would you like to adjust in the design brief?", onEvent); } break; } case "confirmed": case "free_chat": { await handleFreeChat(msg, onEvent); break; } } onEvent({ type: "done" }); } async function showDesignBrief(onEvent: (ev: StreamEvent) => void) { // KB lookups — simulate best-practice retrieval before planning await emitKBLookup("antibody design best practices", onEvent); await emitKBLookup("benchmark catalog", onEvent); await emitKBLookup("target lookup", onEvent); await emitTrace({ kind: "thought", label: "Synthesizing design brief from interview answers" }, onEvent); await emitToolCall("browse_benchmarks", {}, { count: BENCHMARKS.length }, onEvent); await emitToolCall("browse_models", { capability: "full_design" }, { count: MODELS.filter((m) => m.capabilities.includes("full_design")).length }, onEvent); const pipeline = _conv!.seed === "de novo" ? "RFdiffusion → ProteinMPNN → RF2 (filter) → Chai-1 (score)" : "ProteinMPNN (CDR redesign) → RF2 → Chai-1"; await streamText( `Here is your **Design Brief**:\n\n` + `| Parameter | Value |\n` + `|-----------|-------|\n` + `| Target | **${_conv!.target}** |\n` + `| Indication | ${_conv!.indication} |\n` + `| Mechanism | ${_conv!.mechanism} |\n` + `| Format | **${_conv!.format}** |\n` + `| Epitope | ${_conv!.epitope} |\n` + `| Affinity goal | ${_conv!.affinity} |\n` + `| Species cross-rx | ${_conv!.species} |\n` + `| Developability | ${_conv!.developability} |\n` + `| Route | ${_conv!.route} |\n` + `| Competition | ${_conv!.competition} |\n` + `| Starting point | ${_conv!.seed} |\n` + `| Designs | **${_conv!.scale}** |\n` + `| Pipeline | ${pipeline} |\n` + `| Scoring | 3-model consensus (Chai-1 + AF2 + Protenix) |\n\n` + `Ready to launch this campaign, or want to adjust anything?`, onEvent ); // Emit structured plan proposal right before the execute/chat fork onEvent({ type: "plan", plan: buildPlanProposal() }); } async function executePlan(onEvent: (ev: StreamEvent) => void) { await emitTrace({ kind: "thought", label: `Launching campaign for ${_conv!.target}` }, onEvent); // ---- Determine pipeline tools from the plan ---- const plan = buildPlanProposal(); const pipelineTools = plan.steps.flatMap((s) => s.tools); try { // ---- 1. Create real campaign via Phylo backend ---- const campaignParams = { name: `${_conv!.target}-${_conv!.format}-${Date.now()}`, target: _conv!.target, modality: _conv!.format || "VHH", goal: _conv!.mechanism || "blocking", constraints: { epitope: _conv!.epitope, affinity: _conv!.affinity, species: _conv!.species, developability: _conv!.developability, route: _conv!.route, scale: _conv!.scale, seed: _conv!.seed, }, }; const campaign = await createPhyloCampaign(campaignParams); const campaignId = campaign.id; await emitToolCall( "create_campaign", campaignParams, { campaign_id: campaignId, status: "created" }, onEvent, ); await emitTrace( { kind: "tool_result", label: `campaign_id=${campaignId}`, detail: truncate(JSON.stringify(campaign)) }, onEvent, ); await streamText(`Campaign **${campaignId}** created on the Innovation Hub.\n\n`, onEvent); // ---- 2. Estimate costs for each pipeline tool ---- for (const tool of pipelineTools) { try { const endpoint = resolveToolEndpoint(tool); const estimate = await estimateToolCost(endpoint); await emitToolCall( "estimate_cost", { endpoint_name: endpoint, gpu_type: "A100" }, estimate, onEvent, ); const costLine = `**${tool}**: ~$${estimate.estimated_usd.toFixed(2)} (${estimate.minutes_used} min on ${estimate.gpu_type})`; const alertLine = estimate.cost_alert ? ` -- ${estimate.cost_alert}` : ""; await streamText(costLine + alertLine + "\n", onEvent); } catch { // Cost estimation is non-critical — skip silently await streamText(`**${tool}**: cost estimate unavailable\n`, onEvent); } } await streamText("\nSubmitting jobs to the GPU cluster...\n\n", onEvent); // ---- 3. Submit jobs sequentially ---- const completedJobs: { tool: string; jobId: string; output: unknown }[] = []; for (const tool of pipelineTools) { const jobEndpoint = resolveToolEndpoint(tool); await emitTrace({ kind: "tool_call", label: tool, detail: `Submitting ${tool} job (${jobEndpoint})` }, onEvent); // Use scientist-provided file if available, otherwise build placeholder FASTA let inputBlob: Blob; let inputFilename: string; if (_inputFile) { inputBlob = _inputFile; inputFilename = _inputFile.name; } else { const fastaContent = buildMinimalFasta(_conv!.target, _conv!.seed); inputBlob = new Blob([fastaContent], { type: "text/plain" }); inputFilename = "input.fasta"; } const formData = new FormData(); formData.append("endpoint_name", jobEndpoint); formData.append("provider", "runpod"); formData.append("call_params", JSON.stringify({ target: _conv!.target, format: _conv!.format, num_designs: parseInt(_conv!.scale || "50", 10), })); formData.append("role", "design"); formData.append("input_file", inputBlob, inputFilename); let jobResult; try { jobResult = await submitPhyloJob(campaignId, formData); } catch (err: unknown) { const errMsg = err instanceof Error ? err.message : String(err); await emitTrace({ kind: "tool_result", label: `${tool} submission failed`, detail: errMsg }, onEvent); await streamText(`**${tool}** -- job submission failed: ${errMsg}\n`, onEvent); continue; } const jobId = jobResult.campaign_job_id; await emitToolCall( "submit_job", { campaign_id: campaignId, endpoint_name: jobEndpoint }, { job_id: jobId, provider: jobResult.provider }, onEvent, ); await streamText(`**${tool}** job **${jobId}** submitted. Polling for completion...\n`, onEvent); // ---- 4. Poll until complete or timeout (10 min) ---- const POLL_INTERVAL_MS = 5_000; const MAX_POLL_MS = 10 * 60 * 1_000; const pollStart = Date.now(); let finalStatus: Awaited> | null = null; while (Date.now() - pollStart < MAX_POLL_MS) { await sleep(POLL_INTERVAL_MS); try { const status = await getPhyloJobStatus(jobId); const hubStatus = status.hub?.status?.toLowerCase() ?? ""; if (hubStatus === "completed" || hubStatus === "success" || hubStatus === "done") { finalStatus = status; break; } else if (hubStatus === "failed" || hubStatus === "error") { const detail = typeof status.hub?.output_data === "string" ? status.hub.output_data : JSON.stringify(status.hub?.output_data ?? "unknown error"); await emitTrace({ kind: "tool_result", label: `${tool} failed`, detail }, onEvent); await streamText(`**${tool}** failed: ${detail}\n`, onEvent); break; } // Still running — continue polling } catch { // Transient poll failure — keep trying } } if (finalStatus) { await emitTrace( { kind: "tool_result", label: `${tool} completed`, detail: truncate(JSON.stringify(finalStatus.hub)) }, onEvent, ); await streamText(`**${tool}** completed.\n`, onEvent); completedJobs.push({ tool, jobId, output: finalStatus.hub?.output_data }); // ---- 5. Emit real artifacts ---- if (finalStatus.artifacts && finalStatus.artifacts.length > 0) { for (const art of finalStatus.artifacts) { const artifact: Artifact = { id: `art-${jobId}-${art.name}`, name: art.name, bytes: art.bytes ?? 0, mime: art.mime ?? "application/octet-stream", url: art.url, createdAt: Date.now(), }; onEvent({ type: "artifact", artifact }); } } } else if (!finalStatus) { await streamText(`**${tool}** timed out after 10 minutes.\n`, onEvent); } } // ---- Summary ---- const successCount = completedJobs.length; const totalTools = pipelineTools.length; await streamText( `\n---\n**Campaign ${campaignId}**: ${successCount}/${totalTools} pipeline steps completed.\n` + `Track full progress in the **Campaigns** tab.`, onEvent, ); // Emit design brief artifact (always useful even with real backend) const designBriefMd = buildDesignBriefMarkdown(); await emitArtifact("design_brief.md", designBriefMd, "text/markdown", onEvent); } catch (err: unknown) { // ---- Backend unreachable — fall back to demo mode ---- const errMsg = err instanceof Error ? err.message : String(err); await streamText( `> **Backend unreachable** -- showing demo results. (${errMsg})\n\n`, onEvent, ); await executePlanDemo(onEvent); } } /** Original demo-only plan execution — used as fallback when Phylo backend is down. */ async function executePlanDemo(onEvent: (ev: StreamEvent) => void) { const campaignId = "camp-" + Math.random().toString(36).slice(2, 8); await emitToolCall( "plan_campaign", { target: _conv!.target, modality: _conv!.format, goal: _conv!.mechanism }, { campaign_id: campaignId, status: "planned" }, onEvent, ); await streamText( `Campaign **${campaignId}** created (demo mode).\n\n` + `Pipeline steps:\n` + `1. Generate ${_conv!.scale} backbone structures (RFdiffusion)\n` + `2. Design sequences (ProteinMPNN, 2x per backbone)\n` + `3. Filter by RF2 iPAE < 10\n` + `4. Score with Chai-1 AntiConf > 0.50\n` + `5. Rank by 3-model consensus\n\n` + `Track progress in the **Campaigns** tab.`, onEvent, ); const designBriefMd = buildDesignBriefMarkdown(); const topDesignsCsv = buildTopDesignsCsv(); const executionTraceJson = buildExecutionTraceJson(); await emitArtifact("design_brief.md", designBriefMd, "text/markdown", onEvent); await emitArtifact("top_designs.csv", topDesignsCsv, "text/csv", onEvent); await emitArtifact("execution_trace.json", executionTraceJson, "application/json", onEvent); } /** Build a minimal FASTA string from interview fields for job submission. */ function buildMinimalFasta(target: string, seed: string): string { // If the user provided a seed sequence, use it; otherwise use a placeholder const placeholder = "MEVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAR"; const header = `>${target}_design seed=${seed || "de_novo"}`; return `${header}\n${placeholder}\n`; } // ============================================================ // ACTION MODE — AI takes the lead // ============================================================ async function handleActionMode(msg: string, onEvent: (ev: StreamEvent) => void) { switch (_conv!.actionState) { case "greeting": { const target = detectTarget(msg); if (target) { _conv!.target = target; await runActionAutonomous(onEvent); _conv!.actionState = "free_chat"; } else { await streamText( `**Action Mode** — I will handle everything. Just tell me: what is your **target protein**?`, onEvent ); _conv!.actionState = "ask_target"; } break; } case "ask_target": { const target = detectTarget(msg) || msg.replace(/[^a-zA-Z0-9\-\s]/g, "").trim().toUpperCase(); _conv!.target = target; await runActionAutonomous(onEvent); _conv!.actionState = "free_chat"; break; } case "running": case "free_chat": { await handleFreeChat(msg, onEvent); break; } } onEvent({ type: "done" }); } async function runActionAutonomous(onEvent: (ev: StreamEvent) => void) { const target = _conv!.target; // KB lookups before planning await emitKBLookup("antibody design best practices", onEvent); await emitKBLookup("target lookup", onEvent); // Step 1: Look up target await emitTrace({ kind: "thought", label: `Assessing target ${target}` }, onEvent); await emitToolCall("lookup_target", { target_name: target }, getTargetData(target), onEvent); const known = TARGETS.find((t) => t.name.toLowerCase() === target.toLowerCase()); const tClass = known?.target_class || "unknown"; await streamText(`Analyzing **${target}**...`, onEvent); // Step 2: Check benchmarks await emitToolCall("browse_benchmarks", {}, { count: BENCHMARKS.length }, onEvent); // Step 3: Auto-select format const format = tClass === "gpcr" ? "VHH" : "VHH"; _conv!.format = format; // Step 4: Check models await emitToolCall("browse_models", { capability: "full_design" }, { count: 1 }, onEvent); // Step 5: Auto-build plan _conv!.seed = "de novo"; _conv!.scale = "50"; _conv!.mechanism = tClass === "gpcr" ? "blocking" : "blocking / neutralization"; _conv!.indication = "auto-detected"; const benchmarkData = getTargetBindingData(target); let benchmarkNote = ""; if (benchmarkData.length > 0) { const best = benchmarkData.sort((a, b) => (b.hit_rate ?? 0) - (a.hit_rate ?? 0))[0]; benchmarkNote = `\n\nBest published result: **${best.benchmark}** achieved ${fmtPct(best.hit_rate)} hit rate on ${target}.`; } await streamText( `Here is my recommended plan for **${target}**:\n\n` + `| Parameter | Value |\n` + `|-----------|-------|\n` + `| Target | **${target}** (${tClass}) |\n` + `| Format | **${format}** — best hit rates in benchmarks |\n` + `| Approach | **De novo** — RFdiffusion → ProteinMPNN → RF2 → Chai-1 |\n` + `| Designs | **50** |\n` + `| Scoring | 3-model consensus, AntiConf > 0.50 |\n` + benchmarkNote + `\n\nI selected VHH because it consistently achieves the highest hit rates across published benchmarks (JAM-2: 39%). ` + `The de novo pipeline is the most validated approach for new targets.\n\n` + `**Ready to launch?** Or switch to Plan Mode for a deeper interview.`, onEvent ); onEvent({ type: "citation", citation: { label: "JAM-2 benchmark paper", url: "https://doi.org/10.1101/2024.jam2" } }); onEvent({ type: "plan", plan: buildPlanProposal() }); } // ============================================================ // EXPLORE QUERIES (shared) // ============================================================ function isExploreQuery(msg: string): boolean { const kw = ["benchmark", "benchmarks", "compare", "models", "model", "what models", "what benchmarks", "show me", "list", "tell me about", "look up"]; return kw.some((k) => msg.includes(k)); } async function handleExploreQuery(msg: string, onEvent: (ev: StreamEvent) => void) { // KB lookups for benchmark/model/target questions if (msg.includes("benchmark") || msg.includes("model") || msg.includes("target") || msg.includes("compare")) { await emitKBLookup("benchmark catalog", onEvent); await emitKBLookup("model registry", onEvent); } if (msg.includes("compare")) { const names = extractBenchmarkNames(msg); if (names.length >= 2) { const rows = names.map((n) => { const bm = BENCHMARKS.find((b) => b.source.toLowerCase() === n.toLowerCase()); return bm ? { source: bm.source, vhh: bm.avg_hit_rate_vhh, mab: bm.avg_hit_rate_mab, designs: bm.total_designs_tested } : { source: n, error: "not found" }; }); await emitToolCall("compare_benchmarks", { benchmark_names: names }, { comparison: rows }, onEvent); let table = "| Benchmark | VHH Hit Rate | mAb Hit Rate | Designs | Open Source |\n|-----------|:---:|:---:|:---:|:---:|\n"; for (const bm of BENCHMARKS.filter((b) => names.some((n) => n.toLowerCase() === b.source.toLowerCase()))) { table += `| ${bm.source} | ${fmtPct(bm.avg_hit_rate_vhh)} | ${fmtPct(bm.avg_hit_rate_mab)} | ${bm.total_designs_tested} | ${bm.code_available ? "Yes" : "No"} |\n`; } await streamText(table + `\n**JAM-2** leads on hit rates. **Chai-2** has the best developability. **RFantibody** is validated by cryo-EM.`, onEvent); onEvent({ type: "citation", citation: { label: "JAM-2 paper", url: "https://doi.org/10.1101/2024.jam2" } }); onEvent({ type: "citation", citation: { label: "RFantibody", url: "https://github.com/RosettaCommons/RFantibody" } }); } else { await emitToolCall("browse_benchmarks", {}, { count: BENCHMARKS.length }, onEvent); await streamText(`Which benchmarks to compare? Available: ${BENCHMARKS.map((b) => `**${b.source}**`).join(", ")}`, onEvent); } } else if (msg.includes("benchmark")) { await emitToolCall("browse_benchmarks", {}, { count: BENCHMARKS.length }, onEvent); let table = "| Benchmark | Date | VHH Hit Rate | Best KD | Designs | Open |\n|-----------|------|:---:|:---:|:---:|:---:|\n"; for (const bm of BENCHMARKS) { table += `| ${bm.source} | ${bm.paper_date} | ${fmtPct(bm.avg_hit_rate_vhh)} | ${bm.best_affinity_pM ? bm.best_affinity_pM + " pM" : "-"} | ${bm.total_designs_tested} | ${bm.code_available ? "Yes" : "No"} |\n`; } await streamText(`Published antibody design benchmarks:\n\n${table}\n**JAM-2** is current SOTA at 39% VHH hit rate.`, onEvent); onEvent({ type: "citation", citation: { label: "JAM-2 paper", url: "https://doi.org/10.1101/2024.jam2" } }); onEvent({ type: "citation", citation: { label: "Chai-2", url: "https://chaidiscovery.com" } }); } else if (msg.includes("model")) { await emitToolCall("browse_models", {}, { count: MODELS.length }, onEvent); let table = "| Model | Source | Capabilities | GPU | Formats |\n|-------|--------|------------|:---:|---------|\n"; for (const m of MODELS) { table += `| ${m.name} | ${m.source} | ${m.capabilities.map((c) => c.replace(/_/g, " ")).join(", ")} | ${m.gpu_required ? m.min_vram_gb + "GB" : "CPU"} | ${m.antibody_formats.join(", ")} |\n`; } await streamText(`Model registry:\n\n${table}\nDe novo pipeline: **RFdiffusion** → **ProteinMPNN** → **Chai-1/AF2** (scoring).`, onEvent); onEvent({ type: "citation", citation: { label: "RFdiffusion", url: "https://github.com/RosettaCommons/RFdiffusion" } }); onEvent({ type: "citation", citation: { label: "ProteinMPNN", url: "https://github.com/dauparas/ProteinMPNN" } }); onEvent({ type: "citation", citation: { label: "Chai-1", url: "https://chaidiscovery.com" } }); } else if (msg.includes("target") || msg.includes("tell me about") || msg.includes("look up")) { const target = detectTarget(msg); if (target) { await emitToolCall("lookup_target", { target_name: target }, getTargetData(target), onEvent); const t = TARGETS.find((t) => t.name.toLowerCase() === target.toLowerCase()); if (t) { const bd = getTargetBindingData(target); let text = `**${t.name}** — ${t.notes}\n\n- Class: ${t.target_class}\n- PDB: ${t.pdb_id}\n`; if (bd.length > 0) { text += `\n| Benchmark | Format | Hit Rate | Best KD |\n|-----------|--------|:---:|:---:|\n`; for (const d of bd) text += `| ${d.benchmark} | ${d.format} | ${fmtPct(d.hit_rate)} | ${d.best_kd ? d.best_kd + " nM" : "-"} |\n`; } await streamText(text, onEvent); } } else { await streamText(`Available targets: ${TARGETS.map((t) => `**${t.name}**`).join(", ")}`, onEvent); } } else { await streamText( `I can help you explore:\n- **Benchmarks** — "What benchmarks exist?"\n- **Models** — "What models can do full design?"\n- **Targets** — "Tell me about HER2"\n- **Compare** — "Compare JAM-2 and Chai-2"`, onEvent ); } onEvent({ type: "done" }); } // ============================================================ // FREE CHAT (shared) // ============================================================ async function handleFreeChat(msg: string, onEvent: (ev: StreamEvent) => void) { if (isExploreQuery(msg)) { await handleExploreQuery(msg, onEvent); return; } await streamText( `In the full version, I would use the Innovation Hub and FACADS tools here.\n\n` + `For now you can:\n- Explore **benchmarks** and **models**\n- **Compare** benchmarks or **look up** targets\n- Start a new design by telling me your target protein`, onEvent ); onEvent({ type: "done" }); } // ============================================================ // UTILITIES // ============================================================ async function streamText(text: string, onEvent: (ev: StreamEvent) => void): Promise { const words = text.split(" "); for (let i = 0; i < words.length; i++) { onEvent({ type: "text_delta", text: (i === 0 ? "" : " ") + words[i] }); await sleep(18 + Math.random() * 25); } } async function emitToolCall(toolName: string, input: Record, result: unknown, onEvent: (ev: StreamEvent) => void): Promise { onEvent({ type: "tool_use", tool: toolName, input }); await sleep(350 + Math.random() * 250); onEvent({ type: "tool_result", tool: toolName, result }); await sleep(150); } function sleep(ms: number): Promise { return new Promise((r) => setTimeout(r, ms)); } function detectTarget(msg: string): string | null { for (const t of TARGETS) { if (msg.toLowerCase().includes(t.name.toLowerCase())) return t.name; } return null; } function getTargetNotes(name: string): string { return TARGETS.find((t) => t.name.toLowerCase() === name.toLowerCase())?.notes || ""; } function getTargetData(name: string): Record { const t = TARGETS.find((t) => t.name.toLowerCase() === name.toLowerCase()); if (!t) return { error: "Target not found" }; return { name: t.name, target_class: t.target_class, pdb_id: t.pdb_id, notes: t.notes, binding_data: getTargetBindingData(name) }; } function getTargetBindingData(name: string): { benchmark: string; format: string; hit_rate: number | null; best_kd: number | null }[] { const results: { benchmark: string; format: string; hit_rate: number | null; best_kd: number | null }[] = []; for (const bm of BENCHMARKS) { for (const br of bm.binding_results) { if (br.target.toLowerCase() === name.toLowerCase()) { results.push({ benchmark: bm.source, format: br.antibody_format, hit_rate: br.hit_rate, best_kd: br.best_affinity_nM }); } } } return results; } function extractBenchmarkNames(msg: string): string[] { return BENCHMARKS.filter((bm) => msg.toLowerCase().includes(bm.source.toLowerCase())).map((bm) => bm.source); } function extractChoice(msg: string, options: string[]): string { for (const opt of options) { if (msg.includes(opt)) return opt; } return ""; } function fmtPct(v: number | null): string { return v != null ? `${(v * 100).toFixed(0)}%` : "-"; } // ============================================================ // v2 EVENT HELPERS // ============================================================ // ---- Static KB cache ---- interface KBEntry { id: string; title: string; category: string; content: string; } interface KBData { version: string; entries: KBEntry[]; } let _kbCache: KBData | null = null; let _kbFetchPromise: Promise | null = null; /** Keyword map: caller name → terms used for matching KB entries. */ const KB_QUERY_TERMS: Record = { "antibody design best practices": ["design", "cdr", "fc", "bispecific", "engineering"], "benchmark catalog": ["scoring", "developability", "assessment", "metrics"], "target lookup": ["expression", "immunogenicity", "safety", "production"], "model registry": ["format", "bispecific", "fc", "knob"], }; async function fetchKB(): Promise { if (_kbCache) return _kbCache; if (_kbFetchPromise) return _kbFetchPromise; _kbFetchPromise = fetch("/kb/antibody-design-practices.json") .then((res) => { if (!res.ok) return null; return res.json() as Promise; }) .then((data) => { _kbCache = data; return data; }) .catch(() => null); return _kbFetchPromise; } function matchKBEntry(name: string, entries: KBEntry[]): KBEntry | undefined { const terms = KB_QUERY_TERMS[name] || name.toLowerCase().split(/\s+/); let best: KBEntry | undefined; let bestScore = 0; for (const entry of entries) { const hay = `${entry.title} ${entry.category} ${entry.content}`.toLowerCase(); let score = 0; for (const t of terms) { if (hay.includes(t)) score++; } if (score > bestScore) { bestScore = score; best = entry; } } return best; } async function emitKBLookup(name: string, onEvent: (ev: StreamEvent) => void): Promise { const t0 = performance.now(); const kb = await fetchKB(); const ms = Math.round(performance.now() - t0); if (kb && kb.entries.length > 0) { const entry = matchKBEntry(name, kb.entries); const label = entry ? entry.title : name; onEvent({ type: "kb_lookup", kb: { name: label, ms } }); } else { // Fallback: emit with original name if fetch failed onEvent({ type: "kb_lookup", kb: { name, ms: ms || 1 } }); } } async function emitTrace(entry: Omit, onEvent: (ev: StreamEvent) => void): Promise { const t: TraceEntry = { ts: Date.now(), ...entry }; onEvent({ type: "trace", trace: t }); } function truncate(s: string, n = 400): string { return s.length > n ? s.slice(0, n) + "…" : s; } async function emitArtifact( name: string, content: string, mime: string, onEvent: (ev: StreamEvent) => void ): Promise { const id = "art-" + Math.random().toString(36).slice(2, 10); const bytes = new TextEncoder().encode(content).length; // Demo-mode artifact — the actual content is embedded as a data: URL so the // download works fully client-side without requiring the backend to have // saved the bytes. When the real backend lands and saveArtifact() is wired // up, switch to /api/artifacts/{id} and serve from disk. const dataUrl = `data:${mime};base64,${typeof window === "undefined" ? Buffer.from(content, "utf8").toString("base64") : btoa(unescape(encodeURIComponent(content)))}`; const artifact: Artifact = { id, name, bytes, mime, url: dataUrl, createdAt: Date.now(), }; onEvent({ type: "artifact", artifact }); } function buildPlanProposal(): PlanProposal { const seed = _conv!.seed || "de novo"; const format = _conv!.format || "VHH"; const target = _conv!.target || "target"; // Plans use short names that resolve to LIVE Hub endpoints via // resolveToolEndpoint() in tool-mapping.ts. Do not reintroduce Biomni // research-repro names (RFdiffusion / ProteinMPNN / Protenix) here — // those endpoints are not live on hub-api and will 404 at run time. const steps = seed === "de novo" ? [ { label: `Generate candidates against ${target}`, tools: ["Ankh"] }, { label: `Rank by Fc effector profile`, tools: ["CRPS-Fc-DMS", "FcgR2B"] }, { label: `Predict structure (${format})`, tools: ["Chai-1", "AF2"] }, { label: "Developability triage", tools: ["Agmata", "PTM"] }, { label: "FcRn half-life screen", tools: ["FcRn-pH6"] }, ] : seed === "humanization" ? [ { label: "Baseline parent Fc profile", tools: ["FcgR2B", "FcRn-pH6"] }, { label: "Generate humanised variants", tools: ["Ankh"] }, { label: "Re-score Fc binding", tools: ["CRPS-Fc-DMS"] }, { label: "Structural QC", tools: ["Chai-1", "AF2"] }, { label: "Developability triage", tools: ["Agmata", "PTM"] }, ] : [ { label: `Variant generation on seed (${format})`, tools: ["D3PM", "Ankh"] }, { label: "Affinity ranking", tools: ["CRPS-ESM"] }, { label: "Structure prediction", tools: ["Chai-1"] }, { label: "Developability triage", tools: ["Agmata", "PTM"] }, ]; return { title: `Design plan — ${target} (${format}, ${seed})`, steps, }; } function buildDesignBriefMarkdown(): string { const c = _conv!; return [ `# Design Brief`, ``, `| Parameter | Value |`, `|-----------|-------|`, `| Target | ${c.target} |`, `| Indication | ${c.indication} |`, `| Mechanism | ${c.mechanism} |`, `| Format | ${c.format} |`, `| Epitope | ${c.epitope} |`, `| Affinity | ${c.affinity} |`, `| Species | ${c.species} |`, `| Developability | ${c.developability} |`, `| Route | ${c.route} |`, `| Competition | ${c.competition} |`, `| Starting point | ${c.seed} |`, `| Designs | ${c.scale} |`, ``, `Generated by demo-engine at ${new Date().toISOString()}.`, ``, // Pad to ~2KB for demo realism ``, ].join("\n"); } function buildTopDesignsCsv(): string { const rows: string[] = ["id,sequence,iPAE,pLDDT,score"]; const aa = "ACDEFGHIKLMNPQRSTVWY"; for (let i = 0; i < 20; i++) { const id = `d${String(i + 1).padStart(3, "0")}`; let seq = ""; for (let j = 0; j < 120; j++) seq += aa[Math.floor(Math.random() * aa.length)]; const iPAE = (5 + Math.random() * 8).toFixed(2); const pLDDT = (78 + Math.random() * 15).toFixed(1); const score = (0.45 + Math.random() * 0.45).toFixed(3); rows.push(`${id},${seq},${iPAE},${pLDDT},${score}`); } return rows.join("\n"); } function buildExecutionTraceJson(): string { const now = Date.now(); const events = [ { ts: now - 5000, kind: "thought", label: "Start campaign" }, { ts: now - 4500, kind: "tool_call", label: "plan_campaign" }, { ts: now - 4000, kind: "tool_result", label: "campaign planned" }, { ts: now - 3500, kind: "tool_call", label: "RFdiffusion" }, { ts: now - 2500, kind: "tool_result", label: "backbones generated" }, { ts: now - 2000, kind: "tool_call", label: "ProteinMPNN" }, { ts: now - 1200, kind: "tool_result", label: "sequences designed" }, { ts: now - 900, kind: "tool_call", label: "RF2" }, { ts: now - 500, kind: "tool_result", label: "filtered by iPAE" }, { ts: now - 200, kind: "tool_call", label: "Chai-1" }, { ts: now, kind: "tool_result", label: "consensus scored" }, ]; // Pad JSON toward ~12KB const padding = Array.from({ length: 80 }, (_, i) => ({ ts: now - i * 10, kind: "text", label: `step-detail-${i}`, detail: "RFdiffusion/ProteinMPNN/RF2 intermediate log entry with structural metrics", })); return JSON.stringify({ events: [...events, ...padding] }, null, 2); } // Citation helper kept for parity with other emitters; unused today but exported intent // stays local for future use. function _citationEvent(c: Citation): StreamEvent { return { type: "citation", citation: c }; } void _citationEvent;