File size: 11,222 Bytes
3f13033 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 | 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),
};
}
}
|