@retrivora-ai/rag-engine 2.0.8 → 2.1.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/handlers/index.js +113 -75
- package/dist/handlers/index.mjs +113 -75
- package/dist/index.js +19 -12
- package/dist/index.mjs +19 -12
- package/dist/server.js +113 -75
- package/dist/server.mjs +113 -75
- package/package.json +1 -1
- package/src/components/ChatWindow.tsx +22 -22
- package/src/core/Pipeline.ts +3 -3
- package/src/llm/providers/GeminiProvider.ts +4 -4
- package/src/llm/providers/GroqProvider.ts +16 -8
- package/src/llm/providers/OpenAIProvider.ts +16 -8
- package/src/llm/providers/QwenProvider.ts +16 -8
- package/src/llm/providers/UniversalLLMAdapter.ts +56 -36
- package/src/rendering/IntentClassifier.ts +3 -3
- package/src/rendering/rules/Rule1SpecificInfoRule.ts +1 -1
- package/src/utils/ProductExtractor.ts +4 -3
- package/src/utils/UITransformer.ts +12 -0
|
@@ -99,12 +99,16 @@ export class QwenProvider implements ILLMProvider {
|
|
|
99
99
|
content: buildSystemContent(basePrompt, context),
|
|
100
100
|
};
|
|
101
101
|
|
|
102
|
+
const safeMessages = (Array.isArray(messages) ? messages : [])
|
|
103
|
+
.filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
|
|
104
|
+
.map((m) => ({
|
|
105
|
+
role: (m.role || 'user') as 'user' | 'assistant',
|
|
106
|
+
content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
|
|
107
|
+
}));
|
|
108
|
+
|
|
102
109
|
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
103
110
|
systemMessage,
|
|
104
|
-
...
|
|
105
|
-
role: m.role as 'user' | 'assistant',
|
|
106
|
-
content: m.content,
|
|
107
|
-
})),
|
|
111
|
+
...safeMessages,
|
|
108
112
|
];
|
|
109
113
|
|
|
110
114
|
const completion = await this.client.chat.completions.create({
|
|
@@ -129,12 +133,16 @@ export class QwenProvider implements ILLMProvider {
|
|
|
129
133
|
content: buildSystemContent(basePrompt, context),
|
|
130
134
|
};
|
|
131
135
|
|
|
136
|
+
const safeStreamMessages = (Array.isArray(messages) ? messages : [])
|
|
137
|
+
.filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
|
|
138
|
+
.map((m) => ({
|
|
139
|
+
role: (m.role || 'user') as 'user' | 'assistant',
|
|
140
|
+
content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
|
|
141
|
+
}));
|
|
142
|
+
|
|
132
143
|
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
133
144
|
systemMessage,
|
|
134
|
-
...
|
|
135
|
-
role: m.role as 'user' | 'assistant',
|
|
136
|
-
content: m.content,
|
|
137
|
-
})),
|
|
145
|
+
...safeStreamMessages,
|
|
138
146
|
];
|
|
139
147
|
|
|
140
148
|
const stream = await this.client.chat.completions.create({
|
|
@@ -83,9 +83,16 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
83
83
|
async chat(messages: ChatMessage[], context?: string): Promise<string> {
|
|
84
84
|
const path = this.opts.chatPath ?? '/chat/completions';
|
|
85
85
|
|
|
86
|
+
const safeMessages = (Array.isArray(messages) ? messages : [])
|
|
87
|
+
.filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
|
|
88
|
+
.map((m) => ({
|
|
89
|
+
role: m.role || 'user',
|
|
90
|
+
content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
|
|
91
|
+
}));
|
|
92
|
+
|
|
86
93
|
const formattedMessages = [
|
|
87
94
|
{ role: 'system', content: `${this.systemPrompt}\n\nContext:\n${context ?? 'None'}` },
|
|
88
|
-
...
|
|
95
|
+
...safeMessages,
|
|
89
96
|
];
|
|
90
97
|
|
|
91
98
|
let payload: unknown;
|
|
@@ -105,8 +112,17 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
105
112
|
};
|
|
106
113
|
}
|
|
107
114
|
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
// Try direct HTTP POST request to llm.retrivora.com first
|
|
116
|
+
try {
|
|
117
|
+
const { data } = await this.http.post(path, payload);
|
|
118
|
+
const extractPath = this.opts.responseExtractPath ?? 'choices[0].message.content';
|
|
119
|
+
const result = resolvePath(data, extractPath) ?? extractContent(data);
|
|
120
|
+
if (result !== undefined) {
|
|
121
|
+
return String(result);
|
|
122
|
+
}
|
|
123
|
+
} catch (httpErr: any) {
|
|
124
|
+
console.warn(`[UniversalLLMAdapter] Direct HTTP POST to ${this.baseUrl}${path} failed (${httpErr.message}). Attempting in-process gateway dispatch fallback...`);
|
|
125
|
+
|
|
110
126
|
const _g = globalThis as any;
|
|
111
127
|
let dispatch = _g.__retrivoraDispatchChat;
|
|
112
128
|
if (typeof dispatch !== 'function') {
|
|
@@ -121,25 +137,19 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
121
137
|
}
|
|
122
138
|
|
|
123
139
|
if (typeof dispatch === 'function') {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
return content;
|
|
135
|
-
}
|
|
136
|
-
throw new Error(`[UniversalLLMAdapter] In-process dispatch returned empty content for model: ${this.model}`);
|
|
137
|
-
} catch (dispatchErr) {
|
|
138
|
-
throw dispatchErr;
|
|
140
|
+
const res = await dispatch({
|
|
141
|
+
model: this.model,
|
|
142
|
+
messages: formattedMessages as any,
|
|
143
|
+
max_tokens: this.maxTokens,
|
|
144
|
+
temperature: this.temperature,
|
|
145
|
+
}, this.apiKey);
|
|
146
|
+
|
|
147
|
+
const content = extractContent(res?.response);
|
|
148
|
+
if (content !== undefined) {
|
|
149
|
+
return content;
|
|
139
150
|
}
|
|
140
|
-
} else {
|
|
141
|
-
throw new Error(`[UniversalLLMAdapter] In-process gateway dispatch not registered. Direct self-referential HTTP calls to ${this.baseUrl} are disabled on Vercel to prevent HTTP 508 Loop Detected.`);
|
|
142
151
|
}
|
|
152
|
+
throw httpErr;
|
|
143
153
|
}
|
|
144
154
|
|
|
145
155
|
const { data } = await this.http.post(path, payload);
|
|
@@ -166,9 +176,16 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
166
176
|
const extractPath = (this.opts.responseExtractPath ?? 'choices[0].message.content')
|
|
167
177
|
.replace('message.content', 'delta.content');
|
|
168
178
|
|
|
179
|
+
const safeMessages = (Array.isArray(messages) ? messages : [])
|
|
180
|
+
.filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
|
|
181
|
+
.map((m) => ({
|
|
182
|
+
role: m.role || 'user',
|
|
183
|
+
content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
|
|
184
|
+
}));
|
|
185
|
+
|
|
169
186
|
const formattedMessages = [
|
|
170
187
|
{ role: 'system', content: `${this.systemPrompt}\n\nContext:\n${context ?? 'None'}` },
|
|
171
|
-
...
|
|
188
|
+
...safeMessages,
|
|
172
189
|
];
|
|
173
190
|
|
|
174
191
|
let payload: unknown;
|
|
@@ -314,8 +331,17 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
314
331
|
};
|
|
315
332
|
}
|
|
316
333
|
|
|
317
|
-
|
|
318
|
-
|
|
334
|
+
// Try direct HTTP POST request to llm.retrivora.com first
|
|
335
|
+
try {
|
|
336
|
+
const { data } = await this.http.post(path, payload);
|
|
337
|
+
const extractPath = this.opts.embedExtractPath ?? 'data[0].embedding';
|
|
338
|
+
const vector = resolvePath(data, extractPath);
|
|
339
|
+
if (Array.isArray(vector)) {
|
|
340
|
+
return vector as number[];
|
|
341
|
+
}
|
|
342
|
+
} catch (httpErr: any) {
|
|
343
|
+
console.warn(`[UniversalLLMAdapter] Direct HTTP embedding POST to ${this.baseUrl}${path} failed (${httpErr.message}). Attempting in-process gateway dispatch fallback...`);
|
|
344
|
+
|
|
319
345
|
const _g = globalThis as any;
|
|
320
346
|
let dispatch = _g.__retrivoraDispatchEmbedding;
|
|
321
347
|
if (typeof dispatch !== 'function') {
|
|
@@ -330,22 +356,16 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
330
356
|
}
|
|
331
357
|
|
|
332
358
|
if (typeof dispatch === 'function') {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}, this.apiKey);
|
|
359
|
+
const res = await dispatch({
|
|
360
|
+
model: this.model,
|
|
361
|
+
input: text,
|
|
362
|
+
}, this.apiKey);
|
|
338
363
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
}
|
|
342
|
-
throw new Error(`[UniversalLLMAdapter] In-process embedding dispatch returned invalid vector for model: ${this.model}`);
|
|
343
|
-
} catch (dispatchErr) {
|
|
344
|
-
throw dispatchErr;
|
|
364
|
+
if (res?.data?.[0]?.embedding) {
|
|
365
|
+
return res.data[0].embedding;
|
|
345
366
|
}
|
|
346
|
-
} else {
|
|
347
|
-
throw new Error(`[UniversalLLMAdapter] In-process gateway dispatch not registered. Direct self-referential HTTP calls to ${this.baseUrl} are disabled on Vercel to prevent HTTP 508 Loop Detected.`);
|
|
348
367
|
}
|
|
368
|
+
throw httpErr;
|
|
349
369
|
}
|
|
350
370
|
|
|
351
371
|
const { data } = await this.http.post(path, payload);
|
|
@@ -126,10 +126,10 @@ export class IntentClassifier {
|
|
|
126
126
|
// ─── Intent Heuristic Checkers ─────────────────────────────────────────────
|
|
127
127
|
|
|
128
128
|
private static isInformationLookup(query: string): boolean {
|
|
129
|
-
// Exact single-fact pattern matches e.g. "What is X?", "Explain Y", "
|
|
130
|
-
if (/^(what|who|explain|define|where|when|why|how|how does|can|could|does|do|is|are|has|have|will|should|would|meaning of)\s+/i.test(query)) {
|
|
129
|
+
// Exact single-fact pattern matches e.g. "What is X?", "Explain Y", "Explore Z", "Tell me about A"
|
|
130
|
+
if (/^(what|who|explain|explore|define|where|when|why|how|how does|can|could|does|do|is|are|has|have|will|should|would|meaning of|tell me about|show details of|overview of|summarize)\s+/i.test(query)) {
|
|
131
131
|
// Exclude comparison or analytical phrasing inside "what is the trend..."
|
|
132
|
-
if (!/\b(compare|versus|vs\.?|trend|monthly|revenue|top selling|best)\b/i.test(query)) {
|
|
132
|
+
if (!/\b(compare|versus|vs\.?|trend|monthly|revenue|top 10|top selling|best|chart|graph|plot|breakdown|analytics)\b/i.test(query)) {
|
|
133
133
|
return true;
|
|
134
134
|
}
|
|
135
135
|
}
|
|
@@ -10,7 +10,7 @@ export class Rule1SpecificInfoRule implements IRenderRule {
|
|
|
10
10
|
// Check specific fact/definition/explanation indicators
|
|
11
11
|
const isFactQuery =
|
|
12
12
|
intent === 'information_lookup' ||
|
|
13
|
-
/^(what|who|explain|define|where|when|why|meaning of)\s+/i.test(q) ||
|
|
13
|
+
/^(what|who|explain|explore|define|where|when|why|meaning of|tell me about|show details of|overview of|summarize)\s+/i.test(q) ||
|
|
14
14
|
/^(what is the price of|price of|cost of)\b/i.test(q);
|
|
15
15
|
|
|
16
16
|
const isNonVisual = !/\b(compare|versus|vs\.?|trend|monthly|revenue|best|top 10|chart|table|list|grid)\b/i.test(q);
|
|
@@ -196,6 +196,7 @@ export function isLikelyUiOnlyMessage(text: string): boolean {
|
|
|
196
196
|
export function extractProductsFromSources(sources: VectorMatch[] | undefined, isUser: boolean): Product[] {
|
|
197
197
|
if (isUser || !sources) return [];
|
|
198
198
|
return sources
|
|
199
|
+
.filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'))
|
|
199
200
|
.filter(s => {
|
|
200
201
|
const m = s.metadata ?? {};
|
|
201
202
|
const keys = Object.keys(m).map(k => k.toLowerCase());
|
|
@@ -204,7 +205,7 @@ export function extractProductsFromSources(sources: VectorMatch[] | undefined, i
|
|
|
204
205
|
);
|
|
205
206
|
const hasProductIdentity = keys.some(k => ['brand', 'model'].includes(k)) &&
|
|
206
207
|
keys.some(k => ['name', 'title', 'product_name', 'product'].includes(k));
|
|
207
|
-
const hasPricePattern = /\$\s*\d+/.test(s
|
|
208
|
+
const hasPricePattern = /\$\s*\d+/.test(s?.content ?? '');
|
|
208
209
|
return hasStrongProductKey || hasProductIdentity || hasPricePattern || m.type === 'product';
|
|
209
210
|
})
|
|
210
211
|
.map(s => {
|
|
@@ -215,13 +216,13 @@ export function extractProductsFromSources(sources: VectorMatch[] | undefined, i
|
|
|
215
216
|
const description = resolveMetadataValue(m, 'description') as string;
|
|
216
217
|
|
|
217
218
|
return {
|
|
218
|
-
id: s.
|
|
219
|
+
id: s?.id ?? `prod_${Math.random().toString(36).substring(2, 8)}`,
|
|
219
220
|
name: name ?? (s?.content ?? '').split('\n')[0] ?? 'Unknown Product',
|
|
220
221
|
brand: brand,
|
|
221
222
|
price: price,
|
|
222
223
|
image: resolveImage(m),
|
|
223
224
|
link: resolveMetadataValue(m, 'link') as string,
|
|
224
|
-
description: description ? description : ''
|
|
225
|
+
description: description ? description : '',
|
|
225
226
|
};
|
|
226
227
|
});
|
|
227
228
|
}
|
|
@@ -1121,6 +1121,15 @@ RULES:
|
|
|
1121
1121
|
query: string,
|
|
1122
1122
|
): UITransformationResponse | null {
|
|
1123
1123
|
if (profile.records.length === 0) return null;
|
|
1124
|
+
|
|
1125
|
+
const q = query.toLowerCase().trim();
|
|
1126
|
+
// Do not automatically force charts unless query contains visual or analytical intent
|
|
1127
|
+
const isExplicitVisualOrAnalytic = /\b(chart|graph|plot|visualize|trend|monthly|revenue|top\s*\d+|breakdown|analytics|compare|versus|vs\.?|histogram|pie|bar|line|distribution|percentage|share|summary of metrics)\b/i.test(q);
|
|
1128
|
+
|
|
1129
|
+
if (!isExplicitVisualOrAnalytic) {
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1124
1133
|
if (profile.dateFields.length > 0 && profile.numericFields.length > 0) {
|
|
1125
1134
|
return this.transformToLineChart(profile);
|
|
1126
1135
|
}
|
|
@@ -1265,6 +1274,7 @@ RULES:
|
|
|
1265
1274
|
}
|
|
1266
1275
|
|
|
1267
1276
|
private static getProductCategory(item: VectorMatch): string | null {
|
|
1277
|
+
if (!item) return null;
|
|
1268
1278
|
const meta = item.metadata || {};
|
|
1269
1279
|
const metadataCategory = resolveMetadataValue(meta, 'category');
|
|
1270
1280
|
if (this.isUsableCategory(metadataCategory)) return String(metadataCategory).trim();
|
|
@@ -1401,6 +1411,7 @@ RULES:
|
|
|
1401
1411
|
};
|
|
1402
1412
|
|
|
1403
1413
|
data.forEach(item => {
|
|
1414
|
+
if (!item) return;
|
|
1404
1415
|
Object.keys(item.metadata || {}).forEach(addField);
|
|
1405
1416
|
for (const match of (item.content || '').matchAll(/(?:^|\n)\s*([A-Za-z][A-Za-z0-9_ /-]{1,50})\s*[:\-–]\s*([^\n]+)/g)) {
|
|
1406
1417
|
addField(match[1]);
|
|
@@ -1551,6 +1562,7 @@ RULES:
|
|
|
1551
1562
|
}
|
|
1552
1563
|
|
|
1553
1564
|
private static extractStockQuantity(item: VectorMatch): number | null {
|
|
1565
|
+
if (!item) return null;
|
|
1554
1566
|
const meta = item.metadata || {};
|
|
1555
1567
|
const stockValue = resolveMetadataValue(meta, 'stock');
|
|
1556
1568
|
const numericStock = this.toFiniteNumber(stockValue);
|