@retrivora-ai/rag-engine 1.8.1 → 1.8.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/dist/{ILLMProvider-BfRgI1Xh.d.mts → ILLMProvider-BOJFz3Na.d.mts} +47 -1
  2. package/dist/{ILLMProvider-BfRgI1Xh.d.ts → ILLMProvider-BOJFz3Na.d.ts} +47 -1
  3. package/dist/handlers/index.d.mts +2 -2
  4. package/dist/handlers/index.d.ts +2 -2
  5. package/dist/handlers/index.js +509 -162
  6. package/dist/handlers/index.mjs +5849 -15
  7. package/dist/{index-1Z4GuYBi.d.ts → index-BwpcaziY.d.ts} +4 -2
  8. package/dist/{index-BV0z5mb6.d.mts → index-D3V9Et2M.d.mts} +4 -2
  9. package/dist/index.d.mts +30 -16
  10. package/dist/index.d.ts +30 -16
  11. package/dist/index.js +1999 -1210
  12. package/dist/index.mjs +2041 -1205
  13. package/dist/server.d.mts +21 -25
  14. package/dist/server.d.ts +21 -25
  15. package/dist/server.js +536 -163
  16. package/dist/server.mjs +5923 -67
  17. package/package.json +9 -7
  18. package/src/app/api/upload/route.ts +4 -0
  19. package/src/app/constants.tsx +2 -2
  20. package/src/app/globals.css +35 -11
  21. package/src/app/page.tsx +12 -321
  22. package/src/components/AmbientBackground.tsx +29 -0
  23. package/src/components/ArchitectureCard.tsx +17 -0
  24. package/src/components/ArchitectureCardsSection.tsx +15 -0
  25. package/src/components/ChatWidget.tsx +0 -1
  26. package/src/components/ChatWindow.tsx +32 -0
  27. package/src/components/CodeViewer.tsx +51 -0
  28. package/src/components/ConfigProvider.tsx +1 -0
  29. package/src/components/DocViewer.tsx +37 -0
  30. package/src/components/DocumentUpload.tsx +44 -1
  31. package/src/components/Documentation.tsx +58 -0
  32. package/src/components/DynamicChart.tsx +27 -2
  33. package/src/components/Hero.tsx +59 -0
  34. package/src/components/HourglassLoader.tsx +87 -0
  35. package/src/components/Lifecycle.tsx +37 -0
  36. package/src/components/MarkdownComponents.tsx +143 -0
  37. package/src/components/MessageBubble.tsx +91 -1012
  38. package/src/components/Navbar.tsx +55 -0
  39. package/src/components/ObservabilityPanel.tsx +374 -0
  40. package/src/components/ProductCard.tsx +3 -1
  41. package/src/components/UIDispatcher.tsx +341 -0
  42. package/src/components/VisualizationRenderer.tsx +48 -26
  43. package/src/config/ConfigBuilder.ts +38 -1
  44. package/src/core/LangChainAgent.ts +1 -4
  45. package/src/core/Pipeline.ts +209 -86
  46. package/src/core/QueryProcessor.ts +65 -0
  47. package/src/handlers/index.ts +72 -12
  48. package/src/hooks/useRagChat.ts +19 -9
  49. package/src/index.ts +9 -1
  50. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  51. package/src/rag/Reranker.ts +99 -6
  52. package/src/types/chat.ts +2 -0
  53. package/src/types/index.ts +52 -0
  54. package/src/types/props.ts +9 -1
  55. package/src/utils/ProductExtractor.ts +347 -0
  56. package/src/utils/UITransformer.ts +4 -53
  57. package/src/utils/synonyms.ts +78 -0
  58. package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
  59. package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
  60. package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
  61. package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +0 -8
  62. package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
  63. package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
  64. package/dist/RedisProvider-SR65SCKV.mjs +0 -8
  65. package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
  66. package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
  67. package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
  68. package/dist/chunk-5AJ4XHLW.mjs +0 -201
  69. package/dist/chunk-5YGUXK7Z.mjs +0 -80
  70. package/dist/chunk-BFYLQYQU.mjs +0 -3985
  71. package/dist/chunk-CFVEZTBJ.mjs +0 -102
  72. package/dist/chunk-IMP6FUCY.mjs +0 -30
  73. package/dist/chunk-LR3VMDVK.mjs +0 -157
  74. package/dist/chunk-M6JSPGAR.mjs +0 -117
  75. package/dist/chunk-PSFPZXHX.mjs +0 -245
  76. package/dist/chunk-R3RGUMHE.mjs +0 -218
  77. package/dist/chunk-U55XRW3U.mjs +0 -96
  78. package/dist/chunk-VUQJVIJT.mjs +0 -148
  79. package/dist/chunk-X4TOT24V.mjs +0 -89
  80. package/dist/chunk-YLTMFW4M.mjs +0 -49
