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