muthuk1 commited on
Commit
ddb116f
Β·
verified Β·
1 Parent(s): 71f60e2

Add multi-provider compare API route + providers listing endpoint

Browse files
Files changed (1) hide show
  1. web/src/app/api/compare/route.ts +91 -103
web/src/app/api/compare/route.ts CHANGED
@@ -1,16 +1,13 @@
1
  import { NextRequest, NextResponse } from "next/server";
 
2
 
3
  export const runtime = "nodejs";
4
  export const dynamic = "force-dynamic";
5
 
6
- // Initialize Anthropic client lazily
7
- async function getClaude() {
8
- const Anthropic = (await import("@anthropic-ai/sdk")).default;
9
- return new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
10
- }
11
-
12
  interface CompareRequest {
13
  query: string;
 
 
14
  adaptiveRouting?: boolean;
15
  topK?: number;
16
  hops?: number;
@@ -19,162 +16,153 @@ interface CompareRequest {
19
  export async function POST(req: NextRequest) {
20
  try {
21
  const body: CompareRequest = await req.json();
22
- const { query, adaptiveRouting = true } = body;
23
 
24
  if (!query?.trim()) {
25
  return NextResponse.json({ error: "Query required" }, { status: 400 });
26
  }
27
 
28
- const apiKey = process.env.ANTHROPIC_API_KEY;
 
 
 
 
29
 
30
- // If no API key, return demo data
31
- if (!apiKey) {
32
- return NextResponse.json(getDemoResponse(query));
33
  }
34
 
35
- const claude = await getClaude();
36
  const startTime = Date.now();
37
 
38
  // ── Pipeline A: Baseline RAG ────────────────────────
39
- const baselineStart = Date.now();
40
- const baselineMsg = await claude.messages.create({
41
- model: "claude-sonnet-4-20250514",
42
- max_tokens: 512,
43
- system: "You are a helpful assistant. Answer the question accurately and concisely. If you don't have enough information, say so.",
44
- messages: [{ role: "user", content: `Question: ${query}\n\nAnswer:` }],
 
 
 
45
  });
46
 
47
- const baselineText = baselineMsg.content[0].type === "text" ? baselineMsg.content[0].text : "";
48
- const baselineLatency = Date.now() - baselineStart;
49
- const baselineCost =
50
- (baselineMsg.usage.input_tokens / 1000) * 0.003 +
51
- (baselineMsg.usage.output_tokens / 1000) * 0.015;
52
-
53
  // ── Pipeline B: GraphRAG ────────────────────────────
54
- const graphragStart = Date.now();
55
-
56
- // Step 1: Extract keywords
57
- const kwMsg = await claude.messages.create({
58
- model: "claude-sonnet-4-20250514",
59
- max_tokens: 256,
60
- system: "Extract search keywords. Return JSON only: {\"high_level\": [\"themes\"], \"low_level\": [\"entities\"]}",
61
- messages: [{ role: "user", content: query }],
 
 
 
62
  });
63
- const kwText = kwMsg.content[0].type === "text" ? kwMsg.content[0].text : "{}";
64
-
65
- // Step 2: Entity extraction (simulated graph traversal)
66
- const entityMsg = await claude.messages.create({
67
- model: "claude-sonnet-4-20250514",
68
- max_tokens: 1024,
69
- system: `You are a knowledge graph builder. Extract entities and relationships for the question.
70
- Return JSON: {"entities": [{"name": "...", "type": "PERSON|ORG|LOCATION|EVENT|CONCEPT"}], "relations": [{"source": "name", "target": "name", "type": "RELATIONSHIP_TYPE", "description": "brief"}]}`,
71
- messages: [{ role: "user", content: query }],
 
 
 
 
 
72
  });
73
- const entityText = entityMsg.content[0].type === "text" ? entityMsg.content[0].text : "{}";
74
 
75
  let entities: string[] = [];
76
  let relations: string[] = [];
77
  try {
78
- const parsed = JSON.parse(entityText);
79
  entities = (parsed.entities || []).map((e: { name: string }) => e.name);
80
  relations = (parsed.relations || []).map(
81
  (r: { source: string; type: string; target: string; description?: string }) =>
82
  `${r.source} -[${r.type}]-> ${r.target}: ${r.description || ""}`
83
  );
84
- } catch { /* ignore parse errors */ }
85
 
86
- // Step 3: Generate with structured graph context
87
- const graphContext = `### Entities Found:\n${entities.map((e) => `- ${e}`).join("\n")}\n\n### Relationships:\n${relations.map((r) => `- ${r}`).join("\n")}`;
88
 
89
- const graphragMsg = await claude.messages.create({
90
- model: "claude-sonnet-4-20250514",
91
- max_tokens: 512,
92
- system: "You are a knowledgeable assistant with access to a knowledge graph. Use the entities and relationships to answer accurately. Follow relationship chains for multi-hop reasoning. Be concise but thorough.",
93
- messages: [{ role: "user", content: `Context:\n${graphContext}\n\nQuestion: ${query}\n\nAnswer:` }],
 
 
 
 
94
  });
95
 
96
- const graphragText = graphragMsg.content[0].type === "text" ? graphragMsg.content[0].text : "";
97
- const graphragLatency = Date.now() - graphragStart;
98
- const graphragTokens =
99
- kwMsg.usage.input_tokens + kwMsg.usage.output_tokens +
100
- entityMsg.usage.input_tokens + entityMsg.usage.output_tokens +
101
- graphragMsg.usage.input_tokens + graphragMsg.usage.output_tokens;
102
- const graphragCost =
103
- ((kwMsg.usage.input_tokens + entityMsg.usage.input_tokens + graphragMsg.usage.input_tokens) / 1000) * 0.003 +
104
- ((kwMsg.usage.output_tokens + entityMsg.usage.output_tokens + graphragMsg.usage.output_tokens) / 1000) * 0.015;
105
-
106
- // ── Adaptive Routing ────────────────────────────────
107
- let complexity = 0.5;
108
- let queryType = "unknown";
109
- let recommended = "baseline";
110
 
 
 
111
  if (adaptiveRouting) {
112
- // Simple heuristic + LLM analysis
113
- const hasMultipleEntities = entities.length > 2;
114
- const hasComparison = /same|both|compare|which.*first|who.*born/i.test(query);
115
- const hasMultiHop = relations.length > 2;
116
-
117
- complexity = (hasMultipleEntities ? 0.3 : 0) + (hasComparison ? 0.2 : 0) + (hasMultiHop ? 0.3 : 0.1);
118
- complexity = Math.min(complexity + 0.1, 1.0);
119
- queryType = hasComparison ? "comparison" : hasMultiHop ? "multi_hop" : "factoid";
120
  recommended = complexity >= 0.6 ? "graphrag" : "baseline";
121
  }
122
 
123
  return NextResponse.json({
124
  baseline: {
125
- answer: baselineText,
126
- tokens: baselineMsg.usage.input_tokens + baselineMsg.usage.output_tokens,
127
- latencyMs: baselineLatency,
128
- costUsd: baselineCost,
129
  entities: [],
130
  relations: [],
131
  },
132
  graphrag: {
133
- answer: graphragText,
134
- tokens: graphragTokens,
135
  latencyMs: graphragLatency,
136
- costUsd: graphragCost,
137
  entities,
138
  relations,
139
  },
140
  complexity,
141
  queryType,
142
  recommended,
 
 
143
  totalTimeMs: Date.now() - startTime,
144
  });
145
  } catch (error) {
146
  console.error("Compare API error:", error);
147
- // Return demo data on error
148
- return NextResponse.json(getDemoResponse(""));
149
  }
150
  }
151
 
152
- function getDemoResponse(query: string) {
153
  return {
154
  baseline: {
155
- answer: "Based on available information, both Scott Derrickson and Ed Wood were American filmmakers, so yes, they shared the same nationality.",
156
- tokens: 847,
157
- latencyMs: 1240,
158
- costUsd: 0.000203,
159
- entities: [],
160
- relations: [],
161
  },
162
  graphrag: {
163
- answer: "Yes. Scott Derrickson (born in Denver, Colorado, USA) and Ed Wood (born in Poughkeepsie, New York, USA) were both American. Following the NATIONALITY relationships in the knowledge graph: Derrickson β†’ Denver β†’ USA; Wood β†’ Poughkeepsie β†’ USA. Both paths converge at United States, confirming shared American nationality.",
164
- tokens: 2134,
165
- latencyMs: 3820,
166
- costUsd: 0.000518,
167
  entities: ["Scott Derrickson", "Ed Wood", "United States", "Denver", "Poughkeepsie"],
168
- relations: [
169
- "Scott Derrickson -[BORN_IN]-> Denver, Colorado",
170
- "Denver -[LOCATED_IN]-> United States",
171
- "Ed Wood -[BORN_IN]-> Poughkeepsie, New York",
172
- "Poughkeepsie -[LOCATED_IN]-> United States",
173
- ],
174
  },
175
- complexity: 0.72,
176
- queryType: "comparison",
177
- recommended: "graphrag",
178
- totalTimeMs: 5060,
179
  };
180
  }
 
1
  import { NextRequest, NextResponse } from "next/server";
2
+ import { callLLM, PROVIDERS, type ProviderId } from "@/lib/llm-providers";
3
 
4
  export const runtime = "nodejs";
5
  export const dynamic = "force-dynamic";
6
 
 
 
 
 
 
 
7
  interface CompareRequest {
8
  query: string;
9
+ provider?: ProviderId;
10
+ model?: string;
11
  adaptiveRouting?: boolean;
12
  topK?: number;
13
  hops?: number;
 
16
  export async function POST(req: NextRequest) {
17
  try {
18
  const body: CompareRequest = await req.json();
19
+ const { query, provider = "anthropic", model, adaptiveRouting = true } = body;
20
 
21
  if (!query?.trim()) {
22
  return NextResponse.json({ error: "Query required" }, { status: 400 });
23
  }
24
 
25
+ // Check if provider has API key (or is local)
26
+ const providerConfig = PROVIDERS[provider];
27
+ if (!providerConfig) {
28
+ return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 400 });
29
+ }
30
 
31
+ const hasKey = providerConfig.isLocal || !providerConfig.requiresApiKey || !!process.env[providerConfig.apiKeyEnv];
32
+ if (!hasKey) {
33
+ return NextResponse.json(getDemoResponse(query, provider));
34
  }
35
 
36
+ const selectedModel = model || providerConfig.defaultModel;
37
  const startTime = Date.now();
38
 
39
  // ── Pipeline A: Baseline RAG ────────────────────────
40
+ const baselineResp = await callLLM({
41
+ provider,
42
+ model: selectedModel,
43
+ messages: [
44
+ { role: "system", content: "You are a helpful assistant. Answer the question accurately and concisely." },
45
+ { role: "user", content: `Question: ${query}\n\nAnswer:` },
46
+ ],
47
+ temperature: 0,
48
+ maxTokens: 512,
49
  });
50
 
 
 
 
 
 
 
51
  // ── Pipeline B: GraphRAG ────────────────────────────
52
+ // Step 1: Keywords
53
+ const kwResp = await callLLM({
54
+ provider,
55
+ model: selectedModel,
56
+ messages: [
57
+ { role: "system", content: 'Extract keywords. Return JSON: {"high_level": ["themes"], "low_level": ["entities"]}' },
58
+ { role: "user", content: query },
59
+ ],
60
+ temperature: 0,
61
+ maxTokens: 256,
62
+ jsonMode: providerConfig.supportsJSON,
63
  });
64
+
65
+ // Step 2: Entity extraction
66
+ const entityResp = await callLLM({
67
+ provider,
68
+ model: selectedModel,
69
+ messages: [
70
+ { role: "system", content: `Extract entities and relationships. Return JSON:
71
+ {"entities": [{"name": "...", "type": "PERSON|ORG|LOCATION|EVENT|CONCEPT"}],
72
+ "relations": [{"source": "name", "target": "name", "type": "...", "description": "brief"}]}` },
73
+ { role: "user", content: query },
74
+ ],
75
+ temperature: 0,
76
+ maxTokens: 1024,
77
+ jsonMode: providerConfig.supportsJSON,
78
  });
 
79
 
80
  let entities: string[] = [];
81
  let relations: string[] = [];
82
  try {
83
+ const parsed = JSON.parse(entityResp.content);
84
  entities = (parsed.entities || []).map((e: { name: string }) => e.name);
85
  relations = (parsed.relations || []).map(
86
  (r: { source: string; type: string; target: string; description?: string }) =>
87
  `${r.source} -[${r.type}]-> ${r.target}: ${r.description || ""}`
88
  );
89
+ } catch { /* parse errors OK β€” content may not be pure JSON */ }
90
 
91
+ // Step 3: Generate with graph context
92
+ const graphContext = `### Entities:\n${entities.map((e) => `- ${e}`).join("\n")}\n\n### Relations:\n${relations.map((r) => `- ${r}`).join("\n")}`;
93
 
94
+ const graphragResp = await callLLM({
95
+ provider,
96
+ model: selectedModel,
97
+ messages: [
98
+ { role: "system", content: "You are a knowledgeable assistant with knowledge graph access. Use entities and relationships to answer accurately. Follow relationship chains for multi-hop reasoning. Be concise." },
99
+ { role: "user", content: `Context:\n${graphContext}\n\nQuestion: ${query}\n\nAnswer:` },
100
+ ],
101
+ temperature: 0,
102
+ maxTokens: 512,
103
  });
104
 
105
+ const graphragTotalTokens = kwResp.totalTokens + entityResp.totalTokens + graphragResp.totalTokens;
106
+ const graphragTotalCost = kwResp.costUsd + entityResp.costUsd + graphragResp.costUsd;
107
+ const graphragLatency = kwResp.latencyMs + entityResp.latencyMs + graphragResp.latencyMs;
 
 
 
 
 
 
 
 
 
 
 
108
 
109
+ // Adaptive routing
110
+ let complexity = 0.5, queryType = "unknown", recommended = "baseline";
111
  if (adaptiveRouting) {
112
+ const multi = entities.length > 2;
113
+ const compare = /same|both|compare|which.*first|who.*born|difference/i.test(query);
114
+ const hops = relations.length > 2;
115
+ complexity = Math.min((multi ? 0.3 : 0) + (compare ? 0.2 : 0) + (hops ? 0.3 : 0.1) + 0.1, 1.0);
116
+ queryType = compare ? "comparison" : hops ? "multi_hop" : "factoid";
 
 
 
117
  recommended = complexity >= 0.6 ? "graphrag" : "baseline";
118
  }
119
 
120
  return NextResponse.json({
121
  baseline: {
122
+ answer: baselineResp.content,
123
+ tokens: baselineResp.totalTokens,
124
+ latencyMs: baselineResp.latencyMs,
125
+ costUsd: baselineResp.costUsd,
126
  entities: [],
127
  relations: [],
128
  },
129
  graphrag: {
130
+ answer: graphragResp.content,
131
+ tokens: graphragTotalTokens,
132
  latencyMs: graphragLatency,
133
+ costUsd: graphragTotalCost,
134
  entities,
135
  relations,
136
  },
137
  complexity,
138
  queryType,
139
  recommended,
140
+ provider,
141
+ model: selectedModel,
142
  totalTimeMs: Date.now() - startTime,
143
  });
144
  } catch (error) {
145
  console.error("Compare API error:", error);
146
+ const errMsg = error instanceof Error ? error.message : "Unknown error";
147
+ return NextResponse.json(getDemoResponse("", "anthropic", errMsg));
148
  }
149
  }
150
 
151
+ function getDemoResponse(query: string, provider: string, error?: string) {
152
  return {
153
  baseline: {
154
+ answer: "Both Scott Derrickson and Ed Wood were American filmmakers.",
155
+ tokens: 847, latencyMs: 1240, costUsd: 0.000203,
156
+ entities: [], relations: [],
 
 
 
157
  },
158
  graphrag: {
159
+ answer: "Yes. Scott Derrickson (Denver, CO β†’ USA) and Ed Wood (Poughkeepsie, NY β†’ USA) were both American. Graph traversal confirms shared nationality via BORN_IN β†’ LOCATED_IN β†’ United States paths.",
160
+ tokens: 2134, latencyMs: 3820, costUsd: 0.000518,
 
 
161
  entities: ["Scott Derrickson", "Ed Wood", "United States", "Denver", "Poughkeepsie"],
162
+ relations: ["Scott Derrickson -[BORN_IN]-> Denver", "Denver -[LOCATED_IN]-> United States", "Ed Wood -[BORN_IN]-> Poughkeepsie", "Poughkeepsie -[LOCATED_IN]-> United States"],
 
 
 
 
 
163
  },
164
+ complexity: 0.72, queryType: "comparison", recommended: "graphrag",
165
+ provider, model: "demo-mode", totalTimeMs: 5060,
166
+ ...(error ? { demoMode: true, demoReason: error } : { demoMode: true, demoReason: "No API key configured" }),
 
167
  };
168
  }