@@ -12,7 +12,11 @@ import { ProviderRegistry } from './ProviderRegistry';
12
12
  import { BatchProcessor, BatchOptions } from './BatchProcessor';
13
13
  import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
14
14
  import { QueryProcessor } from './QueryProcessor';
15
- import { IngestDocument, ChatResponse, VectorMatch, GraphSearchResult, RetrievalResult, UpsertDocument } from '../types';
15
+ import {
16
+ IngestDocument, ChatResponse, VectorMatch, GraphSearchResult,
17
+ RetrievalResult, UpsertDocument, ObservabilityTrace, LatencyBreakdown,
18
+ TokenUsage, RetrievedChunk,
19
+ } from '../types';
16
20
  import { UITransformer } from '../utils/UITransformer';
17
21
  import { SchemaMapper, SchemaMap } from '../utils/SchemaMapper';
18
22
 
@@ -60,6 +64,89 @@ class LRUEmbeddingCache {
60
64
  }
61
65
  }
62
66
 
67
+ // ─── Token estimation helpers ──────────────────────────────────────────────────
68
+
69
+ /** Rough token count estimate when the provider doesn't expose usage data. */
70
+ function estimateTokens(text: string): number {
71
+ return Math.ceil(text.length / 4);
72
+ }
73
+
74
+ /**
75
+ * Model pricing table (cost per 1k tokens in USD).
76
+ * Used for estimating cost when providers don't return billing data.
77
+ */
78
+ const MODEL_COST_PER_1K: Record<string, { input: number; output: number }> = {
79
+ 'gpt-4o': { input: 0.0025, output: 0.010 },
80
+ 'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
81
+ 'gpt-4-turbo': { input: 0.01, output: 0.03 },
82
+ 'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
83
+ 'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
84
+ 'claude-3-haiku': { input: 0.00025, output: 0.00125 },
85
+ 'gemini-1.5-flash': { input: 0.000075, output: 0.0003 },
86
+ 'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
87
+ };
88
+
89
+ function estimateCostUsd(
90
+ promptTokens: number,
91
+ completionTokens: number,
92
+ model?: string,
93
+ ): number | undefined {
94
+ if (!model) return undefined;
95
+ const key = Object.keys(MODEL_COST_PER_1K).find((k) => model.toLowerCase().includes(k));
96
+ if (!key) return undefined;
97
+ const prices = MODEL_COST_PER_1K[key];
98
+ return (promptTokens / 1000) * prices.input + (completionTokens / 1000) * prices.output;
99
+ }
100
+
101
+ // ─── Hallucination scorer ──────────────────────────────────────────────────────
102
+
103
+ /**
104
+ * Ask the LLM to self-critique: rate how well the answer is supported by the context.
105
+ * Returns a score 0–1 and a brief reason. Silently returns undefined on failure.
106
+ */
107
+ async function scoreHallucination(
108
+ llm: ILLMProvider,
109
+ answer: string,
110
+ context: string,
111
+ ): Promise<{ score: number; reason: string } | undefined> {
112
+ const maxContextChars = 3000;
113
+ const truncatedContext = context.length > maxContextChars
114
+ ? context.slice(0, maxContextChars) + '\n...[truncated]'
115
+ : context;
116
+
117
+ const prompt = `You are an AI quality checker. Given the CONTEXT retrieved from a knowledge base and the ANSWER generated from it, rate how well-grounded the answer is.
118
+
119
+ CONTEXT:
120
+ ${truncatedContext}
121
+
122
+ ANSWER:
123
+ ${answer}
124
+
125
+ Return ONLY a valid JSON object with no markdown fences:
126
+ {"score": <float 0-1>, "reason": "<one sentence>"}
127
+
128
+ Where score 0 = fully grounded in context, score 1 = likely hallucinated or unsupported.`;
129
+
130
+ try {
131
+ const raw = await llm.chat(
132
+ [{ role: 'user', content: prompt }],
133
+ '',
134
+ { temperature: 0, maxTokens: 120 },
135
+ );
136
+
137
+ const jsonMatch = raw.match(/\{[\s\S]*\}/);
138
+ if (!jsonMatch) return undefined;
139
+
140
+ const parsed = JSON.parse(jsonMatch[0]);
141
+ if (typeof parsed.score === 'number' && typeof parsed.reason === 'string') {
142
+ return { score: Math.min(1, Math.max(0, parsed.score)), reason: parsed.reason };
143
+ }
144
+ } catch {
145
+ // Non-critical — fail silently
146
+ }
147
+ return undefined;
148
+ }
149
+
63
150
  // ─── Pipeline ─────────────────────────────────────────────────────────────────
