@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.
- package/dist/{ILLMProvider-BfRgI1Xh.d.mts → ILLMProvider-BOJFz3Na.d.mts} +47 -1
- package/dist/{ILLMProvider-BfRgI1Xh.d.ts → ILLMProvider-BOJFz3Na.d.ts} +47 -1
- package/dist/{MultiTablePostgresProvider-YY7LPNJK.mjs → MultiTablePostgresProvider-ZLGSKTJR.mjs} +1 -1
- package/dist/chunk-ICKRMZQK.mjs +76 -0
- package/dist/{chunk-BFYLQYQU.mjs → chunk-LZVVLSDN.mjs} +192 -100
- package/dist/{chunk-R3RGUMHE.mjs → chunk-OZFBG4BA.mjs} +121 -48
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +368 -147
- package/dist/handlers/index.mjs +4 -1
- package/dist/{index-BV0z5mb6.d.mts → index-BwpcaziY.d.ts} +4 -2
- package/dist/{index-1Z4GuYBi.d.ts → index-D3V9Et2M.d.mts} +4 -2
- package/dist/index.d.mts +23 -3
- package/dist/index.d.ts +23 -3
- package/dist/index.js +1143 -790
- package/dist/index.mjs +1065 -770
- package/dist/server.d.mts +15 -25
- package/dist/server.d.ts +15 -25
- package/dist/server.js +366 -147
- package/dist/server.mjs +3 -2
- package/package.json +4 -2
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/page.tsx +12 -321
- package/src/components/AmbientBackground.tsx +29 -0
- package/src/components/ArchitectureCard.tsx +17 -0
- package/src/components/ArchitectureCardsSection.tsx +15 -0
- package/src/components/ChatWindow.tsx +32 -0
- package/src/components/CodeViewer.tsx +51 -0
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocViewer.tsx +37 -0
- package/src/components/DocumentUpload.tsx +44 -1
- package/src/components/Documentation.tsx +58 -0
- package/src/components/DynamicChart.tsx +27 -2
- package/src/components/Hero.tsx +59 -0
- package/src/components/HourglassLoader.tsx +87 -0
- package/src/components/Lifecycle.tsx +37 -0
- package/src/components/MarkdownComponents.tsx +140 -0
- package/src/components/MessageBubble.tsx +88 -1010
- package/src/components/Navbar.tsx +55 -0
- package/src/components/ObservabilityPanel.tsx +374 -0
- package/src/components/ProductCard.tsx +3 -1
- package/src/components/UIDispatcher.tsx +344 -0
- package/src/components/VisualizationRenderer.tsx +48 -26
- package/src/core/Pipeline.ts +186 -76
- package/src/handlers/index.ts +72 -12
- package/src/hooks/useRagChat.ts +19 -9
- package/src/index.ts +9 -1
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
- package/src/types/chat.ts +2 -0
- package/src/types/index.ts +52 -0
- package/src/types/props.ts +9 -1
- package/src/utils/ProductExtractor.ts +347 -0
- package/src/utils/UITransformer.ts +4 -53
- package/src/utils/synonyms.ts +78 -0
package/src/core/Pipeline.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
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 (
|
|
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 =
|
|
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
|
-
|
|
408
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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]);
|
package/src/handlers/index.ts
CHANGED
|
@@ -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 =
|
|
274
|
-
|
|
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
|
-
|
|
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
|
|
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 };
|