@retrivora-ai/rag-engine 1.8.1 → 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 (55) 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/{MultiTablePostgresProvider-YY7LPNJK.mjs → MultiTablePostgresProvider-ZLGSKTJR.mjs} +1 -1
  4. package/dist/chunk-ICKRMZQK.mjs +76 -0
  5. package/dist/{chunk-BFYLQYQU.mjs → chunk-LZVVLSDN.mjs} +192 -100
  6. package/dist/{chunk-R3RGUMHE.mjs → chunk-OZFBG4BA.mjs} +121 -48
  7. package/dist/handlers/index.d.mts +2 -2
  8. package/dist/handlers/index.d.ts +2 -2
  9. package/dist/handlers/index.js +368 -147
  10. package/dist/handlers/index.mjs +4 -1
  11. package/dist/{index-BV0z5mb6.d.mts → index-BwpcaziY.d.ts} +4 -2
  12. package/dist/{index-1Z4GuYBi.d.ts → index-D3V9Et2M.d.mts} +4 -2
  13. package/dist/index.d.mts +23 -3
  14. package/dist/index.d.ts +23 -3
  15. package/dist/index.js +1143 -790
  16. package/dist/index.mjs +1065 -770
  17. package/dist/server.d.mts +15 -25
  18. package/dist/server.d.ts +15 -25
  19. package/dist/server.js +366 -147
  20. package/dist/server.mjs +3 -2
  21. package/package.json +4 -2
  22. package/src/app/api/upload/route.ts +4 -0
  23. package/src/app/constants.tsx +2 -2
  24. package/src/app/page.tsx +12 -321
  25. package/src/components/AmbientBackground.tsx +29 -0
  26. package/src/components/ArchitectureCard.tsx +17 -0
  27. package/src/components/ArchitectureCardsSection.tsx +15 -0
  28. package/src/components/ChatWindow.tsx +32 -0
  29. package/src/components/CodeViewer.tsx +51 -0
  30. package/src/components/ConfigProvider.tsx +1 -0
  31. package/src/components/DocViewer.tsx +37 -0
  32. package/src/components/DocumentUpload.tsx +44 -1
  33. package/src/components/Documentation.tsx +58 -0
  34. package/src/components/DynamicChart.tsx +27 -2
  35. package/src/components/Hero.tsx +59 -0
  36. package/src/components/HourglassLoader.tsx +87 -0
  37. package/src/components/Lifecycle.tsx +37 -0
  38. package/src/components/MarkdownComponents.tsx +140 -0
  39. package/src/components/MessageBubble.tsx +88 -1010
  40. package/src/components/Navbar.tsx +55 -0
  41. package/src/components/ObservabilityPanel.tsx +374 -0
  42. package/src/components/ProductCard.tsx +3 -1
  43. package/src/components/UIDispatcher.tsx +344 -0
  44. package/src/components/VisualizationRenderer.tsx +48 -26
  45. package/src/core/Pipeline.ts +186 -76
  46. package/src/handlers/index.ts +72 -12
  47. package/src/hooks/useRagChat.ts +19 -9
  48. package/src/index.ts +9 -1
  49. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  50. package/src/types/chat.ts +2 -0
  51. package/src/types/index.ts +52 -0
  52. package/src/types/props.ts +9 -1
  53. package/src/utils/ProductExtractor.ts +347 -0
  54. package/src/utils/UITransformer.ts +4 -53
  55. package/src/utils/synonyms.ts +78 -0
@@ -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,68 @@ 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) {
365
437
  sources = await this.reranker.rerank(sources, question, topK);
366
438
  } else {
367
439
  sources = sources.slice(0, topK);
368
440
  }
441
+ const rerankMs = performance.now() - rerankStart;
369
442
 
370
443
  // 4. Context Augmentation
371
444
  let context = sources.length
@@ -379,44 +452,98 @@ You are a helpful product assistant. Use the provided context to answer question
379
452
  context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
380
453
  }
381
454
 
382
- // 4.5 Schema Training (Dynamic Discovery) - Start training but don't block the stream yet
455
+ // 4.5 Schema Training (non-blocking)
383
456
  const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
384
457
  const trainingPromise = allMetadataKeys.length > 0
385
458
  ? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys)
386
459
  : Promise.resolve(undefined);
387
460
 
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.)";
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.)';
390
463
  const hardenedHistory = [...history];
391
464
  const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
392
465
  const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
