@retrivora-ai/rag-engine 1.8.1 → 1.8.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +509 -162
- package/dist/handlers/index.mjs +5849 -15
- package/dist/{index-1Z4GuYBi.d.ts → index-BwpcaziY.d.ts} +4 -2
- package/dist/{index-BV0z5mb6.d.mts → index-D3V9Et2M.d.mts} +4 -2
- package/dist/index.d.mts +30 -16
- package/dist/index.d.ts +30 -16
- package/dist/index.js +1999 -1210
- package/dist/index.mjs +2041 -1205
- package/dist/server.d.mts +21 -25
- package/dist/server.d.ts +21 -25
- package/dist/server.js +536 -163
- package/dist/server.mjs +5923 -67
- package/package.json +9 -7
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/globals.css +35 -11
- 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/ChatWidget.tsx +0 -1
- 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 +143 -0
- package/src/components/MessageBubble.tsx +91 -1012
- 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 +341 -0
- package/src/components/VisualizationRenderer.tsx +48 -26
- package/src/config/ConfigBuilder.ts +38 -1
- package/src/core/LangChainAgent.ts +1 -4
- package/src/core/Pipeline.ts +209 -86
- package/src/core/QueryProcessor.ts +65 -0
- 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/rag/Reranker.ts +99 -6
- 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/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
- package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
- package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +0 -8
- package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
- package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
- package/dist/RedisProvider-SR65SCKV.mjs +0 -8
- package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
- package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
- package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
- package/dist/chunk-5AJ4XHLW.mjs +0 -201
- package/dist/chunk-5YGUXK7Z.mjs +0 -80
- package/dist/chunk-BFYLQYQU.mjs +0 -3985
- package/dist/chunk-CFVEZTBJ.mjs +0 -102
- package/dist/chunk-IMP6FUCY.mjs +0 -30
- package/dist/chunk-LR3VMDVK.mjs +0 -157
- package/dist/chunk-M6JSPGAR.mjs +0 -117
- package/dist/chunk-PSFPZXHX.mjs +0 -245
- package/dist/chunk-R3RGUMHE.mjs +0 -218
- package/dist/chunk-U55XRW3U.mjs +0 -96
- package/dist/chunk-VUQJVIJT.mjs +0 -148
- package/dist/chunk-X4TOT24V.mjs +0 -89
- package/dist/chunk-YLTMFW4M.mjs +0 -49
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,70 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
314
377
|
let reply = '';
|
|
315
378
|
let sources: VectorMatch[] = [];
|
|
316
379
|
let graphData: GraphSearchResult | undefined;
|
|
317
|
-
|
|
318
380
|
let uiTransformation: unknown;
|
|
381
|
+
let trace: ObservabilityTrace | undefined;
|
|
382
|
+
|
|
319
383
|
for await (const chunk of stream) {
|
|
320
384
|
if (typeof chunk === 'string') {
|
|
321
385
|
reply += chunk;
|
|
322
386
|
} else if (typeof chunk === 'object' && chunk !== null) {
|
|
323
|
-
if ('sources' in chunk) sources = chunk.sources;
|
|
324
|
-
if ('graphData' in chunk) graphData = chunk.graphData;
|
|
325
|
-
if ('ui_transformation' in chunk) uiTransformation = chunk.ui_transformation;
|
|
387
|
+
if ('sources' in chunk) sources = (chunk as ChatResponse).sources;
|
|
388
|
+
if ('graphData' in chunk) graphData = (chunk as ChatResponse).graphData;
|
|
389
|
+
if ('ui_transformation' in chunk) uiTransformation = (chunk as ChatResponse).ui_transformation;
|
|
390
|
+
if ('trace' in chunk) trace = (chunk as { trace: ObservabilityTrace }).trace;
|
|
326
391
|
}
|
|
327
392
|
}
|
|
328
393
|
|
|
329
|
-
return { reply, sources, graphData, ui_transformation: uiTransformation };
|
|
394
|
+
return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
|
|
330
395
|
}
|
|
331
396
|
|
|
332
397
|
/**
|
|
333
398
|
* High-performance streaming RAG flow.
|
|
334
|
-
* Yields text chunks first, then the retrieval metadata at the end.
|
|
399
|
+
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
335
400
|
*/
|
|
336
401
|
async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
|
|
337
402
|
await this.initialize();
|
|
338
403
|
const ns = namespace ?? this.config.projectId;
|
|
339
404
|
const topK = this.config.rag?.topK ?? 5;
|
|
340
405
|
const scoreThreshold = this.config.rag?.scoreThreshold ?? 0.0;
|
|
406
|
+
const requestId = `req_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
407
|
+
const requestStart = performance.now();
|
|
341
408
|
|
|
342
409
|
try {
|
|
343
410
|
let searchQuery = question;
|
|
411
|
+
let rewrittenQuery: string | undefined;
|
|
344
412
|
|
|
345
413
|
// 1. Query Pre-processing
|
|
346
414
|
if (this.config.rag?.useQueryTransformation) {
|
|
347
415
|
searchQuery = await this.rewriteQuery(question, history);
|
|
416
|
+
rewrittenQuery = searchQuery !== question ? searchQuery : undefined;
|
|
348
417
|
}
|
|
349
418
|
|
|
350
|
-
// 2. Parallel Retrieval
|
|
419
|
+
// 2. Parallel Retrieval (with latency tracking)
|
|
351
420
|
const hints = QueryProcessor.extractQueryFieldHints(question, this.config.rag?.filterableFields);
|
|
352
421
|
const filter = QueryProcessor.buildQueryFilter(question, hints);
|
|
353
422
|
|
|
354
|
-
|
|
355
|
-
|
|
423
|
+
const embedStart = performance.now();
|
|
356
424
|
const { sources: rawSources, graphData } = await this.retrieve(searchQuery, {
|
|
357
425
|
namespace: ns,
|
|
358
426
|
topK: topK * 2,
|
|
359
427
|
filter
|
|
360
428
|
});
|
|
429
|
+
const retrieveEnd = performance.now();
|
|
430
|
+
const embedMs = retrieveEnd - embedStart; // embed + retrieve combined (cache may make embed ~0)
|
|
431
|
+
const retrieveMs = retrieveEnd - embedStart;
|
|
361
432
|
|
|
362
433
|
// 3. Reranking
|
|
434
|
+
const rerankStart = performance.now();
|
|
363
435
|
let sources = rawSources.filter((m) => m.score >= scoreThreshold);
|
|
364
436
|
if (this.config.rag?.useReranking) {
|
|
437
|
+
// We do not pass llmProvider here to opt into the fast, deterministic keyword-based re-ranking in production.
|
|
438
|
+
// Pass this.llmProvider as the 4th argument if you want high-accuracy LLM-based re-ranking.
|
|
365
439
|
sources = await this.reranker.rerank(sources, question, topK);
|
|
366
440
|
} else {
|
|
367
441
|
sources = sources.slice(0, topK);
|
|
368
442
|
}
|
|
443
|
+
const rerankMs = performance.now() - rerankStart;
|
|
369
444
|
|
|
370
445
|
// 4. Context Augmentation
|
|
371
446
|
let context = sources.length
|
|
@@ -373,50 +448,105 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
373
448
|
: 'No relevant context found.';
|
|
374
449
|
|
|
375
450
|
if (graphData && graphData.nodes.length > 0) {
|
|
451
|
+
console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
|
|
376
452
|
const graphContext = graphData.nodes.map(n =>
|
|
377
453
|
`Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
|
|
378
454
|
).join('\n');
|
|
379
455
|
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
380
456
|
}
|
|
381
457
|
|
|
382
|
-
// 4.5 Schema Training (
|
|
458
|
+
// 4.5 Schema Training (non-blocking)
|
|
383
459
|
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
|
|
384
460
|
const trainingPromise = allMetadataKeys.length > 0
|
|
385
461
|
? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys)
|
|
386
462
|
: Promise.resolve(undefined);
|
|
387
463
|
|
|
388
|
-
// 5. Generation (Streaming)
|
|
389
|
-
const restrictionSuffix =
|
|
464
|
+
// 5. Generation (Streaming) — capture the full reply for observability
|
|
465
|
+
const restrictionSuffix = '\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)';
|
|
390
466
|
const hardenedHistory = [...history];
|
|
391
467
|
const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
|
|
392
468
|
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
393
469
|
|
|
470
|
+
// Build prompt strings for tracing
|
|
471
|
+
const systemPrompt = this.config.llm.systemPrompt ?? '';
|
|
472
|
+
const userPrompt = messages.map(m => `${m.role}: ${m.content}`).join('\n');
|
|
473
|
+
|
|
474
|
+
let fullReply = '';
|
|
475
|
+
const generateStart = performance.now();
|
|
476
|
+
|
|
394
477
|
if (this.llmProvider.chatStream) {
|
|
395
478
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
396
479
|
if (!stream) {
|
|
397
480
|
throw new Error(`[Pipeline] ${this.config.llm.provider} chatStream returned undefined`);
|
|
398
481
|
}
|
|
399
482
|
for await (const chunk of stream) {
|
|
483
|
+
fullReply += chunk;
|
|
400
484
|
yield chunk;
|
|
401
485
|
}
|
|
402
486
|
} else {
|
|
403
487
|
const reply = await this.llmProvider.chat(messages, context);
|
|
488
|
+
fullReply = reply;
|
|
404
489
|
yield reply;
|
|
405
490
|
}
|
|
406
491
|
|
|
407
|
-
|
|
408
|
-
|
|
492
|
+
const generateMs = performance.now() - generateStart;
|
|
493
|
+
const totalMs = performance.now() - requestStart;
|
|
494
|
+
|
|
495
|
+
const latency: LatencyBreakdown = {
|
|
496
|
+
embedMs: Math.round(embedMs),
|
|
497
|
+
retrieveMs: Math.round(retrieveMs),
|
|
498
|
+
rerankMs: this.config.rag?.useReranking ? Math.round(rerankMs) : undefined,
|
|
499
|
+
generateMs: Math.round(generateMs),
|
|
500
|
+
totalMs: Math.round(totalMs),
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// 6. Token estimation
|
|
504
|
+
const promptText = systemPrompt + '\n' + context + '\n' + userPrompt;
|
|
505
|
+
const promptTokens = estimateTokens(promptText);
|
|
506
|
+
const completionTokens = estimateTokens(fullReply);
|
|
507
|
+
const tokens: TokenUsage = {
|
|
508
|
+
promptTokens,
|
|
509
|
+
completionTokens,
|
|
510
|
+
totalTokens: promptTokens + completionTokens,
|
|
511
|
+
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// 7. Hallucination scoring (async, non-blocking on generation yield)
|
|
515
|
+
const hallucinationResult = await scoreHallucination(this.llmProvider, fullReply, context).catch(() => undefined);
|
|
516
|
+
|
|
517
|
+
// 8. Automated UI transformation
|
|
409
518
|
const trainedSchema = await trainingPromise;
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
519
|
+
const uiTransformation = await this.generateUiTransformation(question, sources, trainedSchema);
|
|
520
|
+
|
|
521
|
+
// 9. Build observability trace
|
|
522
|
+
const trace: ObservabilityTrace = {
|
|
523
|
+
requestId,
|
|
524
|
+
query: question,
|
|
525
|
+
rewrittenQuery,
|
|
526
|
+
systemPrompt,
|
|
527
|
+
userPrompt: question + restrictionSuffix,
|
|
528
|
+
chunks: sources.map((s) => ({
|
|
529
|
+
id: s.id,
|
|
530
|
+
score: s.score,
|
|
531
|
+
content: s.content,
|
|
532
|
+
metadata: s.metadata ?? {},
|
|
533
|
+
namespace: ns,
|
|
534
|
+
} satisfies RetrievedChunk)),
|
|
535
|
+
latency,
|
|
536
|
+
tokens,
|
|
537
|
+
hallucinationScore: hallucinationResult?.score,
|
|
538
|
+
hallucinationReason: hallucinationResult?.reason,
|
|
539
|
+
timestamp: new Date().toISOString(),
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
// Yield metadata + trace + UI transformation
|
|
414
543
|
yield {
|
|
415
544
|
reply: '',
|
|
416
545
|
sources,
|
|
417
546
|
graphData,
|
|
418
|
-
ui_transformation: uiTransformation
|
|
419
|
-
|
|
547
|
+
ui_transformation: uiTransformation,
|
|
548
|
+
trace,
|
|
549
|
+
} as ChatResponse & { trace: ObservabilityTrace };
|
|
420
550
|
|
|
421
551
|
} catch (error) {
|
|
422
552
|
throw new Error(`[Pipeline] Stream failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -430,7 +560,6 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
430
560
|
private async generateUiTransformation(
|
|
431
561
|
question: string,
|
|
432
562
|
sources: VectorMatch[],
|
|
433
|
-
_context: string,
|
|
434
563
|
trainedSchema?: SchemaMap
|
|
435
564
|
): Promise<unknown> {
|
|
436
565
|
if (!sources || sources.length === 0) {
|
|
@@ -438,9 +567,6 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
438
567
|
}
|
|
439
568
|
|
|
440
569
|
try {
|
|
441
|
-
// Delegate to the centralized LLM-driven visualization decision.
|
|
442
|
-
// UITransformer.analyzeAndDecide() internally falls back to the keyword
|
|
443
|
-
// heuristic if the LLM call fails or returns unparseable JSON.
|
|
444
570
|
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
445
571
|
} catch (err) {
|
|
446
572
|
console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
|
|
@@ -455,29 +581,37 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
455
581
|
const cacheKey = `${ns}::${query}`;
|
|
456
582
|
let queryVector = this.embeddingCache.get(cacheKey);
|
|
457
583
|
|
|
584
|
+
// Determine strategy
|
|
585
|
+
const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmProvider);
|
|
586
|
+
console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
|
|
587
|
+
|
|
458
588
|
const [retrievedVector, graphData] = await Promise.all([
|
|
459
|
-
|
|
460
|
-
|
|
589
|
+
// Only embed if we need vector search (strategy is 'vector' or 'both')
|
|
590
|
+
(strategy === 'vector' || strategy === 'both')
|
|
591
|
+
? (queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: 'query' }))
|
|
592
|
+
: Promise.resolve([]),
|
|
593
|
+
|
|
594
|
+
// Only query graph if we need graph search (strategy is 'graph' or 'both')
|
|
595
|
+
(strategy === 'graph' || strategy === 'both') && this.graphDB && this.config.rag?.useGraphRetrieval
|
|
461
596
|
? this.graphDB.query(query)
|
|
462
597
|
: Promise.resolve(undefined)
|
|
463
598
|
]);
|
|
464
599
|
|
|
465
|
-
if (!queryVector) {
|
|
600
|
+
if ((strategy === 'vector' || strategy === 'both') && !queryVector && retrievedVector && retrievedVector.length > 0) {
|
|
466
601
|
this.embeddingCache.set(cacheKey, retrievedVector);
|
|
467
602
|
queryVector = retrievedVector;
|
|
468
603
|
}
|
|
469
604
|
|
|
470
|
-
const sources =
|
|
605
|
+
const sources = (strategy === 'vector' || strategy === 'both') && queryVector && queryVector.length > 0
|
|
606
|
+
? await this.vectorDB.query(queryVector, topK, ns, options.filter)
|
|
607
|
+
: [];
|
|
471
608
|
|
|
472
609
|
return { sources, graphData };
|
|
473
610
|
}
|
|
474
611
|
|
|
475
|
-
/**
|
|
476
|
-
* Rewrite the user query for better retrieval performance.
|
|
477
|
-
*/
|
|
612
|
+
/** Rewrite the user query for better retrieval performance. */
|
|
478
613
|
private async rewriteQuery(question: string, history: ChatMessage[]): Promise<string> {
|
|
479
|
-
const prompt = `
|
|
480
|
-
Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
614
|
+
const prompt = `Given the following conversation history and a new question, rewrite the question to be a better search query for a vector database.
|
|
481
615
|
Focus on extracting the core intent and entities. Do not answer the question, just rewrite it.
|
|
482
616
|
|
|
483
617
|
History:
|
|
@@ -498,32 +632,22 @@ Optimized Search Query:`;
|
|
|
498
632
|
return rewrite.trim() || question;
|
|
499
633
|
}
|
|
500
634
|
|
|
501
|
-
/**
|
|
502
|
-
* Generate 3-5 short, relevant questions based on the vector database content.
|
|
503
|
-
*/
|
|
635
|
+
/** Generate 3-5 short, relevant questions based on the vector database content. */
|
|
504
636
|
async getSuggestions(query: string, namespace?: string): Promise<string[]> {
|
|
505
|
-
if (!query || query.trim().length < 3)
|
|
506
|
-
return [];
|
|
507
|
-
}
|
|
637
|
+
if (!query || query.trim().length < 3) return [];
|
|
508
638
|
|
|
509
639
|
await this.initialize();
|
|
510
640
|
const ns = namespace ?? this.config.projectId;
|
|
511
641
|
|
|
512
642
|
try {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
if (sources.length === 0) {
|
|
517
|
-
return [];
|
|
518
|
-
}
|
|
643
|
+
const { sources } = await this.retrieve(query, { namespace: ns, topK: 5 });
|
|
644
|
+
if (sources.length === 0) return [];
|
|
519
645
|
|
|
520
|
-
// 2. Generate suggestions using LLM
|
|
521
646
|
const context = sources.map((s) => s.content).join('\n\n---\n\n');
|
|
522
|
-
const prompt = `
|
|
523
|
-
Based on the following snippets from a document, what are 3 short, relevant questions a user might ask?
|
|
647
|
+
const prompt = `Based on the following snippets from a document, what are at least 5 short, relevant questions a user might ask?
|
|
524
648
|
Focus on questions that can be answered by the context.
|
|
525
649
|
Keep each question under 10 words and make them very specific to the content.
|
|
526
|
-
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
650
|
+
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3", "Question 4", "Question 5"].
|
|
527
651
|
|
|
528
652
|
Context:
|
|
529
653
|
${context}
|
|
@@ -538,12 +662,11 @@ Suggestions:`;
|
|
|
538
662
|
''
|
|
539
663
|
);
|
|
540
664
|
|
|
541
|
-
// Simple parsing of JSON array from LLM response
|
|
542
665
|
const match = response.match(/\[[\s\S]*\]/);
|
|
543
666
|
if (match) {
|
|
544
667
|
const suggestions = JSON.parse(match[0]);
|
|
545
668
|
if (Array.isArray(suggestions)) {
|
|
546
|
-
return suggestions.map(s => String(s)).slice(0,
|
|
669
|
+
return suggestions.map(s => String(s)).slice(0, 5);
|
|
547
670
|
}
|
|
548
671
|
}
|
|
549
672
|
} catch (error) {
|
|
@@ -198,4 +198,69 @@ export class QueryProcessor {
|
|
|
198
198
|
|
|
199
199
|
return filter;
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Determines the retrieval strategy based on the query content.
|
|
204
|
+
*/
|
|
205
|
+
static async determineRetrievalStrategy(
|
|
206
|
+
query: string,
|
|
207
|
+
llm?: import('../llm/ILLMProvider').ILLMProvider,
|
|
208
|
+
customGraphKeywords?: string[],
|
|
209
|
+
customVectorKeywords?: string[]
|
|
210
|
+
): Promise<'vector' | 'graph' | 'both'> {
|
|
211
|
+
|
|
212
|
+
if (llm) {
|
|
213
|
+
try {
|
|
214
|
+
const prompt = `You are a routing expert in a RAG system.
|
|
215
|
+
Given the user query, determine which retrieval mechanism is needed:
|
|
216
|
+
- 'vector': For semantic search, finding specific documents or content based on similarity.
|
|
217
|
+
- 'graph': For finding relationships between entities, multi-hop questions, or network structures.
|
|
218
|
+
- 'both': If the query requires both specific content and relationship/structural analysis.
|
|
219
|
+
|
|
220
|
+
Query: "${query}"
|
|
221
|
+
|
|
222
|
+
Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
|
|
223
|
+
|
|
224
|
+
const response = await llm.chat(
|
|
225
|
+
[{ role: 'user', content: prompt }],
|
|
226
|
+
'',
|
|
227
|
+
{ temperature: 0, maxTokens: 10 }
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const cleanResponse = response.trim().toLowerCase();
|
|
231
|
+
if (cleanResponse.includes('vector')) return 'vector';
|
|
232
|
+
if (cleanResponse.includes('graph')) return 'graph';
|
|
233
|
+
if (cleanResponse.includes('both')) return 'both';
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('[QueryProcessor] LLM strategy classification failed, falling back to keywords:', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fallback to keywords
|
|
240
|
+
const normalized = query.toLowerCase();
|
|
241
|
+
|
|
242
|
+
// Graph keywords
|
|
243
|
+
const graphKeywords = customGraphKeywords || [
|
|
244
|
+
'relationship', 'connect', 'link', 'network', 'friend', 'colleague',
|
|
245
|
+
'manager', 'hierarchy', 'multi-hop', 'shortest path', 'between',
|
|
246
|
+
'related to', 'associated with', 'belongs to', 'part of'
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const hasGraphKeyword = graphKeywords.some(kw => normalized.includes(kw));
|
|
250
|
+
|
|
251
|
+
// Vector keywords (usually generic search or content specific)
|
|
252
|
+
const vectorKeywords = customVectorKeywords || [
|
|
253
|
+
'find', 'search', 'tell me about', 'what is', 'how to', 'documents about'
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const hasVectorKeyword = vectorKeywords.some(kw => normalized.includes(kw));
|
|
257
|
+
|
|
258
|
+
if (hasGraphKeyword && !hasVectorKeyword) {
|
|
259
|
+
return 'graph';
|
|
260
|
+
} else if (hasGraphKeyword && hasVectorKeyword) {
|
|
261
|
+
return 'both';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 'vector';
|
|
265
|
+
}
|
|
201
266
|
}
|