@retrivora-ai/rag-engine 1.8.0 → 1.8.1
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-BfRgI1Xh.d.mts} +58 -1
- package/dist/{index-DPsQodME.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +58 -1
- package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
- package/dist/{chunk-PV3MFHWU.mjs → chunk-BFYLQYQU.mjs} +808 -439
- package/dist/chunk-R3RGUMHE.mjs +218 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +995 -603
- package/dist/handlers/index.mjs +1 -1
- package/dist/{index-Bb2yEopi.d.mts → index-1Z4GuYBi.d.ts} +7 -1
- package/dist/{index-CkbTzj9J.d.ts → index-BV0z5mb6.d.mts} +7 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +491 -316
- package/dist/index.mjs +411 -217
- package/dist/server.d.mts +35 -5
- package/dist/server.d.ts +35 -5
- package/dist/server.js +1146 -777
- package/dist/server.mjs +163 -176
- package/package.json +4 -2
- package/src/app/page.tsx +1 -2
- package/src/components/MessageBubble.tsx +211 -69
- package/src/components/ProductCard.tsx +2 -2
- package/src/components/VisualizationRenderer.tsx +347 -247
- package/src/config/RagConfig.ts +5 -0
- package/src/config/serverConfig.ts +3 -1
- package/src/core/Pipeline.ts +65 -206
- package/src/core/ProviderRegistry.ts +2 -2
- package/src/core/VectorPlugin.ts +9 -0
- package/src/handlers/index.ts +23 -7
- package/src/hooks/useRagChat.ts +2 -2
- 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/types/chat.ts +6 -0
- package/src/types/index.ts +80 -0
- package/src/utils/SchemaMapper.ts +129 -0
- package/src/utils/UITransformer.ts +491 -181
- 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/config/RagConfig.ts
CHANGED
|
@@ -182,6 +182,11 @@ export interface RAGConfig {
|
|
|
182
182
|
* Used to dynamically extract hints from natural language queries.
|
|
183
183
|
*/
|
|
184
184
|
filterableFields?: string[];
|
|
185
|
+
/** Optional mapping from database metadata fields to standard UI properties.
|
|
186
|
+
* Useful for databases with custom schemas.
|
|
187
|
+
* e.g., { "name": "ProductName", "price": "SalesPrice", "image": "ThumbnailUrl" }
|
|
188
|
+
*/
|
|
189
|
+
uiMapping?: Record<string, string>;
|
|
185
190
|
}
|
|
186
191
|
|
|
187
192
|
// ---------------------------------------------------------------------------
|
|
@@ -76,7 +76,9 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
76
76
|
vectorDbOptions.apiKey = readString(env, 'PINECONE_API_KEY') ?? (base?.vectorDb?.options?.apiKey as string) ?? '';
|
|
77
77
|
vectorDbOptions.indexName = readString(env, 'PINECONE_INDEX') ?? (base?.vectorDb?.indexName as string) ?? (base?.vectorDb?.options?.indexName as string);
|
|
78
78
|
} else if (vectorProvider === 'pgvector' || vectorProvider === 'postgresql') {
|
|
79
|
-
vectorDbOptions.connectionString = readString(env, 'PGVECTOR_CONNECTION_STRING') ?? (base?.vectorDb?.options?.connectionString as string) ?? '';
|
|
79
|
+
vectorDbOptions.connectionString = readString(env, 'PGVECTOR_CONNECTION_STRING') ?? readString(env, 'POSTGRES_URL') ?? (base?.vectorDb?.options?.connectionString as string) ?? '';
|
|
80
|
+
vectorDbOptions.tables = (readString(env, 'VECTOR_DB_TABLES') ?? readString(env, 'POSTGRES_TABLES'))?.split(',').map(t => t.trim()) ?? (base?.vectorDb?.options?.tables as string[]);
|
|
81
|
+
vectorDbOptions.searchFields = readString(env, 'POSTGRES_SEARCH_FIELDS')?.split(',').map(f => f.trim()) ?? (base?.vectorDb?.options?.searchFields as string[]);
|
|
80
82
|
vectorDbOptions.dimensions = embeddingDimensions;
|
|
81
83
|
} else if (vectorProvider === 'mongodb') {
|
|
82
84
|
vectorDbOptions.uri = readString(env, 'MONGODB_URI') ?? (base?.vectorDb?.options?.uri as string) ?? '';
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
|
|
|
14
14
|
import { QueryProcessor } from './QueryProcessor';
|
|
15
15
|
import { IngestDocument, ChatResponse, VectorMatch, GraphSearchResult, RetrievalResult, UpsertDocument } from '../types';
|
|
16
16
|
import { UITransformer } from '../utils/UITransformer';
|
|
17
|
+
import { SchemaMapper, SchemaMap } from '../utils/SchemaMapper';
|
|
17
18
|
|
|
18
19
|
// ─── LRU Embedding Cache ───────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -97,209 +98,36 @@ export class Pipeline {
|
|
|
97
98
|
this.reranker = new Reranker();
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Expose the underlying LLM provider (set after initialize()).
|
|
103
|
+
* Used by the stream handler to pass to UITransformer.analyzeAndDecide().
|
|
104
|
+
*/
|
|
105
|
+
getLLMProvider(): ILLMProvider | undefined {
|
|
106
|
+
return this.initialised ? this.llmProvider : undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
100
109
|
async initialize(): Promise<void> {
|
|
101
110
|
if (this.initialised) return;
|
|
102
111
|
|
|
103
112
|
// Augment system prompt with a Universal Visualization Protocol
|
|
104
113
|
// We use a unique marker to ensure this is only injected once but is ALWAYS present.
|
|
105
|
-
const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
|
|
114
|
+
//const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
|
|
106
115
|
const chartInstruction = `
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
###
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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)
|
|
127
|
-
|
|
128
|
-
Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
|
|
129
|
-
- TRENDS/GROWTH → "chart" (type: "line")
|
|
130
|
-
- DISTRIBUTIONS/RATIOS → "chart" (type: "pie")
|
|
131
|
-
- COMPARISONS → "chart" (type: "bar")
|
|
132
|
-
- PRODUCT LISTS → "carousel"
|
|
133
|
-
- RAW DATA/SPECS → "table"
|
|
134
|
-
|
|
135
|
-
⚠️ Dynamic Overrides:
|
|
136
|
-
- If query is "show products", ALWAYS use carousel.
|
|
137
|
-
- If data is statistical, ALWAYS use a chart.
|
|
138
|
-
- NEVER explain why you chose a view.
|
|
139
|
-
|
|
140
|
-
---
|
|
141
|
-
|
|
142
|
-
## 3. RESPONSE FORMAT (STRICT JSON ONLY INSIDE BLOCK)
|
|
143
|
-
|
|
144
|
-
\`\`\`ui
|
|
145
|
-
{
|
|
146
|
-
"view": "carousel" | "chart" | "table",
|
|
147
|
-
"title": "string",
|
|
148
|
-
"description": "string",
|
|
149
|
-
"metadata": {
|
|
150
|
-
"intent": "explore" | "analyze" | "compare" | "detail",
|
|
151
|
-
"confidence": 0.0-1.0
|
|
152
|
-
},
|
|
153
|
-
|
|
154
|
-
// CHART ONLY
|
|
155
|
-
"chart": {
|
|
156
|
-
"type": "pie" | "bar" | "line",
|
|
157
|
-
"xKey": "label",
|
|
158
|
-
"yKeys": ["value"],
|
|
159
|
-
"data": []
|
|
160
|
-
},
|
|
161
|
-
|
|
162
|
-
// TABLE ONLY
|
|
163
|
-
"table": {
|
|
164
|
-
"columns": [],
|
|
165
|
-
"rows": []
|
|
166
|
-
},
|
|
167
|
-
|
|
168
|
-
// CAROUSEL ONLY
|
|
169
|
-
"carousel": {
|
|
170
|
-
"items": []
|
|
171
|
-
},
|
|
172
|
-
|
|
173
|
-
"insights": []
|
|
174
|
-
}
|
|
175
|
-
\`\`\`
|
|
176
|
-
|
|
177
|
-
---
|
|
178
|
-
|
|
179
|
-
## 4. DATA CONTRACT (CRITICAL)
|
|
180
|
-
|
|
181
|
-
### 🔹 CAROUSEL ITEM FORMAT
|
|
182
|
-
{
|
|
183
|
-
"id": "string",
|
|
184
|
-
"name": "string",
|
|
185
|
-
"brand": "string",
|
|
186
|
-
"price": number,
|
|
187
|
-
"image": "url",
|
|
188
|
-
"link": "url",
|
|
189
|
-
"inStock": boolean
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
### 🔹 CHART DATA FORMAT
|
|
193
|
-
{
|
|
194
|
-
"label": "string",
|
|
195
|
-
"value": number,
|
|
196
|
-
"inStockCount": number (optional),
|
|
197
|
-
"outOfStockCount": number (optional)
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
### 🔹 TABLE FORMAT
|
|
201
|
-
- columns MUST match row keys
|
|
202
|
-
- rows MUST be flat objects
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## 5. DATA RULES
|
|
207
|
-
|
|
208
|
-
- NEVER hallucinate fields or data.
|
|
209
|
-
- ONLY use data provided in the retrieved context.
|
|
210
|
-
- STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
|
|
211
|
-
- IF NO DATA IS FOUND: You MUST NOT generate a chart. You MUST NOT say "This example assumes" or provide hypothetical numbers. Simply state "No relevant data found in the database." and STOP.
|
|
212
|
-
- NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
|
|
213
|
-
- NEVER explain YOUR formatting choice.
|
|
214
|
-
- Aggregate BEFORE rendering chart.
|
|
215
|
-
- DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
|
|
216
|
-
|
|
217
|
-
---
|
|
218
|
-
|
|
219
|
-
## 6. SMART RULES (IMPORTANT)
|
|
220
|
-
|
|
221
|
-
- If user asks BOTH:
|
|
222
|
-
"show products AND distribution"
|
|
223
|
-
→ PRIORITIZE "explore" → carousel
|
|
224
|
-
|
|
225
|
-
- If dataset size > 20:
|
|
226
|
-
→ aggregate → chart or table
|
|
227
|
-
|
|
228
|
-
---
|
|
229
|
-
|
|
230
|
-
## 7. OUTPUT RULES
|
|
231
|
-
|
|
232
|
-
- ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block.
|
|
233
|
-
- JSON must be VALID.
|
|
234
|
-
- FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
|
|
235
|
-
- NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
|
|
236
|
-
- SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
|
|
237
|
-
- TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
|
|
238
|
-
|
|
239
|
-
---
|
|
240
|
-
|
|
241
|
-
## 8. EXAMPLES
|
|
242
|
-
|
|
243
|
-
User: "show me beauty products"
|
|
244
|
-
|
|
245
|
-
\`\`\`ui
|
|
246
|
-
{
|
|
247
|
-
"view": "carousel",
|
|
248
|
-
"title": "TITLE",
|
|
249
|
-
"description": "DESCRIPTION",
|
|
250
|
-
"metadata": {
|
|
251
|
-
"intent": "explore",
|
|
252
|
-
"confidence": 0.95
|
|
253
|
-
},
|
|
254
|
-
"carousel": {
|
|
255
|
-
"items": [
|
|
256
|
-
{
|
|
257
|
-
"id": "ID",
|
|
258
|
-
"name": "PRODUCT_NAME",
|
|
259
|
-
"brand": "BRAND",
|
|
260
|
-
"price": 0,
|
|
261
|
-
"image": "URL",
|
|
262
|
-
"link": "URL",
|
|
263
|
-
"inStock": true
|
|
264
|
-
}
|
|
265
|
-
]
|
|
266
|
-
},
|
|
267
|
-
"insights": ["INSIGHT"]
|
|
268
|
-
}
|
|
269
|
-
\`\`\`
|
|
270
|
-
|
|
271
|
-
---
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
\`\`\`ui
|
|
276
|
-
{
|
|
277
|
-
"view": "chart",
|
|
278
|
-
"title": "TITLE",
|
|
279
|
-
"description": "DESCRIPTION",
|
|
280
|
-
"metadata": {
|
|
281
|
-
"intent": "analyze",
|
|
282
|
-
"confidence": 0.98
|
|
283
|
-
},
|
|
284
|
-
"chart": {
|
|
285
|
-
"type": "pie",
|
|
286
|
-
"data": [
|
|
287
|
-
{ "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
|
|
288
|
-
{ "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
|
|
289
|
-
]
|
|
290
|
-
},
|
|
291
|
-
"insights": ["INSIGHT"]
|
|
292
|
-
}
|
|
293
|
-
\`\`\`
|
|
116
|
+
You are a helpful product assistant. Use the provided context to answer questions accurately.
|
|
117
|
+
|
|
118
|
+
### UI STYLE RULES (CRITICAL):
|
|
119
|
+
- NEVER generate markdown tables. If you do, the UI will break.
|
|
120
|
+
- NEVER generate HTML tags like <figure>, <tbody>, <tr>, etc.
|
|
121
|
+
- NEVER generate text-based charts or graphs.
|
|
122
|
+
- ONLY use plain text and bullet points.
|
|
123
|
+
|
|
124
|
+
### PRODUCT DISPLAY:
|
|
125
|
+
- When recommending products, simply list their names, prices, and features in a friendly, conversational manner.
|
|
126
|
+
- The UI will automatically detect these products and show high-quality product cards in a carousel below your message.
|
|
127
|
+
- Do NOT try to format product lists as tables.
|
|
294
128
|
`;
|
|
295
|
-
|
|
296
|
-
|
|
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
|
-
}
|
|
129
|
+
|
|
130
|
+
this.config.llm.systemPrompt = chartInstruction;
|
|
303
131
|
|
|
304
132
|
console.log(`[Pipeline] Initializing with provider: ${this.config.vectorDb.provider}...`);
|
|
305
133
|
// Resolve vector DB provider
|
|
@@ -487,7 +315,7 @@ User: "show me beauty products"
|
|
|
487
315
|
let sources: VectorMatch[] = [];
|
|
488
316
|
let graphData: GraphSearchResult | undefined;
|
|
489
317
|
|
|
490
|
-
let uiTransformation:
|
|
318
|
+
let uiTransformation: unknown;
|
|
491
319
|
for await (const chunk of stream) {
|
|
492
320
|
if (typeof chunk === 'string') {
|
|
493
321
|
reply += chunk;
|
|
@@ -551,8 +379,17 @@ User: "show me beauty products"
|
|
|
551
379
|
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
552
380
|
}
|
|
553
381
|
|
|
382
|
+
// 4.5 Schema Training (Dynamic Discovery) - Start training but don't block the stream yet
|
|
383
|
+
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
|
|
384
|
+
const trainingPromise = allMetadataKeys.length > 0
|
|
385
|
+
? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys)
|
|
386
|
+
: Promise.resolve(undefined);
|
|
387
|
+
|
|
554
388
|
// 5. Generation (Streaming)
|
|
555
|
-
const
|
|
389
|
+
const restrictionSuffix = "\n\n(IMPORTANT: Use plain text only. NEVER generate tables, HTML figures, or text charts. If listing products, use simple bullet points.)";
|
|
390
|
+
const hardenedHistory = [...history];
|
|
391
|
+
const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
|
|
392
|
+
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
556
393
|
|
|
557
394
|
if (this.llmProvider.chatStream) {
|
|
558
395
|
const stream = this.llmProvider.chatStream(messages, context);
|
|
@@ -568,16 +405,17 @@ User: "show me beauty products"
|
|
|
568
405
|
}
|
|
569
406
|
|
|
570
407
|
// 6. Automated UI Fallback (UITransformer)
|
|
571
|
-
//
|
|
572
|
-
|
|
573
|
-
|
|
408
|
+
// Wait for training to finish if it hasn't already
|
|
409
|
+
const trainedSchema = await trainingPromise;
|
|
410
|
+
|
|
411
|
+
const uiTransformation = await this.generateUiTransformation(question, sources, context, trainedSchema);
|
|
574
412
|
|
|
575
413
|
// Yield retrieval metadata and automated UI transformation at the end
|
|
576
|
-
yield {
|
|
577
|
-
reply: '',
|
|
578
|
-
sources,
|
|
579
|
-
graphData,
|
|
580
|
-
ui_transformation: uiTransformation
|
|
414
|
+
yield {
|
|
415
|
+
reply: '',
|
|
416
|
+
sources,
|
|
417
|
+
graphData,
|
|
418
|
+
ui_transformation: uiTransformation
|
|
581
419
|
} as ChatResponse;
|
|
582
420
|
|
|
583
421
|
} catch (error) {
|
|
@@ -589,6 +427,27 @@ User: "show me beauty products"
|
|
|
589
427
|
* Universal retrieval method combining all enabled providers.
|
|
590
428
|
* Uses an LRU-bounded embedding cache to avoid re-embedding the same query.
|
|
591
429
|
*/
|
|
430
|
+
private async generateUiTransformation(
|
|
431
|
+
question: string,
|
|
432
|
+
sources: VectorMatch[],
|
|
433
|
+
_context: string,
|
|
434
|
+
trainedSchema?: SchemaMap
|
|
435
|
+
): Promise<unknown> {
|
|
436
|
+
if (!sources || sources.length === 0) {
|
|
437
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
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
|
+
return await UITransformer.analyzeAndDecide(question, sources, this.llmProvider);
|
|
445
|
+
} catch (err) {
|
|
446
|
+
console.warn('[Pipeline] generateUiTransformation failed, using heuristic fallback:', err);
|
|
447
|
+
return UITransformer.transform(question, sources, this.config, trainedSchema);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
592
451
|
async retrieve(query: string, options: { namespace?: string; topK?: number; filter?: Record<string, unknown> }): Promise<RetrievalResult> {
|
|
593
452
|
const ns = options.namespace ?? this.config.projectId;
|
|
594
453
|
const topK = options.topK ?? 5;
|
|
@@ -77,8 +77,8 @@ export class ProviderRegistry {
|
|
|
77
77
|
}
|
|
78
78
|
case 'pgvector':
|
|
79
79
|
case 'postgresql': {
|
|
80
|
-
const {
|
|
81
|
-
return
|
|
80
|
+
const { MultiTablePostgresProvider } = await import('../providers/vectordb/MultiTablePostgresProvider');
|
|
81
|
+
return MultiTablePostgresProvider as unknown as VectorProviderClass;
|
|
82
82
|
}
|
|
83
83
|
case 'mongodb': {
|
|
84
84
|
const { MongoDBProvider } = await import('../providers/vectordb/MongoDBProvider');
|
package/src/core/VectorPlugin.ts
CHANGED
|
@@ -38,6 +38,15 @@ export class VectorPlugin {
|
|
|
38
38
|
return this.config;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Get the initialized LLM provider (available after the first request).
|
|
43
|
+
* Used by handlers to pass to UITransformer.analyzeAndDecide() for
|
|
44
|
+
* LLM-driven visualization decisions.
|
|
45
|
+
*/
|
|
46
|
+
getLLMProvider() {
|
|
47
|
+
return this.pipeline.getLLMProvider();
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
/**
|
|
42
51
|
* Perform pre-flight health checks on all configured providers.
|
|
43
52
|
* Useful to verify connectivity before running operations.
|
package/src/handlers/index.ts
CHANGED
|
@@ -27,7 +27,7 @@ function sseMetaFrame(meta: unknown): string {
|
|
|
27
27
|
|
|
28
28
|
/** Encode the UI transformation result as an SSE frame. */
|
|
29
29
|
function sseUIFrame(uiTransformation: unknown): string {
|
|
30
|
-
return `data: ${JSON.stringify({ type: 'ui_transformation',
|
|
30
|
+
return `data: ${JSON.stringify({ type: 'ui_transformation', data: uiTransformation })}\n\n`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/** Encode a stream error as an SSE frame. */
|
|
@@ -152,15 +152,31 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
152
152
|
// Retrieval metadata object — always the final frame
|
|
153
153
|
enqueue(sseMetaFrame(chunk));
|
|
154
154
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
// ── 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
|
+
const responseChunk = chunk as ChatResponse;
|
|
160
|
+
const sources = responseChunk?.sources || [];
|
|
161
|
+
|
|
162
|
+
if (sources.length > 0) {
|
|
158
163
|
try {
|
|
159
|
-
const
|
|
160
|
-
|
|
164
|
+
const llmProvider = plugin.getLLMProvider?.();
|
|
165
|
+
const uiTransformation = responseChunk?.ui_transformation ??
|
|
166
|
+
(llmProvider
|
|
167
|
+
? await UITransformer.analyzeAndDecide(message, sources, llmProvider)
|
|
168
|
+
: UITransformer.transform(message, sources, plugin.getConfig()));
|
|
169
|
+
|
|
170
|
+
if (uiTransformation) {
|
|
171
|
+
enqueue(sseUIFrame(uiTransformation));
|
|
172
|
+
}
|
|
161
173
|
} catch (transformError) {
|
|
162
174
|
console.warn('[createStreamHandler] UI transformation warning:', transformError);
|
|
163
|
-
//
|
|
175
|
+
// Last-resort fallback: heuristic transform
|
|
176
|
+
try {
|
|
177
|
+
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
178
|
+
if (fallback) enqueue(sseUIFrame(fallback));
|
|
179
|
+
} catch { /* silent */ }
|
|
164
180
|
}
|
|
165
181
|
}
|
|
166
182
|
}
|
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -155,7 +155,7 @@ export function useRagChat(
|
|
|
155
155
|
buffer = buffer.slice(lastBoundary + 2);
|
|
156
156
|
|
|
157
157
|
for (const frame of parseSseChunk(complete)) {
|
|
158
|
-
if (frame.type === 'text') {
|
|
158
|
+
if (frame.type === 'text' && frame.text) {
|
|
159
159
|
assistantContent += frame.text;
|
|
160
160
|
} else if (frame.type === 'metadata') {
|
|
161
161
|
sources = frame.sources ?? [];
|
|
@@ -184,7 +184,7 @@ export function useRagChat(
|
|
|
184
184
|
// Process any remaining buffered data
|
|
185
185
|
if (buffer.trim()) {
|
|
186
186
|
for (const frame of parseSseChunk(buffer)) {
|
|
187
|
-
if (frame.type === 'text') assistantContent += frame.text;
|
|
187
|
+
if (frame.type === 'text' && frame.text) assistantContent += frame.text;
|
|
188
188
|
else if (frame.type === 'metadata') sources = frame.sources ?? [];
|
|
189
189
|
}
|
|
190
190
|
}
|
package/src/llm/LLMFactory.ts
CHANGED
|
@@ -12,10 +12,50 @@ interface LLMProviderStatic {
|
|
|
12
12
|
getHealthChecker?: () => IProviderHealthChecker;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Registry for custom provider factories registered at runtime. */
|
|
16
|
+
const customProviders = new Map<string, (config: LLMConfig) => ILLMProvider>();
|
|
17
|
+
|
|
15
18
|
/**
|
|
16
19
|
* LLMFactory — instantiates the correct ILLMProvider based on llmConfig.provider.
|
|
17
20
|
*/
|
|
18
21
|
export class LLMFactory {
|
|
22
|
+
/**
|
|
23
|
+
* Register a custom LLM provider factory at runtime.
|
|
24
|
+
*
|
|
25
|
+
* Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
|
|
26
|
+
* Bedrock, etc.) without modifying this library's source code.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // In your Next.js app initialization:
|
|
30
|
+
* import { LLMFactory } from '@retrivora-ai/rag-engine/server';
|
|
31
|
+
* import { MyCustomProvider } from './providers/MyCustomProvider';
|
|
32
|
+
*
|
|
33
|
+
* LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
|
|
34
|
+
*
|
|
35
|
+
* // Then set in your .env.local:
|
|
36
|
+
* // LLM_PROVIDER=my-provider
|
|
37
|
+
*/
|
|
38
|
+
static register(name: string, factory: (config: LLMConfig) => ILLMProvider): void {
|
|
39
|
+
customProviders.set(name.toLowerCase(), factory);
|
|
40
|
+
console.log(`[LLMFactory] Registered custom provider: "${name}"`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Unregister a previously registered custom provider.
|
|
45
|
+
*/
|
|
46
|
+
static unregister(name: string): void {
|
|
47
|
+
customProviders.delete(name.toLowerCase());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* List all registered provider names (built-in + custom).
|
|
52
|
+
*/
|
|
53
|
+
static listProviders(): string[] {
|
|
54
|
+
return [
|
|
55
|
+
'openai', 'anthropic', 'ollama', 'gemini', 'rest', 'universal_rest', 'custom',
|
|
56
|
+
...Array.from(customProviders.keys()),
|
|
57
|
+
];
|
|
58
|
+
}
|
|
19
59
|
static create(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig): ILLMProvider {
|
|
20
60
|
switch (llmConfig.provider) {
|
|
21
61
|
case 'openai':
|
|
@@ -30,11 +70,23 @@ export class LLMFactory {
|
|
|
30
70
|
case 'universal_rest':
|
|
31
71
|
case 'custom':
|
|
32
72
|
return new UniversalLLMAdapter(llmConfig);
|
|
33
|
-
default:
|
|
73
|
+
default: {
|
|
74
|
+
// Check the runtime custom provider registry first
|
|
75
|
+
const providerName = String(llmConfig.provider ?? '').toLowerCase();
|
|
76
|
+
const customFactory = customProviders.get(providerName);
|
|
77
|
+
if (customFactory) {
|
|
78
|
+
return customFactory(llmConfig);
|
|
79
|
+
}
|
|
80
|
+
// Fall back to UniversalLLMAdapter if a baseUrl is provided
|
|
34
81
|
if (llmConfig.baseUrl || (llmConfig.options as Record<string, unknown>)?.baseUrl) {
|
|
35
82
|
return new UniversalLLMAdapter(llmConfig);
|
|
36
83
|
}
|
|
37
|
-
throw new Error(
|
|
84
|
+
throw new Error(
|
|
85
|
+
`[LLMFactory] Unknown provider "${llmConfig.provider}". ` +
|
|
86
|
+
`Built-in providers: ${LLMFactory.listProviders().join(', ')}. ` +
|
|
87
|
+
`Register a custom provider with LLMFactory.register().`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
38
90
|
}
|
|
39
91
|
}
|
|
40
92
|
|
|
@@ -75,13 +75,15 @@ export class AnthropicProvider implements ILLMProvider {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
78
|
-
const systemPrompt
|
|
78
|
+
const resolvedPrompt = options?.systemPrompt ??
|
|
79
79
|
this.llmConfig.systemPrompt ??
|
|
80
80
|
`You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
|
|
81
81
|
|
|
82
|
-
const system =
|
|
83
|
-
?
|
|
84
|
-
:
|
|
82
|
+
const system = resolvedPrompt.includes('{{context}}')
|
|
83
|
+
? resolvedPrompt.replace('{{context}}', context)
|
|
84
|
+
: options?.systemPrompt
|
|
85
|
+
? resolvedPrompt // per-call: context is already in the user message
|
|
86
|
+
: `${resolvedPrompt}\n\nContext:\n${context}`;
|
|
85
87
|
|
|
86
88
|
const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
|
|
87
89
|
role: m.role === 'assistant' ? 'assistant' : 'user',
|
|
@@ -100,13 +102,15 @@ export class AnthropicProvider implements ILLMProvider {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
103
|
-
const systemPrompt
|
|
105
|
+
const resolvedPrompt = options?.systemPrompt ??
|
|
104
106
|
this.llmConfig.systemPrompt ??
|
|
105
107
|
`You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
|
|
106
108
|
|
|
107
|
-
const system =
|
|
108
|
-
?
|
|
109
|
-
:
|
|
109
|
+
const system = resolvedPrompt.includes('{{context}}')
|
|
110
|
+
? resolvedPrompt.replace('{{context}}', context)
|
|
111
|
+
: options?.systemPrompt
|
|
112
|
+
? resolvedPrompt
|
|
113
|
+
: `${resolvedPrompt}\n\nContext:\n${context}`;
|
|
110
114
|
|
|
111
115
|
const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
|
|
112
116
|
role: m.role === 'assistant' ? 'assistant' : 'user',
|