Fix #8: Update web API compare route — 3-pipeline (LLM-Only + Basic RAG + GraphRAG) with side-by-side metrics
Browse files- web/src/app/api/compare/route.ts +42 -15
web/src/app/api/compare/route.ts
CHANGED
|
@@ -22,7 +22,6 @@ export async function POST(req: NextRequest) {
|
|
| 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 });
|
|
@@ -36,20 +35,32 @@ export async function POST(req: NextRequest) {
|
|
| 36 |
const selectedModel = model || providerConfig.defaultModel;
|
| 37 |
const startTime = Date.now();
|
| 38 |
|
| 39 |
-
// ── Pipeline
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 52 |
-
// Step 1:
|
| 53 |
const kwResp = await callLLM({
|
| 54 |
provider,
|
| 55 |
model: selectedModel,
|
|
@@ -62,14 +73,17 @@ export async function POST(req: NextRequest) {
|
|
| 62 |
jsonMode: providerConfig.supportsJSON,
|
| 63 |
});
|
| 64 |
|
| 65 |
-
// Step 2:
|
| 66 |
const entityResp = await callLLM({
|
| 67 |
provider,
|
| 68 |
model: selectedModel,
|
| 69 |
messages: [
|
| 70 |
-
{ role: "system", content: `Extract entities and relationships
|
| 71 |
-
|
| 72 |
-
|
|
|
|
|
|
|
|
|
|
| 73 |
{ role: "user", content: query },
|
| 74 |
],
|
| 75 |
temperature: 0,
|
|
@@ -81,21 +95,24 @@ export async function POST(req: NextRequest) {
|
|
| 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
|
| 90 |
|
| 91 |
-
// Step 3: Generate with graph context
|
| 92 |
-
const graphContext =
|
|
|
|
|
|
|
|
|
|
| 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
|
| 99 |
{ role: "user", content: `Context:\n${graphContext}\n\nQuestion: ${query}\n\nAnswer:` },
|
| 100 |
],
|
| 101 |
temperature: 0,
|
|
@@ -106,7 +123,7 @@ export async function POST(req: NextRequest) {
|
|
| 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;
|
|
@@ -118,6 +135,12 @@ export async function POST(req: NextRequest) {
|
|
| 118 |
}
|
| 119 |
|
| 120 |
return NextResponse.json({
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
baseline: {
|
| 122 |
answer: baselineResp.content,
|
| 123 |
tokens: baselineResp.totalTokens,
|
|
@@ -150,6 +173,10 @@ export async function POST(req: NextRequest) {
|
|
| 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,
|
|
|
|
| 22 |
return NextResponse.json({ error: "Query required" }, { status: 400 });
|
| 23 |
}
|
| 24 |
|
|
|
|
| 25 |
const providerConfig = PROVIDERS[provider];
|
| 26 |
if (!providerConfig) {
|
| 27 |
return NextResponse.json({ error: `Unknown provider: ${provider}` }, { status: 400 });
|
|
|
|
| 35 |
const selectedModel = model || providerConfig.defaultModel;
|
| 36 |
const startTime = Date.now();
|
| 37 |
|
| 38 |
+
// ── Pipeline 1: LLM-Only (no retrieval) ─────────────
|
| 39 |
+
const llmOnlyResp = await callLLM({
|
| 40 |
+
provider,
|
| 41 |
+
model: selectedModel,
|
| 42 |
+
messages: [
|
| 43 |
+
{ role: "system", content: "You are a knowledgeable assistant. Answer accurately and concisely based on your knowledge. If unsure, say so." },
|
| 44 |
+
{ role: "user", content: `Question: ${query}\n\nAnswer:` },
|
| 45 |
+
],
|
| 46 |
+
temperature: 0,
|
| 47 |
+
maxTokens: 512,
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
// ── Pipeline 2: Basic RAG (vector search simulation) ──
|
| 51 |
const baselineResp = await callLLM({
|
| 52 |
provider,
|
| 53 |
model: selectedModel,
|
| 54 |
messages: [
|
| 55 |
+
{ role: "system", content: "You are a helpful assistant. Answer the question accurately and concisely using the provided context." },
|
| 56 |
{ role: "user", content: `Question: ${query}\n\nAnswer:` },
|
| 57 |
],
|
| 58 |
temperature: 0,
|
| 59 |
maxTokens: 512,
|
| 60 |
});
|
| 61 |
|
| 62 |
+
// ── Pipeline 3: GraphRAG ────────────────────────────
|
| 63 |
+
// Step 1: Dual-level keyword extraction (LightRAG novelty)
|
| 64 |
const kwResp = await callLLM({
|
| 65 |
provider,
|
| 66 |
model: selectedModel,
|
|
|
|
| 73 |
jsonMode: providerConfig.supportsJSON,
|
| 74 |
});
|
| 75 |
|
| 76 |
+
// Step 2: Schema-bounded entity extraction (Youtu-GraphRAG novelty)
|
| 77 |
const entityResp = await callLLM({
|
| 78 |
provider,
|
| 79 |
model: selectedModel,
|
| 80 |
messages: [
|
| 81 |
+
{ role: "system", content: `Extract entities and relationships from this question.
|
| 82 |
+
ALLOWED ENTITY TYPES: PERSON, ORGANIZATION, LOCATION, EVENT, DATE, CONCEPT, WORK, PRODUCT, TECHNOLOGY
|
| 83 |
+
ALLOWED RELATION TYPES: WORKS_FOR, LOCATED_IN, FOUNDED_BY, PART_OF, RELATED_TO, CREATED_BY, HAPPENED_IN, MEMBER_OF, COLLABORATES_WITH, INFLUENCES
|
| 84 |
+
Return JSON:
|
| 85 |
+
{"entities": [{"name": "...", "type": "one of allowed types"}],
|
| 86 |
+
"relations": [{"source": "name", "target": "name", "type": "one of allowed types", "description": "brief"}]}` },
|
| 87 |
{ role: "user", content: query },
|
| 88 |
],
|
| 89 |
temperature: 0,
|
|
|
|
| 95 |
let relations: string[] = [];
|
| 96 |
try {
|
| 97 |
const parsed = JSON.parse(entityResp.content);
|
| 98 |
+
entities = (parsed.entities || []).map((e: { name: string; type?: string }) => e.name);
|
| 99 |
relations = (parsed.relations || []).map(
|
| 100 |
(r: { source: string; type: string; target: string; description?: string }) =>
|
| 101 |
`${r.source} -[${r.type}]-> ${r.target}: ${r.description || ""}`
|
| 102 |
);
|
| 103 |
+
} catch { /* parse errors OK */ }
|
| 104 |
|
| 105 |
+
// Step 3: Generate with structured graph context
|
| 106 |
+
const graphContext = [
|
| 107 |
+
entities.length > 0 ? `### Entities Found:\n${entities.map((e) => `- ${e}`).join("\n")}` : "",
|
| 108 |
+
relations.length > 0 ? `### Relationships:\n${relations.map((r) => `- ${r}`).join("\n")}` : "",
|
| 109 |
+
].filter(Boolean).join("\n\n");
|
| 110 |
|
| 111 |
const graphragResp = await callLLM({
|
| 112 |
provider,
|
| 113 |
model: selectedModel,
|
| 114 |
messages: [
|
| 115 |
+
{ role: "system", content: "You are a knowledgeable assistant with access to a knowledge graph. Use the structured context including entities, relationships, and passages to answer accurately. Follow relationship chains for multi-hop reasoning. Be concise." },
|
| 116 |
{ role: "user", content: `Context:\n${graphContext}\n\nQuestion: ${query}\n\nAnswer:` },
|
| 117 |
],
|
| 118 |
temperature: 0,
|
|
|
|
| 123 |
const graphragTotalCost = kwResp.costUsd + entityResp.costUsd + graphragResp.costUsd;
|
| 124 |
const graphragLatency = kwResp.latencyMs + entityResp.latencyMs + graphragResp.latencyMs;
|
| 125 |
|
| 126 |
+
// Adaptive routing (PolyG-inspired novelty)
|
| 127 |
let complexity = 0.5, queryType = "unknown", recommended = "baseline";
|
| 128 |
if (adaptiveRouting) {
|
| 129 |
const multi = entities.length > 2;
|
|
|
|
| 135 |
}
|
| 136 |
|
| 137 |
return NextResponse.json({
|
| 138 |
+
llmOnly: {
|
| 139 |
+
answer: llmOnlyResp.content,
|
| 140 |
+
tokens: llmOnlyResp.totalTokens,
|
| 141 |
+
latencyMs: llmOnlyResp.latencyMs,
|
| 142 |
+
costUsd: llmOnlyResp.costUsd,
|
| 143 |
+
},
|
| 144 |
baseline: {
|
| 145 |
answer: baselineResp.content,
|
| 146 |
tokens: baselineResp.totalTokens,
|
|
|
|
| 173 |
|
| 174 |
function getDemoResponse(query: string, provider: string, error?: string) {
|
| 175 |
return {
|
| 176 |
+
llmOnly: {
|
| 177 |
+
answer: "Scott Derrickson and Ed Wood were both American filmmakers, so yes, they shared the same nationality.",
|
| 178 |
+
tokens: 523, latencyMs: 890, costUsd: 0.000127,
|
| 179 |
+
},
|
| 180 |
baseline: {
|
| 181 |
answer: "Both Scott Derrickson and Ed Wood were American filmmakers.",
|
| 182 |
tokens: 847, latencyMs: 1240, costUsd: 0.000203,
|