| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |
|
|
| |
| let _inputFile: File | null = null; |
|
|
| |
| 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<string, string> { |
| if (!_conv) return {}; |
| const b: Record<string, string> = {}; |
| 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<void> { |
| if (!_conv) return; |
| _conv.messageCount++; |
| const msg = message.toLowerCase().trim(); |
|
|
| |
| if (isExploreQuery(msg)) { |
| await handleExploreQuery(msg, onEvent); |
| return; |
| } |
|
|
| if (_conv.mode === "plan") { |
| await handlePlanMode(msg, onEvent); |
| } else { |
| await handleActionMode(msg, onEvent); |
| } |
| } |
|
|
| |
| |
| |
|
|
| 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) { |
| |
| 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 |
| ); |
|
|
| |
| onEvent({ type: "plan", plan: buildPlanProposal() }); |
| } |
|
|
| async function executePlan(onEvent: (ev: StreamEvent) => void) { |
| await emitTrace({ kind: "thought", label: `Launching campaign for ${_conv!.target}` }, onEvent); |
|
|
| |
| const plan = buildPlanProposal(); |
| const pipelineTools = plan.steps.flatMap((s) => s.tools); |
|
|
| try { |
| |
| 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); |
|
|
| |
| 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 { |
| |
| await streamText(`**${tool}**: cost estimate unavailable\n`, onEvent); |
| } |
| } |
|
|
| await streamText("\nSubmitting jobs to the GPU cluster...\n\n", onEvent); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| const POLL_INTERVAL_MS = 5_000; |
| const MAX_POLL_MS = 10 * 60 * 1_000; |
| const pollStart = Date.now(); |
| let finalStatus: Awaited<ReturnType<typeof getPhyloJobStatus>> | 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; |
| } |
| |
| } catch { |
| |
| } |
| } |
|
|
| 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 }); |
|
|
| |
| 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); |
| } |
| } |
|
|
| |
| 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, |
| ); |
|
|
| |
| const designBriefMd = buildDesignBriefMarkdown(); |
| await emitArtifact("design_brief.md", designBriefMd, "text/markdown", onEvent); |
| } catch (err: unknown) { |
| |
| const errMsg = err instanceof Error ? err.message : String(err); |
| await streamText( |
| `> **Backend unreachable** -- showing demo results. (${errMsg})\n\n`, |
| onEvent, |
| ); |
| await executePlanDemo(onEvent); |
| } |
| } |
|
|
| |
| 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); |
| } |
|
|
| |
| function buildMinimalFasta(target: string, seed: string): string { |
| |
| const placeholder = "MEVQLVESGGGLVQPGGSLRLSCAASGFTFSSYAMSWVRQAPGKGLEWVSAISGSGGSTYYADSVKGRFTISRDNSKNTLYLQMNSLRAEDTAVYYCAR"; |
| const header = `>${target}_design seed=${seed || "de_novo"}`; |
| return `${header}\n${placeholder}\n`; |
| } |
|
|
| |
| |
| |
|
|
| 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; |
|
|
| |
| await emitKBLookup("antibody design best practices", onEvent); |
| await emitKBLookup("target lookup", onEvent); |
|
|
| |
| 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); |
|
|
| |
| await emitToolCall("browse_benchmarks", {}, { count: BENCHMARKS.length }, onEvent); |
|
|
| |
| const format = tClass === "gpcr" ? "VHH" : "VHH"; |
| _conv!.format = format; |
|
|
| |
| await emitToolCall("browse_models", { capability: "full_design" }, { count: 1 }, onEvent); |
|
|
| |
| _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() }); |
| } |
|
|
| |
| |
| |
|
|
| 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) { |
| |
| 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" }); |
| } |
|
|
| |
| |
| |
|
|
| 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" }); |
| } |
|
|
| |
| |
| |
|
|
| async function streamText(text: string, onEvent: (ev: StreamEvent) => void): Promise<void> { |
| 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<string, unknown>, result: unknown, onEvent: (ev: StreamEvent) => void): Promise<void> { |
| 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<void> { |
| 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<string, unknown> { |
| 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)}%` : "-"; |
| } |
|
|
| |
| |
| |
|
|
| |
|
|
| interface KBEntry { |
| id: string; |
| title: string; |
| category: string; |
| content: string; |
| } |
|
|
| interface KBData { |
| version: string; |
| entries: KBEntry[]; |
| } |
|
|
| let _kbCache: KBData | null = null; |
| let _kbFetchPromise: Promise<KBData | null> | null = null; |
|
|
| |
| const KB_QUERY_TERMS: Record<string, string[]> = { |
| "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<KBData | null> { |
| 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<KBData>; |
| }) |
| .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<void> { |
| 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 { |
| |
| onEvent({ type: "kb_lookup", kb: { name, ms: ms || 1 } }); |
| } |
| } |
|
|
| async function emitTrace(entry: Omit<TraceEntry, "ts">, onEvent: (ev: StreamEvent) => void): Promise<void> { |
| 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<void> { |
| const id = "art-" + Math.random().toString(36).slice(2, 10); |
| const bytes = new TextEncoder().encode(content).length; |
| |
| |
| |
| |
| 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"; |
|
|
| |
| |
| |
| |
| 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()}.`, |
| ``, |
| |
| `<!-- ${"notes ".repeat(200)}-->`, |
| ].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" }, |
| ]; |
| |
| 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); |
| } |
|
|
| |
| |
| function _citationEvent(c: Citation): StreamEvent { |
| return { type: "citation", citation: c }; |
| } |
| void _citationEvent; |
|
|