@retrivora-ai/rag-engine 1.8.0 → 1.8.2

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 (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -12,8 +12,13 @@ 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';
21
+ import { SchemaMapper, SchemaMap } from '../utils/SchemaMapper';
17
22
 
18
23
  // ─── LRU Embedding Cache ───────────────────────────────────────────────────────
19
24
 
@@ -59,6 +64,89 @@ class LRUEmbeddingCache {
59
64
  }
60
65
  }
61
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
+
62
150
  // ─── Pipeline ─────────────────────────────────────────────────────────────────
63
151
 
64
152
  /**
@@ -71,6 +159,7 @@ class LRUEmbeddingCache {
71
159
  * - Error recovery for transient failures
72
160
  * - Multi-tenancy support via namespacing
73
161
  * - LRU-bounded embedding cache (max 500 entries, prevents memory leaks)
162
+ * - Full observability tracing (latency, tokens, hallucination scoring)
74
163
  */
75
164
  export class Pipeline {
76
165
  private vectorDB!: BaseVectorProvider;
@@ -97,215 +186,36 @@ export class Pipeline {
97
186
  this.reranker = new Reranker();
98
187
  }
99
188
 
189
+ /**
190
+ * Expose the underlying LLM provider (set after initialize()).
191
+ * Used by the stream handler to pass to UITransformer.analyzeAndDecide().
192
+ */
193
+ getLLMProvider(): ILLMProvider | undefined {
194
+ return this.initialised ? this.llmProvider : undefined;
195
+ }
196
+
100
197
  async initialize(): Promise<void> {
101
198
  if (this.initialised) return;
102
199
 
103
- // Augment system prompt with a Universal Visualization Protocol
104
- // We use a unique marker to ensure this is only injected once but is ALWAYS present.
105
- const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
106
- const chartInstruction = `
107
-
108
- ${CHART_MARKER}
109
- ### UNIVERSAL UI PROTOCOL V7 (STRICT MODE)
110
-
111
- You are responsible for returning a SINGLE structured UI block when visualization is required.
112
-
113
- ---
114
-
115
- ## 1. INTENT CLASSIFICATION (MANDATORY)
116
-
117
- Classify the query into ONE of the following intents:
118
-
119
- - "explore" → user wants to browse items (e.g., "show me products")
120
- - "analyze" → user wants aggregation/distribution (e.g., "distribution of products")
121
- - "compare" → user wants side-by-side comparison
122
- - "detail" → user wants structured/tabular data
123
-
124
- ---
125
-
126
- ## 2. VIEW SELECTION (DYNAMIC)
200
+ const chartInstruction = `\
201
+ You are a helpful product assistant. Use the provided context to answer questions accurately.
127
202
 
128
- Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
129
- - TRENDS/GROWTH "chart" (type: "line")
130
- - DISTRIBUTIONS/RATIOS "chart" (type: "pie")
131
- - COMPARISONS "chart" (type: "bar")
132
- - PRODUCT LISTS "carousel"
133
- - RAW DATA/SPECS → "table"
134
-
135
- ⚠️ Dynamic Overrides:
136
- - If query is "show products", ALWAYS use carousel.
137
- - If data is statistical, ALWAYS use a chart.
138
- - NEVER explain why you chose a view.
139
-
140
- ---
141
-
142
- ## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
143
-
144
- \`\`\`ui
145
- {
146
- "view": "carousel" | "chart" | "table",
147
- "title": "string",
148
- "description": "string",
149
- "metadata": {
150
- "intent": "explore" | "analyze" | "compare" | "detail",
151
- "confidence": 0.0-1.0
152
- },
153
-
154
- // CHART ONLY
155
- "chart": {
156
- "type": "pie" | "bar" | "line",
157
- "xKey": "label",
158
- "yKeys": ["value"],
159
- "data": []
160
- },
161
-
162
- // TABLE ONLY
163
- "table": {
164
- "columns": [],
165
- "rows": []
166
- },
167
-
168
- // CAROUSEL ONLY
169
- "carousel": {
170
- "items": []
171
- },
172
-
173
- "insights": []
174
- }
175
- \`\`\`
203
+ ### UI STYLE RULES (CRITICAL):
204
+ - NEVER generate markdown tables. If you do, the UI will break.
205
+ - NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
206
+ - NEVER generate text-based charts or graphs.
207
+ - ONLY use plain text and bullet points.
176
208
 
177
- ---
178
-
179
- ## 4. DATA CONTRACT (CRITICAL)
180
-
181
- ### 🔹 CAROUSEL ITEM FORMAT
182
- {
183
- "id": "string",
184
- "name": "string",
185
- "brand": "string",
186
- "price": number,
187
- "image": "url",
188
- "link": "url",
189
- "inStock": boolean
190
- }
191
-
192
- ### 🔹 CHART DATA FORMAT
193
- {
194
- "label": "string",
195
- "value": number,
196
- "inStockCount": number (optional),
197
- "outOfStockCount": number (optional)
198
- }
199
-
200
- ### 🔹 TABLE FORMAT
201
- - columns MUST match row keys
202
- - rows MUST be flat objects
203
-
204
- ---
205
-
206
- ## 5. DATA RULES
207
-
208
- - NEVER hallucinate fields or data.
209
- - ONLY use data provided in the retrieved context.
210
- - STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
211
- - IF NO DATA IS FOUND: You MUST NOT generate a chart. You MUST NOT say "This example assumes" or provide hypothetical numbers. Simply state "No relevant data found in the database." and STOP.
212
- - NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
213
- - NEVER explain YOUR formatting choice.
214
- - Aggregate BEFORE rendering chart.
215
- - DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
216
-
217
- ---
218
-
219
- ## 6. SMART RULES (IMPORTANT)
220
-
221
- - If user asks BOTH:
222
- "show products AND distribution"
223
- → PRIORITIZE "explore" → carousel
224
-
225
- - If dataset size > 20:
226
- → aggregate → chart or table
227
-
228
- ---
229
-
230
- ## 7. OUTPUT RULES
231
-
232
- - ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block.
233
- - JSON must be VALID.
234
- - FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
235
- - NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
236
- - SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
237
- - TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
238
-
239
- ---
240
-
241
- ## 8. EXAMPLES
242
-
243
- User: "show me beauty products"
244
-
245
- \`\`\`ui
246
- {
247
- "view": "carousel",
248
- "title": "TITLE",
249
- "description": "DESCRIPTION",
250
- "metadata": {
251
- "intent": "explore",
252
- "confidence": 0.95
253
- },
254
- "carousel": {
255
- "items": [
256
- {
257
- "id": "ID",
258
- "name": "PRODUCT_NAME",
259
- "brand": "BRAND",
260
- "price": 0,
261
- "image": "URL",
262
- "link": "URL",
263
- "inStock": true
264
- }
265
- ]
266
- },
267
- "insights": ["INSIGHT"]
268
- }
269
- \`\`\`
270
-
271
- ---
272
-
273
-
274
-
275
- \`\`\`ui
276
- {
277
- "view": "chart",
278
- "title": "TITLE",
279
- "description": "DESCRIPTION",
280
- "metadata": {
281
- "intent": "analyze",
282
- "confidence": 0.98
283
- },
284
- "chart": {
285
- "type": "pie",
286
- "data": [
287
- { "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
288
- { "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
289
- ]
290
- },
291
- "insights": ["INSIGHT"]
292
- }
293
- \`\`\`
209
+ ### PRODUCT DISPLAY:
210
+ - When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
211
+ - The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
212
+ - Do NOT try to format product lists as tables.
294
213
  `;
295
- if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
296
- // Clean up ANY existing protocol markers (V1-V7) to avoid prompt bloat
297
- let cleanPrompt = this.config.llm.systemPrompt || '';
298
- cleanPrompt = cleanPrompt.replace(/\n\n### UNIVERSAL UI PROTOCOL[\s\S]*?(?=\n\n|$)/g, '');
299
- cleanPrompt = cleanPrompt.replace(/<!-- UI_PROTOCOL_V\d+ -->[\s\S]*?(?=\n\n|$)/g, '');
300
-
301
- this.config.llm.systemPrompt = cleanPrompt.trim() + chartInstruction;
302
- }
303
214
 
304
- console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
305
- // Resolve vector DB provider
215
+ this.config.llm.systemPrompt = chartInstruction;
216
+
306
217
  this.vectorDB = await ProviderRegistry.createVectorProvider(this.config.vectorDb);
307
218
 
308
- // Resolve LLM + embedding providers
309
219
  const { llmProvider, embeddingProvider } = await EmbeddingStrategyResolver.resolve(
310
220
  this.config.llm,
311
221
  this.config.embedding
@@ -322,7 +232,6 @@ User: "show me beauty products"
322
232
 
323
233
  await this.vectorDB.initialize();
324
234
 
325
- // Initialize Agentic Layer if configured
326
235
  if (this.config.rag?.architecture === 'agentic') {
327
236
  this.agent = new LangChainAgent(this, this.config);
328
237
  await this.agent.initialize(this.llmProvider as unknown);
@@ -333,7 +242,6 @@ User: "show me beauty products"
333
242
 
334
243
  /**
335
244
  * Ingest documents with automatic chunking, embedding, and batch upsert.
336
- * Handles retries for transient failures.
337
245
  */
338
246
  async ingest(
339
247
  documents: IngestDocument[],
@@ -357,10 +265,7 @@ User: "show me beauty products"
357
265
 
358
266
  const totalProcessed = await this.processUpserts(upsertDocs, ns);
359
267
 
360
- results.push({
361
- docId: doc.docId,
362
- chunksIngested: totalProcessed,
363
- });
268
+ results.push({ docId: doc.docId, chunksIngested: totalProcessed });
364
269
 
365
270
  if (this.graphDB && this.entityExtractor) {
366
271
  await this.processGraphIngestion(doc.docId, chunks);
@@ -374,23 +279,17 @@ User: "show me beauty products"
374
279
  return results;
375
280
  }
376
281
 
377
- /**
378
- * Step 1: Chunk the document content.
379
- */
282
+ /** Step 1: Chunk the document content. */
380
283
  private async prepareChunks(doc: IngestDocument): Promise<Chunk[]> {
381
284
  if (this.llamaIngestor) {
382
- return await this.llamaIngestor.chunk(doc.content, {
285
+ return this.llamaIngestor.chunk(doc.content, {
383
286
  docId: doc.docId,
384
287
  metadata: doc.metadata,
385
288
  chunkSize: this.config.rag?.chunkSize,
386
289
  chunkOverlap: this.config.rag?.chunkOverlap,
387
290
  });
388
291
  }
389
-
390
- return this.chunker.chunk(doc.content, {
391
- docId: doc.docId,
392
- metadata: doc.metadata,
393
- });
292
+ return this.chunker.chunk(doc.content, { docId: doc.docId, metadata: doc.metadata });
394
293
  }
395
294
 
396
295
  /**
@@ -410,7 +309,6 @@ User: "show me beauty products"
410
309
  embedBatchOptions
411
310
  );
412
311
 
413
- // mapWithRetry filters out undefined on failure; guard against mismatch explicitly
414
312
  if (vectors.length !== chunks.length) {
415
313
  throw new Error(
416
314
  `[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. ` +
@@ -421,9 +319,7 @@ User: "show me beauty products"
421
319
  return vectors;
422
320
  }
423
321
 
424
- /**
425
- * Step 3: Upsert chunks to vector database with retry logic.
426
- */
322
+ /** Step 3: Upsert chunks to vector database with retry logic. */
427
323
  private async processUpserts(upsertDocs: UpsertDocument[], namespace: string): Promise<number> {
428
324
  const upsertBatchOptions: BatchOptions = {
429
325
  batchSize: 100,
@@ -444,14 +340,10 @@ User: "show me beauty products"
444
340
  return upsertResult.totalProcessed;
445
341
  }
446
342
 
447
- /**
448
- * Step 4: Optional graph-based entity extraction and ingestion.
449
- */
343
+ /** Step 4: Optional graph-based entity extraction and ingestion. */
450
344
  private async processGraphIngestion(docId: string | number, chunks: Chunk[]): Promise<void> {
451
- console.log(`[Pipeline] Extracting entities for doc ${docId} (${chunks.length} chunks)...`);
452
-
453
345
  const extractionOptions: BatchOptions = {
454
- batchSize: 2, // Low concurrency for LLM extraction
346
+ batchSize: 2,
455
347
  maxRetries: 1,
456
348
  initialDelayMs: 500,
457
349
  };
@@ -465,7 +357,7 @@ User: "show me beauty products"
465
357
  if (nodes.length > 0) await this.graphDB!.addNodes(nodes);
466
358
  if (edges.length > 0) await this.graphDB!.addEdges(edges);
467
359
  } catch (err) {
468
- console.warn(`[Pipeline] Entity extraction failed for chunk:`, err);
360
+ console.warn(`[Pipeline] Entity extraction failed for chunk in doc ${docId}:`, err);
469
361
  }
470
362
  }
471
363
  },
@@ -477,7 +369,6 @@ User: "show me beauty products"
477
369
  await this.initialize();
478
370
 
479
371
  if (this.config.rag?.architecture === 'agentic' && this.agent) {
480
- console.log('[Pipeline] 🤖 Executing in Agentic Mode...');
481
372
  const agentReply = await this.agent.run(question, history);
482
373
  return { reply: agentReply, sources: [] };
483
374
  }
@@ -486,58 +377,68 @@ User: "show me beauty products"
486
377
  let reply = '';
487
378
  let sources: VectorMatch[] = [];
488
379
  let graphData: GraphSearchResult | undefined;
380
+ let uiTransformation: unknown;
381
+ let trace: ObservabilityTrace | undefined;
489
382
 
490
- let uiTransformation: any;
491
383
  for await (const chunk of stream) {
492
384
  if (typeof chunk === 'string') {
493
385
  reply += chunk;
494
386
  } else if (typeof chunk === 'object' && chunk !== null) {
495
- if ('sources' in chunk) sources = chunk.sources;
496
- if ('graphData' in chunk) graphData = chunk.graphData;
497
- 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;
498
391
  }
499
392
  }
500
393
 
501
- return { reply, sources, graphData, ui_transformation: uiTransformation };
394
+ return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
502
395
  }
503
396
 
504
397
  /**
505
398
  * High-performance streaming RAG flow.
506
- * 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.
507
400
  */
508
401
  async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
509
402
  await this.initialize();
510
403
  const ns = namespace ?? this.config.projectId;
511
404
  const topK = this.config.rag?.topK ?? 5;
512
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();
513
408
 
514
409
  try {
515
410
  let searchQuery = question;
411
+ let rewrittenQuery: string | undefined;
516
412
 
517
413
  // 1. Query Pre-processing
518
414
  if (this.config.rag?.useQueryTransformation) {
519
415
  searchQuery = await this.rewriteQuery(question, history);
416
+ rewrittenQuery = searchQuery !== question ? searchQuery : undefined;
520
417
  }
521
418
 
522
- // 2. Parallel Retrieval
419
+ // 2. Parallel Retrieval (with latency tracking)
523
420
  const hints = QueryProcessor.extractQueryFieldHints(question, this.config.rag?.filterableFields);
524
421
  const filter = QueryProcessor.buildQueryFilter(question, hints);
525
422
 
526
- console.log(`[Pipeline] 🧩 Extracted filters:`, JSON.stringify(filter));
527
-
423
+ const embedStart = performance.now();
528
424
  const { sources: rawSources, graphData } = await this.retrieve(searchQuery, {
529
425
  namespace: ns,
530
426
  topK: topK * 2,
531
427
  filter
532
428
  });
429
+ const retrieveEnd = performance.now();
430
+ const embedMs = retrieveEnd - embedStart; // embed + retrieve combined (cache may make embed ~0)
431
+ const retrieveMs = retrieveEnd - embedStart;
533
432
 
534
433
  // 3. Reranking
434
+ const rerankStart = performance.now();
535
435
  let sources = rawSources.filter((m) => m.score >= scoreThreshold);
536
436
  if (this.config.rag?.useReranking) {
537
437
  sources = await this.reranker.rerank(sources, question, topK);
538
438
  } else {
539
439
  sources = sources.slice(0, topK);
540
440
  }
441
+ const rerankMs = performance.now() - rerankStart;
541
442
 
542
443
  // 4. Context Augmentation
543
444
  let context = sources.length
@@ -551,8 +452,24 @@ User: "show me beauty products"
551
452
  context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
552
453
  }
553
454
 
554
- // 5. Generation (Streaming)
555
- const messages: ChatMessage[] = [...history, { role: 'user', content: question }];
455
+ // 4.5 Schema Training (non-blocking)
456
+ const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
457
+ const trainingPromise = allMetadataKeys.length > 0
458
+ ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys)
459
+ : Promise.resolve(undefined);
460
+
461
+ // 5. Generation (Streaming) — capture the full reply for observability
462
+ const restrictionSuffix = '\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)';
463
+ const hardenedHistory = [...history];
464
+ const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
465
+ const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
466
+
467
+ // Build prompt strings for tracing
468
+ const systemPrompt = this.config.llm.systemPrompt ?? '';
469
+ const userPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
470
+
471
+ let fullReply = '';
472
+ const generateStart = performance.now();
556
473
 
557
474
  if (this.llmProvider.chatStream) {
558
475
  const stream = this.llmProvider.chatStream(messages, context);
@@ -560,25 +477,73 @@ User: "show me beauty products"
560
477
  throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
561
478
  }
562
479
  for await (const chunk of stream) {
480
+ fullReply += chunk;
563
481
  yield chunk;
564
482
  }
565
483
  } else {
566
484
  const reply = await this.llmProvider.chat(messages, context);
485
+ fullReply = reply;
567
486
  yield reply;
568
487
  }
569
488
 
570
- // 6. Automated UI Fallback (UITransformer)
571
- // This provides a secondary, rule-based visualization frame in case the LLM
572
- // fails to adhere to the strict protocol or outputs a text fallback.
573
- const uiTransformation = UITransformer.transform(question, sources);
574
-
575
- // Yield retrieval metadata and automated UI transformation at the end
576
- yield {
577
- reply: '',
578
- sources,
579
- graphData,
580
- ui_transformation: uiTransformation
581
- } as ChatResponse;
489
+ const generateMs = performance.now() - generateStart;
490
+ const totalMs = performance.now() - requestStart;
491
+
492
+ const latency: LatencyBreakdown = {
493
+ embedMs: Math.round(embedMs),
494
+ retrieveMs: Math.round(retrieveMs),
495
+ rerankMs: this.config.rag?.useReranking ? Math.round(rerankMs) : undefined,
496
+ generateMs: Math.round(generateMs),
497
+ totalMs: Math.round(totalMs),
498
+ };
499
+
500
+ // 6. Token estimation
501
+ const promptText = systemPrompt + '\n' + context + '\n' + userPrompt;
502
+ const promptTokens = estimateTokens(promptText);
503
+ const completionTokens = estimateTokens(fullReply);
504
+ const tokens: TokenUsage = {
505
+ promptTokens,
506
+ completionTokens,
507
+ totalTokens: promptTokens + completionTokens,
508
+ estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
509
+ };
510
+
511
+ // 7. Hallucination scoring (async, non-blocking on generation yield)
512
+ const hallucinationResult = await scoreHallucination(this.llmProvider, fullReply, context).catch(() => undefined);
513
+
514
+ // 8. Automated UI transformation
515
+ const trainedSchema = await trainingPromise;
516
+ const uiTransformation = await this.generateUiTransformation(question, sources, context, trainedSchema);
517
+
518
+ // 9. Build observability trace
519
+ const trace: ObservabilityTrace = {
520
+ requestId,
521
+ query: question,
522
+ rewrittenQuery,
523
+ systemPrompt,
524
+ userPrompt: question + restrictionSuffix,
525
+ chunks: sources.map((s) => ({
526
+ id: s.id,
527
+ score: s.score,
528
+ content: s.content,
529
+ metadata: s.metadata ?? {},
530
+ namespace: ns,
531
+ } satisfies RetrievedChunk)),
532
+ latency,
533
+ tokens,
534
+ hallucinationScore: hallucinationResult?.score,
535
+ hallucinationReason: hallucinationResult?.reason,
536
+ timestamp: new Date().toISOString(),
537
+ };
538
+
539
+ // Yield metadata + trace + UI transformation
540
+ yield {
541
+ reply: '',
542
+ sources,
543
+ graphData,
544
+ ui_transformation: uiTransformation,
545
+ trace,
546
+ } as ChatResponse & { trace: ObservabilityTrace };
582
547
 
583
548
  } catch (error) {
584
549
  throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -589,6 +554,24 @@ User: "show me beauty products"
589
554
  * Universal retrieval method combining all enabled providers.
590
555
  * Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
591
556
  */
557
+ private async generateUiTransformation(
558
+ question: string,
559
+ sources: VectorMatch[],
560
+ _context: string,
561
+ trainedSchema?: SchemaMap
562
+ ): Promise<unknown> {
563
+ if (!sources || sources.length === 0) {
564
+ return UITransformer.transform(question, sources, this.config, trainedSchema);
565
+ }
566
+
567
+ try {
568
+ return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
569
+ } catch (err) {
570
+ console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
571
+ return UITransformer.transform(question, sources, this.config, trainedSchema);
572
+ }
573
+ }
574
+
592
575
  async retrieve(query: string, options: { namespace?: string; topK?: number; filter?: Record<string, unknown> }): Promise<RetrievalResult> {
593
576
  const ns = options.namespace ?? this.config.projectId;
594
577
  const topK = options.topK ?? 5;
@@ -613,12 +596,9 @@ User: "show me beauty products"
613
596
  return { sources, graphData };
614
597
  }
615
598
 
616
- /**
617
- * Rewrite the user query for better retrieval performance.
618
- */
599
+ /** Rewrite the user query for better retrieval performance. */
619
600
  private async rewriteQuery(question: string, history: ChatMessage[]): Promise<string> {
620
- const prompt = `
621
- Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
601
+ const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
622
602
  Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
623
603
 
624
604
  History:
@@ -639,29 +619,19 @@ Optimized Search Query:`;
639
619
  return rewrite.trim() || question;
640
620
  }
641
621
 
642
- /**
643
- * Generate 3-5 short, relevant questions based on the vector database content.
644
- */
622
+ /** Generate 3-5 short, relevant questions based on the vector database content. */
645
623
  async getSuggestions(query: string, namespace?: string): Promise<string[]> {
646
- if (!query || query.trim().length < 3) {
647
- return [];
648
- }
624
+ if (!query || query.trim().length < 3) return [];
649
625
 
650
626
  await this.initialize();
651
627
  const ns = namespace ?? this.config.projectId;
652
628
 
653
629
  try {
654
- // 1. Retrieve relevant context (top 3 matches)
655
630
  const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
631
+ if (sources.length === 0) return [];
656
632
 
657
- if (sources.length === 0) {
658
- return [];
659
- }
660
-
661
- // 2. Generate suggestions using LLM
662
633
  const context = sources.map((s) => s.content).join('\n\n---\n\n');
663
- const prompt = `
664
- Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
634
+ const prompt = `Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
665
635
  Focus on questions that can be answered by the context.
666
636
  Keep each question under 10 words and make them very specific to the content.
667
637
  Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
@@ -679,7 +649,6 @@ Suggestions:`;
679
649
  ''
680
650
  );
681
651
 
682
- // Simple parsing of JSON array from LLM response
683
652
  const match = response.match(/\[[\s\S]*\]/);
684
653
  if (match) {
685
654
  const suggestions = JSON.parse(match[0]);