| import { NextRequest, NextResponse } from "next/server"; |
|
|
| export const dynamic = "force-dynamic"; |
| export const maxDuration = 60; |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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 }); |
| } |
|
|
| |
| const lower = text.toLowerCase().trim(); |
| const deterministic = matchDeterministic(lower, context); |
| if (deterministic) { |
| return NextResponse.json({ ...deterministic, llmUsed: false }); |
| } |
|
|
| |
| 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 { |
| |
| const navMap: Record<string, { route: string; label: string }> = { |
| "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" }, |
| }; |
|
|
| |
| 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}.`, |
| }; |
| } |
| } |
|
|
| |
| 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." }; |
| } |
|
|
| |
| const emailLabMap: Record<string, string> = { |
| "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." }; |
| } |
|
|
| |
| 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." }; |
| } |
|
|
| |
| 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<any> { |
| 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 || ""; |
| |
| 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), |
| }; |
| } |
| } |
|
|