over / src /app /api /llm /command /route.ts
luguog's picture
fix(microsoft): use configurable Azure AD client ID for device-code flow
3f13033 verified
Raw
History Blame Contribute Delete
11.2 kB
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<string, unknown> — 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<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" },
};
// 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<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." };
}
// 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<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 || "";
// 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),
};
}
}