393
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();
473
+
394
474
  if (this.llmProvider.chatStream) {
395
475
  const stream = this.llmProvider.chatStream(messages, context);
396
476
  if (!stream) {
397
477
  throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
398
478
  }
399
479
  for await (const chunk of stream) {
480
+ fullReply += chunk;
400
481
  yield chunk;
401
482
  }
402
483
  } else {
403
484
  const reply = await this.llmProvider.chat(messages, context);
485
+ fullReply = reply;
404
486
  yield reply;
405
487
  }
406
488
 
407
- // 6. Automated UI Fallback (UITransformer)
408
- // Wait for training to finish if it hasn't already
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
409
515
  const trainedSchema = await trainingPromise;
410
-
411
516
  const uiTransformation = await this.generateUiTransformation(question, sources, context, trainedSchema);
412
517
 
413
- // Yield retrieval metadata and automated UI transformation at the end
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
414
540
  yield {
415
541
  reply: '',
416
542
  sources,
417
543
  graphData,
418
- ui_transformation: uiTransformation
419
- } as ChatResponse;
544
+ ui_transformation: uiTransformation,
545
+ trace,
546
+ } as ChatResponse & { trace: ObservabilityTrace };
420
547
 
421
548
  } catch (error) {
422
549
  throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -438,9 +565,6 @@ You are a helpful product assistant. Use the provided context to answer question
438
565
  }
439
566
 
440
567
  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
568
  return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
445
569
  } catch (err) {
446
570
  console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
@@ -472,12 +596,9 @@ You are a helpful product assistant. Use the provided context to answer question
472
596
  return { sources, graphData };
473
597
  }
474
598
 
475
- /**
476
- * Rewrite the user query for better retrieval performance.
477
- */
599
+ /** Rewrite the user query for better retrieval performance. */
478
600
  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.
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.
481
602
  Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
482
603
 
483
604
  History:
@@ -498,29 +619,19 @@ Optimized Search Query:`;
498
619
  return rewrite.trim() || question;
499
620
  }
500
621
 
501
- /**
502
- * Generate 3-5 short, relevant questions based on the vector database content.
503
- */
622
+ /** Generate 3-5 short, relevant questions based on the vector database content. */
504
623
  async getSuggestions(query: string, namespace?: string): Promise<string[]> {
505
- if (!query || query.trim().length < 3) {
506
- return [];
507
- }
624
+ if (!query || query.trim().length < 3) return [];
508
625
 
509
626
  await this.initialize();
510
627
  const ns = namespace ?? this.config.projectId;
511
628
 
512
629
  try {
513
- // 1. Retrieve relevant context (top 3 matches)
514
630
  const { sources } = await this.retrieve(query, { namespace: ns, topK: 3 });
631
+ if (sources.length === 0) return [];
515
632
 
516
- if (sources.length === 0) {
517
- return [];
518
- }
519
-
520
- // 2. Generate suggestions using LLM
521
633
  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?
634
+ const prompt = `Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
524
635
  Focus on questions that can be answered by the context.
525
636
  Keep each question under 10 words and make them very specific to the content.
526
637
  Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
