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),
    };
  }
}