import { NextRequest, NextResponse } from "next/server"; export const dynamic = "force-dynamic"; export const maxDuration = 60; /** * POST /api/llm/command * * Natural language → structured command interpreter. * Takes spoken text from the voice layer and returns a structured * command that the frontend can execute (navigate, run LLM, record, etc). * * Body: * text: string — the spoken or typed command * context?: string — current page for context-aware interpretation * * Returns: * { * action: "navigate" | "run_research" | "run_confounders" | "run_challenge" | * "run_derivatives" | "run_assess" | "record_outcome" | "allocate" | * "accept" | "reject" | "speak" | "unknown", * target?: string — route path for navigate, or specific target * params?: Record — additional parameters * speech?: string — text to speak back to the user * llmUsed: boolean, * llmError?: string, * } */ export async function POST(request: NextRequest) { try { const body = await request.json().catch(() => ({})); const text = body.text; const context = body.context || "/today"; const conversation = body.conversation || ""; if (!text) { return NextResponse.json({ error: "text is required" }, { status: 400 }); } // First try deterministic matching for speed (no LLM needed) const lower = text.toLowerCase().trim(); const deterministic = matchDeterministic(lower, context); if (deterministic) { return NextResponse.json({ ...deterministic, llmUsed: false }); } // Fall back to LLM interpretation for complex commands const result = await interpretWithLLM(text, context, conversation); return NextResponse.json(result); } catch (e) { console.error("[llm/command] error:", e); return NextResponse.json( { action: "unknown", speech: "I couldn't process that command.", llmUsed: false, llmError: String(e) }, { status: 200 } ); } } function matchDeterministic(lower: string, context: string): any | null { // Navigation commands const navMap: Record = { "today": { route: "/today", label: "Today's Hypothesis" }, "inbox": { route: "/inbox", label: "Inbox Intelligence" }, "foundry": { route: "/foundry", label: "Hypothesis Foundry" }, "experiment": { route: "/experiment", label: "Experiment" }, "results": { route: "/results", label: "Discovery Canopy" }, "golden nodes": { route: "/golden-nodes", label: "Golden Nodes" }, "golden node": { route: "/golden-nodes", label: "Golden Nodes" }, "history": { route: "/history", label: "History" }, "leaderboard": { route: "/results", label: "Discovery Canopy" }, "canopy": { route: "/results", label: "Discovery Canopy" }, "organism": { route: "/foundry", label: "Hypothesis Foundry" }, }; // Check "go to X" / "navigate to X" / "open X" / "show X" for (const [key, val] of Object.entries(navMap)) { if (lower.includes(`go to ${key}`) || lower.includes(`navigate to ${key}`) || lower.includes(`open ${key}`) || lower.includes(`show ${key}`) || lower.includes(`take me to ${key}`) || lower === key) { return { action: "navigate", target: val.route, speech: `Navigating to ${val.label}.`, }; } } // Action commands if (lower.includes("run research") || lower.includes("prior art") || lower.includes("research this")) { return { action: "run_research", speech: "Running cross-category prior-art research." }; } if (lower.includes("confounder") || lower.includes("attack the hypothesis")) { return { action: "run_confounders", speech: "Analyzing potential confounders." }; } if (lower.includes("challenge") || lower.includes("adversarial")) { return { action: "run_challenge", speech: "Launching adversarial challenge." }; } if (lower.includes("derivative") || lower.includes("generate variant")) { return { action: "run_derivatives", speech: "Generating derivative hypotheses." }; } if (lower.includes("assess") || lower.includes("golden node assessment") || lower.includes("evaluate for golden")) { return { action: "run_assess", speech: "Running Golden Node assessment." }; } if (lower.includes("allocate") || lower.includes("plant seed") || lower.includes("daily seed") || lower.includes("new hypothesis")) { return { action: "allocate", speech: "Planting a new Daily Seed." }; } if (lower.includes("accept") || lower.includes("accept mission") || lower.includes("accept hypothesis")) { return { action: "accept", speech: "Mission accepted." }; } if (lower.includes("reject") || lower.includes("reject mission")) { return { action: "reject", speech: "Mission rejected." }; } if (lower.includes("record outcome") || lower.includes("record result") || lower.includes("submit outcome")) { return { action: "record_outcome", speech: "Opening outcome recording form." }; } if (lower.includes("analyze inbox") || lower.includes("analyze email")) { return { action: "analyze_inbox", speech: "Analyzing inbox for research signals." }; } if (lower.includes("audit fairness") || lower.includes("fairness audit")) { return { action: "audit_fairness", speech: "Running fairness audit." }; } if (lower.includes("generate insight") || lower.includes("leaderboard insight")) { return { action: "generate_insight", speech: "Generating leaderboard insight." }; } if (lower.includes("explain lineage") || lower.includes("lineage")) { return { action: "explain_lineage", speech: "Explaining research lineage." }; } if (lower.includes("generate protocol") || lower.includes("experiment protocol")) { return { action: "generate_protocol", speech: "Generating experiment protocol." }; } // Email Lab commands const emailLabMap: Record = { "email lab": "/email-lab", "email experiment": "/email-lab", }; for (const [key, route] of Object.entries(emailLabMap)) { if (lower.includes(`go to ${key}`) || lower.includes(`open ${key}`) || lower.includes(`show ${key}`)) { return { action: "navigate", target: route, speech: `Navigating to Email Lab.` }; } } if (lower.includes("detect email signals") || lower.includes("scan email") || lower.includes("email signals")) { return { action: "detect_email_signals", speech: "Scanning mailbox for email behavioral signals." }; } if (lower.includes("generate hypotheses") || lower.includes("competing hypotheses") || lower.includes("email hypotheses")) { return { action: "generate_hypotheses", speech: "Generating competing email hypotheses." }; } if (lower.includes("run email experiment") || lower.includes("email experiment")) { return { action: "run_email_experiment", speech: "Creating and approving email experiment." }; } if (lower.includes("promote golden node") || lower.includes("promote email golden")) { return { action: "promote_golden_node", speech: "Promoting to Golden Node." }; } if (lower.includes("reverse falsify") || lower.includes("palindrome test") || lower.includes("attack the method")) { return { action: "reverse_falsify", speech: "Generating reverse falsification tests." }; } // Status / page reading — handled by VoiceContext on the client if (lower === "status" || lower === "what's here" || lower === "what is here" || lower === "summarize" || lower === "what do i have" || lower === "what am i looking at" || lower.includes("read the page") || lower.includes("what's on this page") || lower.includes("what is on this page")) { return { action: "status", speech: "Reading current page state." }; } // Help if (lower.includes("help") || lower.includes("what can you do")) { return { action: "speak", speech: "You can say: go to foundry, run research, attack with confounders, challenge the hypothesis, generate derivatives, assess for golden node, allocate a seed, accept mission, record outcome, or analyze inbox.", }; } return null; } async function interpretWithLLM(text: string, context: string, conversation: string): Promise { const systemPrompt = `You are Foundry, the voice intelligence for Advantage Foundry, a pharma research innovation platform. The user spoke a voice command. Interpret it and return ONLY valid JSON. You are not a generic assistant. You are Foundry — direct, scientific, slightly intense. Your speech should be brief, confident, and action-oriented. No filler words. Current page context: ${context} ${conversation ? `Recent conversation:\n${conversation}\n` : ""} Available actions: - navigate: go to a page (target: /today, /foundry, /experiment, /results, /golden-nodes, /history, /inbox, /email-lab, /voice-demo) - run_research: run LLM cross-category prior-art research - run_confounders: detect confounders for the current hypothesis - run_challenge: adversarial challenge against the hypothesis - run_derivatives: generate derivative hypotheses - run_assess: assess for Golden Node promotion - allocate: plant a new Daily Seed hypothesis - accept: accept the current mission - reject: reject the current mission - record_outcome: record an experiment outcome - analyze_inbox: analyze inbox for research signals - audit_fairness: run fairness audit - generate_insight: generate leaderboard insight - explain_lineage: explain a Golden Node's research lineage - generate_protocol: generate an experiment protocol - detect_email_signals: scan mailbox for email behavioral signals - generate_hypotheses: generate competing email hypotheses - run_email_experiment: create and approve an email experiment - promote_golden_node: promote a winning email experiment to Golden Node - reverse_falsify: generate reverse falsification tests for a Golden Node - speak: just respond with speech (for questions or comments) - unknown: cannot interpret Return JSON: { "action": "...", "target": "...", "params": {}, "speech": "brief Foundry-style confirmation to speak aloud" }`; try { const res = await fetch("https://api.llm7.io/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ model: "gpt-oss:20b", messages: [ { role: "system", content: systemPrompt }, { role: "user", content: text }, ], temperature: 0.2, max_tokens: 512, }), signal: AbortSignal.timeout(30000), }); if (!res.ok) throw new Error(`LLM HTTP ${res.status}`); const data = await res.json(); const content = data.choices?.[0]?.message?.content || ""; // Extract JSON const jsonMatch = content.match(/\{[\s\S]*\}/); if (jsonMatch) { const parsed = JSON.parse(jsonMatch[0]); return { ...parsed, llmUsed: true }; } return { action: "unknown", speech: "I couldn't interpret that command.", llmUsed: true }; } catch (e) { return { action: "unknown", speech: "Command interpretation failed. Try saying 'go to foundry' or 'run research'.", llmUsed: false, llmError: String(e), }; } }