64
151
 
65
152
  /**
@@ -72,6 +159,7 @@ class LRUEmbeddingCache {
72
159
  * - Error recovery for transient failures
73
160
  * - Multi-tenancy support via namespacing
74
161
  * - LRU-bounded embedding cache (max 500 entries, prevents memory leaks)
162
+ * - Full observability tracing (latency, tokens, hallucination scoring)
75
163
  */
76
164
  export class Pipeline {
77
165
  private vectorDB!: BaseVectorProvider;
@@ -109,10 +197,7 @@ export class Pipeline {
109
197
  async initialize(): Promise<void> {
110
198
  if (this.initialised) return;
111
199
 
112
- // Augment system prompt with a Universal Visualization Protocol
113
- // We use a unique marker to ensure this is only injected once but is ALWAYS present.
114
- //const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
115
- const chartInstruction = `
200
+ const chartInstruction = `\
116
201
  You are a helpful product assistant. Use the provided context to answer questions accurately.
117
202
 
118
203
  ### UI STYLE RULES (CRITICAL):
@@ -129,11 +214,8 @@ You are a helpful product assistant. Use the provided context to answer question
129
214
 
130
215
  this.config.llm.systemPrompt = chartInstruction;
131
216
 
132
- console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
133
- // Resolve vector DB provider
134
217
  this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
135
218
 
136
- // Resolve LLM + embedding providers
137
219
  const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
138
220
  this.config.llm,
139
221
  this.config.embedding
@@ -150,7 +232,6 @@ You are a helpful product assistant. Use the provided context to answer question
150
232
 
151
233
  await this.vectorDB.initialize();
152
234
 
153
- // Initialize Agentic Layer if configured
154
235
  if (this.config.rag?.architecture === 'agentic') {
155
236
  this.agent = new LangChainAgent(this, this.config);
156
237
  await this.agent.initialize(this.llmProvider as unknown);
@@ -161,7 +242,6 @@ You are a helpful product assistant. Use the provided context to answer question
161
242
 
162
243
  /**
163
244
  * Ingest documents with automatic chunking, embedding, and batch upsert.
164
- * Handles retries for transient failures.
165
245
  */
166
246
  async ingest(
167
247
  documents: IngestDocument[],
@@ -185,10 +265,7 @@ You are a helpful product assistant. Use the provided context to answer question
185
265
 
186
266
  const totalProcessed = await this.processUpserts(upsertDocs, ns);
187
267
 
188
- results.push({
189
- docId: doc.docId,
190
- chunksIngested: totalProcessed,
191
- });
268
+ results.push({ docId: doc.docId, chunksIngested: totalProcessed });
192
269
 
193
270
  if (this.graphDB && this.entityExtractor) {
194
271
  await this.processGraphIngestion(doc.docId, chunks);
@@ -202,23 +279,17 @@ You are a helpful product assistant. Use the provided context to answer question
202
279
  return results;
203
280
  }
204
281
 
205
- /**
206
- * Step 1: Chunk the document content.
207
- */
282
+ /** Step 1: Chunk the document content. */
208
283
  private async prepareChunks(doc: IngestDocument): Promise<Chunk[]> {
209
284
  if (this.llamaIngestor) {
210
- return await this.llamaIngestor.chunk(doc.content, {
285
+ return this.llamaIngestor.chunk(doc.content, {
211
286
  docId: doc.docId,
212
287
  metadata: doc.metadata,
213
288
  chunkSize: this.config.rag?.chunkSize,
214
289
  chunkOverlap: this.config.rag?.chunkOverlap,
215
290
  });
216
291
  }
217
-
218
- return this.chunker.chunk(doc.content, {
219
- docId: doc.docId,
220
- metadata: doc.metadata,
221
- });
292
+ return this.chunker.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata });
222
293
  }
223
294
 
224
295
  /**
@@ -238,7 +309,6 @@ You are a helpful product assistant. Use the provided context to answer question
238
309
  embedBatchOptions
239
310
  );
240
311
 
241
- // mapWithRetry filters out undefined on failure; guard against mismatch explicitly
242
312
  if (vectors.length !== chunks.length) {
243
313
  throw new Error(
244
314
  `[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. ` +
@@ -249,9 +319,7 @@ You are a helpful product assistant. Use the provided context to answer question
249
319
  return vectors;
250
320
  }
251
321
 
252
- /**
253
- * Step 3: Upsert chunks to vector database with retry logic.
254
- */
322
+ /** Step 3: Upsert chunks to vector database with retry logic. */
255
323
  private async processUpserts(upsertDocs: UpsertDocument[], namespace: string): Promise<number> {
256
324
  const upsertBatchOptions: BatchOptions = {
257
325
  batchSize: 100,
@@ -272,14 +340,10 @@ You are a helpful product assistant. Use the provided context to answer question
272
340
  return upsertResult.totalProcessed;
273
341
  }
274
342
 
275
- /**
276
- * Step 4: Optional graph-based entity extraction and ingestion.
277
- */
343
+ /** Step 4: Optional graph-based entity extraction and ingestion. */
278
344
  private async processGraphIngestion(docId: string | number, chunks: Chunk[]): Promise<void> {
279
- console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
280
-
281
345
  const extractionOptions: BatchOptions = {
282
- batchSize: 2, // Low concurrency for LLM extraction
346
+ batchSize: 2,
283
347
  maxRetries: 1,
284
348
  initialDelayMs: 500,
285
349
  };
@@ -293,7 +357,7 @@ You are a helpful product assistant. Use the provided context to answer question
293
357
  if (nodes.length > 0) await this.graphDB!.addNodes(nodes);
294
358
  if (edges.length > 0) await this.graphDB!.addEdges(edges);
295
359
  } catch (err) {
296
- console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
360
+ console.warn(`[Pipeline] Entity extraction failed for chunk in doc ${docId}:`, err);
297
361
  }
298
362
  }
299
363
  },
@@ -305,7 +369,6 @@ You are a helpful product assistant. Use the provided context to answer question
305
369
  await this.initialize();
306
370
 
307
371
  if (this.config.rag?.architecture === 'agentic' && this.agent) {
308
- console.log('[Pipeline] 🤖 Executing in Agentic Mode...');
309
372
  const agentReply = await this.agent.run(question, history);
310
373
  return { reply: agentReply, sources: [] };
311
374
  }
@@ -314,58 +377,70 @@ You are a helpful product assistant. Use the provided context to answer question
314
377
  let reply = '';
315
378
  let sources: VectorMatch[] = [];
316
379
  let graphData: GraphSearchResult | undefined;
317
-
318
380
  let uiTransformation: unknown;
381
+ let trace: ObservabilityTrace | undefined;
382
+
319
383
  for await (const chunk of stream) {
320
384
  if (typeof chunk === 'string') {
321
385
  reply += chunk;
322
386
  } else if (typeof chunk === 'object' && chunk !== null) {
323
- if ('sources' in chunk) sources = chunk.sources;
324
- if ('graphData' in chunk) graphData = chunk.graphData;
325
- if ('ui_transformation' in chunk) uiTransformation = chunk.ui_transformation;
387
+ if ('sources' in chunk) sources = (chunk as ChatResponse).sources;
388
+ if ('graphData' in chunk) graphData = (chunk as ChatResponse).graphData;
389
+ if ('ui_transformation' in chunk) uiTransformation = (chunk as ChatResponse).ui_transformation;
390
+ if ('trace' in chunk) trace = (chunk as { trace: ObservabilityTrace }).trace;
326
391
  }
327
392
  }
328
393
 
329
- return { reply, sources, graphData, ui_transformation: uiTransformation };
394
+ return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
330
395
  }
331
396
 
332
397
  /**
333
398
  * High-performance streaming RAG flow.
334
- * Yields text chunks first, then the retrieval metadata at the end.
399
+ * Yields text chunks first, then the retrieval metadata + observability trace at the end.
335
400
  */
336
401
  async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
337
402
  await this.initialize();
338
403
  const ns = namespace ?? this.config.projectId;
339
404
  const topK = this.config.rag?.topK ?? 5;
340
405
  const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.0;
406
+ const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
407
+ const requestStart = performance.now();
341
408
 
342
409
  try {
343
410
  let searchQuery = question;
411
+ let rewrittenQuery: string | undefined;
344
412
 
345
413
  // 1. Query Pre-processing
346
414
  if (this.config.rag?.useQueryTransformation) {
347
415
  searchQuery = await this.rewriteQuery(question, history);
416
+ rewrittenQuery = searchQuery !== question ? searchQuery : undefined;
348
417
  }
349
418
 
350
- // 2. Parallel Retrieval
419
+ // 2. Parallel Retrieval (with latency tracking)
351
420
  const hints = QueryProcessor.extractQueryFieldHints(question, this.config.rag?.filterableFields);
352
421
  const filter = QueryProcessor.buildQueryFilter(question, hints);
353
422
 
354
- console.log(`[Pipeline] 🧩 Extracted filters:`, JSON.stringify(filter));
355
-
423
+ const embedStart = performance.now();
356
424
  const { sources: rawSources, graphData } = await this.retrieve(searchQuery, {
357
425
  namespace: ns,
358
426
  topK: topK * 2,
359
427
  filter
360
428
  });
429
+ const retrieveEnd = performance.now();
430
+ const embedMs = retrieveEnd - embedStart; // embed + retrieve combined (cache may make embed ~0)
431
+ const retrieveMs = retrieveEnd - embedStart;
361
432
 
362
433
  // 3. Reranking
434
+ const rerankStart = performance.now();
363
435
  let sources = rawSources.filter((m) => m.score >= scoreThreshold);
364
436
  if (this.config.rag?.useReranking) {
437
+ // We do not pass llmProvider here to opt into the fast, deterministic keyword-based re-ranking in production.
438
+ // Pass this.llmProvider as the 4th argument if you want high-accuracy LLM-based re-ranking.
365
439
  sources = await this.reranker.rerank(sources, question, topK);
366
440
  } else {
367
441
  sources = sources.slice(0, topK);
368
442
  }
443
+ const rerankMs = performance.now() - rerankStart;
369
444
 
370
445
  // 4. Context Augmentation
371
446
  let context = sources.length
@@ -373,50 +448,105 @@ You are a helpful product assistant. Use the provided context to answer question
373
448
  : 'No relevant context found.';
374
449
 
375
450
  if (graphData && graphData.nodes.length > 0) {
451
+ console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
376
452
  const graphContext = graphData.nodes.map(n =>
377
453
  `Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
378
454
  ).join('\n');
379
455
  context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
380
456
  }
381
457
 
382
- // 4.5 Schema Training (Dynamic Discovery) - Start training but don't block the stream yet
458
+ // 4.5 Schema Training (non-blocking)
383
459
  const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
384
460
  const trainingPromise = allMetadataKeys.length > 0
385
461
  ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys)
386
462
  : Promise.resolve(undefined);
387
463
 
388
- // 5. Generation (Streaming)
389
- const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
464
+ // 5. Generation (Streaming) — capture the full reply for observability
465
+ const restrictionSuffix = '\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)';
390
466
  const hardenedHistory = [...history];
391
467
  const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
392
468
  const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
393
469
 
470
+ // Build prompt strings for tracing
471
+ const systemPrompt = this.config.llm.systemPrompt ?? '';
472
+ const userPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
473
+
474
+ let fullReply = '';
475
+ const generateStart = performance.now();
476
+
394
477
  if (this.llmProvider.chatStream) {
395
478
  const stream = this.llmProvider.chatStream(messages, context);
396
479
  if (!stream) {
397
480
  throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
398
481
  }
399
482
  for await (const chunk of stream) {
483
+ fullReply += chunk;
400
484
  yield chunk;
401
485
  }
402
486
  } else {
403
487
  const reply = await this.llmProvider.chat(messages, context);
488
+ fullReply = reply;
404
489
  yield reply;
405
490
  }
406
491
 
407
- // 6. Automated UI Fallback (UITransformer)
408
- // Wait for training to finish if it hasn't already
492
+ const generateMs = performance.now() - generateStart;
493
+ const totalMs = performance.now() - requestStart;
494
+
495
+ const latency: LatencyBreakdown = {
496
+ embedMs: Math.round(embedMs),
497
+ retrieveMs: Math.round(retrieveMs),
498
+ rerankMs: this.config.rag?.useReranking ? Math.round(rerankMs) : undefined,
499
+ generateMs: Math.round(generateMs),
500
+ totalMs: Math.round(totalMs),
501
+ };
502
+
503
+ // 6. Token estimation
504
+ const promptText = systemPrompt + '\n' + context + '\n' + userPrompt;
505
+ const promptTokens = estimateTokens(promptText);
506
+ const completionTokens = estimateTokens(fullReply);
507
+ const tokens: TokenUsage = {
508
+ promptTokens,
509
+ completionTokens,
510
+ totalTokens: promptTokens + completionTokens,
511
+ estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
512
+ };
513
+
514
+ // 7. Hallucination scoring (async, non-blocking on generation yield)
515
+ const hallucinationResult = await scoreHallucination(this.llmProvider, fullReply, context).catch(() => undefined);
516
+
517
+ // 8. Automated UI transformation
409
518
  const trainedSchema = await trainingPromise;
410
-
411
- const uiTransformation = await this.generateUiTransformation(question, sources, context, trainedSchema);
412
-
413
- // Yield retrieval metadata and automated UI transformation at the end
519
+ const uiTransformation = await this.generateUiTransformation(question, sources, trainedSchema);
520
+
521
+ // 9. Build observability trace
522
+ const trace: ObservabilityTrace = {
523
+ requestId,
524
+ query: question,
525
+ rewrittenQuery,
526
+ systemPrompt,
527
+ userPrompt: question + restrictionSuffix,
528
+ chunks: sources.map((s) => ({
529
+ id: s.id,
530
+ score: s.score,
531
+ content: s.content,
532
+ metadata: s.metadata ?? {},
533
+ namespace: ns,
534
+ } satisfies RetrievedChunk)),
535
+ latency,
536
+ tokens,
537
+ hallucinationScore: hallucinationResult?.score,
538
+ hallucinationReason: hallucinationResult?.reason,
539
+ timestamp: new Date().toISOString(),
540
+ };
541
+
542
+ // Yield metadata + trace + UI transformation
414
543
  yield {
415
544
  reply: '',
416
545
  sources,
417
546
  graphData,
418
- ui_transformation: uiTransformation
419
- } as ChatResponse;
547
+ ui_transformation: uiTransformation,
548
+ trace,
549
+ } as ChatResponse & { trace: ObservabilityTrace };
420
550
 
421
551
  } catch (error) {
422
552
  throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -430,7 +560,6 @@ You are a helpful product assistant. Use the provided context to answer question
430
560
  private async generateUiTransformation(
431
561
  question: string,
432
562
  sources: VectorMatch[],
433
- _context: string,
434
563
  trainedSchema?: SchemaMap
435
564
  ): Promise<unknown> {
436
565
  if (!sources || sources.length === 0) {
@@ -438,9 +567,6 @@ You are a helpful product assistant. Use the provided context to answer question
438
567
  }
439
568
 
440
569
  try {
441
- // Delegate to the centralized LLM-driven visualization decision.
442
- // UITransformer.analyzeAndDecide() internally falls back to the keyword
443
- // heuristic if the LLM call fails or returns unparseable JSON.
444
570
  return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
445
571
  } catch (err) {
446
572
  console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
@@ -455,29 +581,37 @@ You are a helpful product assistant. Use the provided context to answer question
455
581
  const cacheKey = `${ns}::${query}`;
456
582
  let queryVector = this.embeddingCache.get(cacheKey);
457
583
 
584
+ // Determine strategy
585
+ const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmProvider);
586
+ console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
587
+
458
588
  const [retrievedVector, graphData] = await Promise.all([
459
- queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: 'query' }),
460
- this.graphDB && this.config.rag?.useGraphRetrieval
589
+ // Only embed if we need vector search (strategy is 'vector' or 'both')
590
+ (strategy === 'vector' || strategy === 'both')
591
+ ? (queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: 'query' }))
592
+ : Promise.resolve([]),
593
+
594
+ // Only query graph if we need graph search (strategy is 'graph' or 'both')
595
+ (strategy === 'graph' || strategy === 'both') && this.graphDB && this.config.rag?.useGraphRetrieval
461
596
  ? this.graphDB.query(query)
462
597
  : Promise.resolve(undefined)
463
598
  ]);
464
599
 
465
- if (!queryVector) {
600
+ if ((strategy === 'vector' || strategy === 'both') && !queryVector && retrievedVector && retrievedVector.length > 0) {
466
601
  this.embeddingCache.set(cacheKey, retrievedVector);
467
602
  queryVector = retrievedVector;
468
603
  }
469
604
 
470
- const sources = await this.vectorDB.query(queryVector, topK, ns, options.filter);
605
+ const sources = (strategy === 'vector' || strategy === 'both') && queryVector && queryVector.length > 0
606
+ ? await this.vectorDB.query(queryVector, topK, ns, options.filter)
607
+ : [];
471
608
 
472
609
  return { sources, graphData };
473
610
  }
474
611
 
475
- /**
476
- * Rewrite the user query for better retrieval performance.
477
- */
612
+ /** Rewrite the user query for better retrieval performance. */
478
613
  private async rewriteQuery(question: string, history: ChatMessage[]): Promise<string> {
479
- const prompt = `
480
- Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
614
+ const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
481
615
  Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
482
616
 
483
617
  History:
@@ -498,32 +632,22 @@ Optimized Search Query:`;
498
632
  return rewrite.trim() || question;
499
633
  }
500
634
 
501
- /**
502
- * Generate 3-5 short, relevant questions based on the vector database content.
503
- */
635
+ /** Generate 3-5 short, relevant questions based on the vector database content. */
504
636
  async getSuggestions(query: string, namespace?: string): Promise<string[]> {
505
- if (!query || query.trim().length < 3) {
506
- return [];
507
- }
637
+ if (!query || query.trim().length < 3) return [];
508
638
 
509
639
  await this.initialize();
510
640
  const ns = namespace ?? this.config.projectId;
511
641
 
512
642
  try {
513
- // 1. Retrieve relevant context (top 3 matches)
514
- const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
515
-
516
- if (sources.length === 0) {
517
- return [];
518
- }
643
+ const { sources } = await this.retrieve(query, { namespace: ns, topK: 5 });
644
+ if (sources.length === 0) return [];
519
645
 
520
- // 2. Generate suggestions using LLM
521
646
  const context = sources.map((s) => s.content).join('\n\n---\n\n');
522
- const prompt = `
523
- Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
647
+ const prompt = `Based on the following snippets from a document, what are at least 5 short, relevant questions a user might ask?
524
648
  Focus on questions that can be answered by the context.
525
649
  Keep each question under 10 words and make them very specific to the content.
526
- Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
650
+ Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3", "Question 4", "Question 5"].
527
651
 
528
652
  Context:
529
653
  ${context}
@@ -538,12 +662,11 @@ Suggestions:`;
538
662
  ''
539
663
  );
540
664
 
541
- // Simple parsing of JSON array from LLM response
542
665
  const match = response.match(/\[[\s\S]*\]/);
543
666
  if (match) {
544
667
  const suggestions = JSON.parse(match[0]);
545
668
  if (Array.isArray(suggestions)) {
546
- return suggestions.map(s => String(s)).slice(0, 3);
669
+ return suggestions.map(s => String(s)).slice(0, 5);
547
670
  }
548
671
  }
549
672
  } catch (error) {
@@ -198,4 +198,69 @@ export class QueryProcessor {
198
198
 
199
199
  return filter;
200
200
  }
201
+
202
+ /**
203
+ * Determines the retrieval strategy based on the query content.
204
+ */
205
+ static async determineRetrievalStrategy(
206
+ query: string,
207
+ llm?: import('../llm/ILLMProvider').ILLMProvider,
208
+ customGraphKeywords?: string[],
209
+ customVectorKeywords?: string[]
210
+ ): Promise<'vector' | 'graph' | 'both'> {
211
+
212
+ if (llm) {
213
+ try {
214
+ const prompt = `You are a routing expert in a RAG system.
215
+ Given the user query, determine which retrieval mechanism is needed:
216
+ - 'vector': For semantic search, finding specific documents or content based on similarity.
217
+ - 'graph': For finding relationships between entities, multi-hop questions, or network structures.
218
+ - 'both': If the query requires both specific content and relationship/structural analysis.
219
+
220
+ Query: "${query}"
221
+
222
+ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
223
+
224
+ const response = await llm.chat(
225
+ [{ role: 'user', content: prompt }],
226
+ '',
227
+ { temperature: 0, maxTokens: 10 }
228
+ );
229
+
230
+ const cleanResponse = response.trim().toLowerCase();
231
+ if (cleanResponse.includes('vector')) return 'vector';
232
+ if (cleanResponse.includes('graph')) return 'graph';
233
+ if (cleanResponse.includes('both')) return 'both';
234
+ } catch (error) {
235
+ console.warn('[QueryProcessor] LLM strategy classification failed, falling back to keywords:', error);
236
+ }
237
+ }
238
+
239
+ // Fallback to keywords
240
+ const normalized = query.toLowerCase();
241
+
242
+ // Graph keywords
243
+ const graphKeywords = customGraphKeywords || [
244
+ 'relationship', 'connect', 'link', 'network', 'friend', 'colleague',
245
+ 'manager', 'hierarchy', 'multi-hop', 'shortest path', 'between',
246
+ 'related to', 'associated with', 'belongs to', 'part of'
247
+ ];
248
+
249
+ const hasGraphKeyword = graphKeywords.some(kw => normalized.includes(kw));
250
+
251
+ // Vector keywords (usually generic search or content specific)
252
+ const vectorKeywords = customVectorKeywords || [
253
+ 'find', 'search', 'tell me about', 'what is', 'how to', 'documents about'
254
+ ];
255
+
256
+ const hasVectorKeyword = vectorKeywords.some(kw => normalized.includes(kw));
257
+
258
+ if (hasGraphKeyword && !hasVectorKeyword) {
259
+ return 'graph';
260
+ } else if (hasGraphKeyword && hasVectorKeyword) {
261
+ return 'both';
262
+ }
263
+
264
+ return 'vector';
265
+ }
201
266
  }