@@ -538,7 +649,6 @@ Suggestions:`;
538
649
  ''
539
650
  );
540
651
 
541
- // Simple parsing of JSON array from LLM response
542
652
  const match = response.match(/\[[\s\S]*\]/);
543
653
  if (match) {
544
654
  const suggestions = JSON.parse(match[0]);
@@ -30,6 +30,11 @@ function sseUIFrame(uiTransformation: unknown): string {
30
30
  return `data: ${JSON.stringify({ type: 'ui_transformation', data: uiTransformation })}\n\n`;
31
31
  }
32
32
 
33
+ /** Encode the observability trace as an SSE frame. */
34
+ function sseObservabilityFrame(trace: unknown): string {
35
+ return `data: ${JSON.stringify({ type: 'observability', data: trace })}\n\n`;
36
+ }
37
+
33
38
  /** Encode a stream error as an SSE frame. */
34
39
  function sseErrorFrame(message: string): string {
35
40
  return `data: ${JSON.stringify({ type: 'error', error: message })}\n\n`;
@@ -153,12 +158,14 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
153
158
  enqueue(sseMetaFrame(chunk));
154
159
 
155
160
  // ── LLM-driven visualization decision ─────────────────────────
156
- // Sends question + RAG context to the LLM and asks it to choose
157
- // the best visualization format (bar_chart, pie_chart, table…).
158
- // Falls back to keyword heuristic if the LLM call fails.
159
161
  const responseChunk = chunk as ChatResponse;
160
162
  const sources = responseChunk?.sources || [];
161
163
 
164
+ // Emit observability trace if present
165
+ if (responseChunk?.trace) {
166
+ enqueue(sseObservabilityFrame(responseChunk.trace));
167
+ }
168
+
162
169
  if (sources.length > 0) {
163
170
  try {
164
171
  const llmProvider = plugin.getLLMProvider?.();
@@ -172,7 +179,6 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
172
179
  }
173
180
  } catch (transformError) {
174
181
  console.warn('[createStreamHandler] UI transformation warning:', transformError);
175
- // Last-resort fallback: heuristic transform
176
182
  try {
177
183
  const fallback = UITransformer.transform(message, sources, plugin.getConfig());
178
184
  if (fallback) enqueue(sseUIFrame(fallback));
@@ -265,15 +271,68 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
265
271
  const formData = await req.formData();
266
272
  const files = formData.getAll('files') as File[];
267
273
  const namespace = (formData.get('namespace') as string) || undefined;
274
+ const dimensionRaw = formData.get('dimension');
275
+ const dimension = dimensionRaw ? parseInt(dimensionRaw as string, 10) : undefined;
268
276
 
269
277
  if (!files || files.length === 0) {
270
278
  return NextResponse.json({ error: 'No files provided' }, { status: 400 });
271
279
  }
272
280
 
273
- const documents = await Promise.all(
274
- files.map(async (file) => {
281
+ const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
282
+
283
+ for (const file of files) {
284
+ if (file.name.toLowerCase().endsWith('.csv') || file.type === 'text/csv') {
285
+ const text = await file.text();
286
+ const Papa = await import('papaparse');
287
+ const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
288
+
289
+ if (parsed.data && parsed.data.length > 0) {
290
+ let i = 0;
291
+ let lastRowData: Record<string, string> | null = null;
292
+
293
+ for (const row of parsed.data) {
294
+ i++;
295
+ const rowData = row as Record<string, string>;
296
+
297
+ // Dynamically determine the grouping key (typically the first column like ID or Handle)
298
+ const groupingKey = Object.keys(rowData)[0];
299
+
300
+ // Forward-fill ALL empty columns for variants belonging to the same group
301
+ if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
302
+ for (const key of Object.keys(rowData)) {
303
+ // Forward-fill if the current row lacks the field but the previous row has it
304
+ if (!rowData[key] && lastRowData[key]) {
305
+ rowData[key] = lastRowData[key];
306
+ }
307
+ }
308
+ }
309
+ // Save a copy of the filled rowData for the next iteration
310
+ lastRowData = { ...rowData };
311
+
312
+ const contentParts: string[] = [];
313
+ for (const [key, val] of Object.entries(rowData)) {
314
+ if (key && val) {
315
+ contentParts.push(`${key}: ${val}`);
316
+ }
317
+ }
318
+
319
+ documents.push({
320
+ docId: `${file.name}-row-${i}`,
321
+ content: contentParts.join(', '),
322
+ metadata: {
323
+ fileName: file.name,
324
+ fileSize: file.size,
325
+ fileType: file.type,
326
+ uploadedAt: new Date().toISOString(),
327
+ ...(dimension ? { dimension } : {}),
328
+ ...rowData
329
+ }
330
+ });
331
+ }
332
+ }
333
+ } else {
275
334
  const content = await DocumentParser.parse(file, file.name, file.type);
276
- return {
335
+ documents.push({
277
336
  docId: file.name,
278
337
  content,
279
338
  metadata: {
@@ -281,10 +340,11 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
281
340
  fileSize: file.size,
282
341
  fileType: file.type,
283
342
  uploadedAt: new Date().toISOString(),
343
+ ...(dimension ? { dimension } : {}),
284
344
  },
285
- };
286
- })
287
- );
345
+ });
346
+ }
347
+ }
288
348
 
289
349
  const results = await plugin.ingest(documents, namespace);
290
350
  return NextResponse.json({ message: 'Upload successful', results });
@@ -325,5 +385,5 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
325
385
  };
326
386
  }
327
387
 
328
- // Re-export SSE helper so host apps can use it in custom handlers
329
- export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame };
388
+ // Re-export SSE helpers so host apps can use them in custom handlers
389
+ export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame, sseObservabilityFrame };