@retrivora-ai/rag-engine 2.0.7 → 2.0.8
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 +706 -54
- package/dist/handlers/index.mjs +706 -54
- package/dist/index.js +5 -4
- package/dist/index.mjs +5 -4
- package/dist/server.js +706 -54
- package/dist/server.mjs +706 -54
- package/package.json +1 -1
- package/src/core/Pipeline.ts +19 -37
- package/src/llm/providers/UniversalLLMAdapter.ts +74 -16
- package/src/utils/UITransformer.ts +4 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "UNLICENSED",
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -580,8 +580,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
580
580
|
|
|
581
581
|
const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
|
|
582
582
|
const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
|
|
583
|
-
const wantsExhaustiveList =
|
|
584
|
-
const retrievalLimit =
|
|
583
|
+
const wantsExhaustiveList = true; // Always query all available resources across the database
|
|
584
|
+
const retrievalLimit = Math.max(topK * 50, 1000);
|
|
585
585
|
|
|
586
586
|
const rawSources = ((strategyResult === 'vector' || strategyResult === 'both') && queryVector && queryVector.length > 0
|
|
587
587
|
? await this.vectorDB.query(queryVector, retrievalLimit, ns, filter)
|
|
@@ -602,52 +602,37 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
602
602
|
const embedMs = retrieveEnd - embedStart;
|
|
603
603
|
const retrieveMs = retrieveEnd - embedStart;
|
|
604
604
|
|
|
605
|
-
// 3. Reranking
|
|
605
|
+
// 3. Reranking & Filtering across ALL matching resources
|
|
606
606
|
const rerankStart = performance.now();
|
|
607
607
|
const structuredSources = this.applyStructuredFilters(rawSources, filter).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
608
608
|
let fullSources = hasNumericPredicates
|
|
609
609
|
? structuredSources
|
|
610
610
|
: structuredSources.filter((m) => (m?.score ?? 0) >= scoreThreshold);
|
|
611
|
-
const rerankLimit =
|
|
611
|
+
const rerankLimit = Math.max(retrievalLimit, fullSources.length);
|
|
612
612
|
const useReranking = arch !== 'naive' && (arch === 'retrieve-and-rerank' || this.config.rag?.useReranking);
|
|
613
613
|
if (!hasNumericPredicates && useReranking) {
|
|
614
614
|
fullSources = (await this.reranker.rerank(fullSources, question, rerankLimit)).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
615
|
-
} else if (!wantsExhaustiveList) {
|
|
616
|
-
fullSources = fullSources.slice(0, topK);
|
|
617
615
|
}
|
|
618
616
|
const rerankMs = performance.now() - rerankStart;
|
|
619
617
|
|
|
620
|
-
// 4. Context Augmentation
|
|
621
|
-
|
|
622
|
-
|
|
618
|
+
// 4. Context Augmentation — Cap context sent to LLM to top 15 sources (~3000 tokens)
|
|
619
|
+
// to keep prompt under Groq's 6000 token limit while preserving ALL sources for UI citations.
|
|
620
|
+
const totalCount = fullSources.length;
|
|
621
|
+
const promptSources = fullSources.slice(0, 15);
|
|
622
|
+
let context = promptSources.length
|
|
623
|
+
? `[SEARCH SUMMARY: Found ${totalCount} matching record(s) out of ${rawSources.length} total resources searched in database. Context includes top ${promptSources.length} relevant records]\n\n` +
|
|
624
|
+
promptSources.map((m, i) => `[Source ${i + 1}]\n${m?.content ?? ''}`).join('\n\n---\n\n')
|
|
623
625
|
: 'No relevant context found.';
|
|
624
626
|
|
|
625
|
-
//
|
|
626
|
-
let displayCount = 15; // default fallback
|
|
627
|
-
|
|
628
|
-
if (hasMetadataFilter) {
|
|
629
|
-
// If a metadata filter matches (e.g. brand, category), all of those matches are highly relevant
|
|
630
|
-
displayCount = fullSources.length;
|
|
631
|
-
} else {
|
|
632
|
-
// For general semantic queries, count the number of matches meeting high relevance threshold
|
|
633
|
-
const baseThreshold = Math.max(scoreThreshold, 0.4);
|
|
634
|
-
const highlyRelevant = fullSources.filter(m => (m?.score ?? 0) >= baseThreshold);
|
|
635
|
-
displayCount = Math.max(highlyRelevant.length, topK);
|
|
636
|
-
// Cap non-metadata semantic searches to a maximum of 15 to keep the UI clean
|
|
637
|
-
if (displayCount > 15) {
|
|
638
|
-
displayCount = 15;
|
|
639
|
-
}
|
|
640
|
-
}
|
|
641
|
-
|
|
642
|
-
// Ensure they are sorted from highest to lowest score, then slice using our dynamic count
|
|
627
|
+
// Do NOT truncate UI sources — keep all qualified matches for UI citations & tables
|
|
643
628
|
const sources = [...fullSources]
|
|
644
629
|
.filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'))
|
|
645
|
-
.sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0))
|
|
646
|
-
.slice(0, displayCount);
|
|
630
|
+
.sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0));
|
|
647
631
|
|
|
648
632
|
console.log('[Pipeline] Final sources for prompt & UI:', {
|
|
649
633
|
count: sources.length,
|
|
650
|
-
|
|
634
|
+
totalRawCount: rawSources.length,
|
|
635
|
+
sources: sources.slice(0, 10).map((s, idx) => ({
|
|
651
636
|
rank: idx + 1,
|
|
652
637
|
id: s?.id,
|
|
653
638
|
score: s?.score,
|
|
@@ -864,10 +849,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
864
849
|
yield fullReply;
|
|
865
850
|
}
|
|
866
851
|
|
|
867
|
-
// Kick off hallucination scoring
|
|
868
|
-
|
|
869
|
-
const runHallucination = this.config.llm.options?.hallucinationScoring === true ||
|
|
870
|
-
(this.config.llm.provider !== 'ollama' && this.config.llm.options?.hallucinationScoring !== false);
|
|
852
|
+
// Kick off hallucination scoring ONLY when explicitly enabled by options
|
|
853
|
+
const runHallucination = this.config.llm.options?.hallucinationScoring === true;
|
|
871
854
|
if (runHallucination) {
|
|
872
855
|
hallucinationScoringPromise = scoreHallucination(this.llmProvider, fullReply, context)
|
|
873
856
|
.catch(() => undefined);
|
|
@@ -1003,9 +986,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1003
986
|
);
|
|
1004
987
|
}
|
|
1005
988
|
|
|
1006
|
-
const
|
|
1007
|
-
|
|
1008
|
-
if (forceDeterministic || isLocalProvider || disableLlmUiTransform) {
|
|
989
|
+
const enableLlmUiTransform = this.config.llm.options?.enableLlmUiTransform === true;
|
|
990
|
+
if (forceDeterministic || !enableLlmUiTransform) {
|
|
1009
991
|
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
1010
992
|
}
|
|
1011
993
|
|
|
@@ -18,6 +18,17 @@ interface UniversalLLMOptions {
|
|
|
18
18
|
timeout?: number;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function extractContent(obj: unknown): string | undefined {
|
|
22
|
+
if (!obj || typeof obj !== 'object') return undefined;
|
|
23
|
+
const choice = (obj as any).choices?.[0];
|
|
24
|
+
if (!choice) return undefined;
|
|
25
|
+
if (typeof choice.message?.content === 'string') return choice.message.content;
|
|
26
|
+
if (typeof choice.delta?.content === 'string') return choice.delta.content;
|
|
27
|
+
if (typeof choice.text === 'string') return choice.text;
|
|
28
|
+
if (typeof choice.content === 'string') return choice.content;
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
export class UniversalLLMAdapter implements ILLMProvider {
|
|
22
33
|
private readonly http: AxiosInstance;
|
|
23
34
|
private readonly model: string;
|
|
@@ -97,28 +108,44 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
97
108
|
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
98
109
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
99
110
|
const _g = globalThis as any;
|
|
100
|
-
|
|
111
|
+
let dispatch = _g.__retrivoraDispatchChat;
|
|
112
|
+
if (typeof dispatch !== 'function') {
|
|
101
113
|
try {
|
|
102
|
-
const
|
|
114
|
+
const gateway = await import('../../../../../services/llm-gateway/router');
|
|
115
|
+
if (gateway?.dispatchChatCompletion) {
|
|
116
|
+
_g.__retrivoraDispatchChat = gateway.dispatchChatCompletion;
|
|
117
|
+
_g.__retrivoraDispatchEmbedding = gateway.dispatchEmbedding;
|
|
118
|
+
dispatch = gateway.dispatchChatCompletion;
|
|
119
|
+
}
|
|
120
|
+
} catch { /* proceed */ }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (typeof dispatch === 'function') {
|
|
124
|
+
try {
|
|
125
|
+
const res = await dispatch({
|
|
103
126
|
model: this.model,
|
|
104
127
|
messages: formattedMessages as any,
|
|
105
128
|
max_tokens: this.maxTokens,
|
|
106
129
|
temperature: this.temperature,
|
|
107
130
|
}, this.apiKey);
|
|
108
131
|
|
|
109
|
-
|
|
110
|
-
|
|
132
|
+
const content = extractContent(res?.response);
|
|
133
|
+
if (content !== undefined) {
|
|
134
|
+
return content;
|
|
111
135
|
}
|
|
136
|
+
throw new Error(`[UniversalLLMAdapter] In-process dispatch returned empty content for model: ${this.model}`);
|
|
112
137
|
} catch (dispatchErr) {
|
|
113
138
|
throw dispatchErr;
|
|
114
139
|
}
|
|
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.`);
|
|
115
142
|
}
|
|
116
143
|
}
|
|
117
144
|
|
|
118
145
|
const { data } = await this.http.post(path, payload);
|
|
119
146
|
|
|
120
147
|
const extractPath = this.opts.responseExtractPath ?? 'choices[0].message.content';
|
|
121
|
-
const result = resolvePath(data, extractPath);
|
|
148
|
+
const result = resolvePath(data, extractPath) ?? extractContent(data);
|
|
122
149
|
|
|
123
150
|
if (result === undefined) {
|
|
124
151
|
throw new Error(`[UniversalLLMAdapter] Could not extract text from path '${extractPath}' in response.`);
|
|
@@ -171,9 +198,21 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
171
198
|
|
|
172
199
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
173
200
|
const _g = globalThis as any;
|
|
174
|
-
|
|
201
|
+
let dispatch = _g.__retrivoraDispatchChat;
|
|
202
|
+
if (typeof dispatch !== 'function') {
|
|
203
|
+
try {
|
|
204
|
+
const gateway = await import('../../../../../services/llm-gateway/router');
|
|
205
|
+
if (gateway?.dispatchChatCompletion) {
|
|
206
|
+
_g.__retrivoraDispatchChat = gateway.dispatchChatCompletion;
|
|
207
|
+
_g.__retrivoraDispatchEmbedding = gateway.dispatchEmbedding;
|
|
208
|
+
dispatch = gateway.dispatchChatCompletion;
|
|
209
|
+
}
|
|
210
|
+
} catch { /* proceed */ }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof dispatch === 'function') {
|
|
175
214
|
try {
|
|
176
|
-
const res = await
|
|
215
|
+
const res = await dispatch({
|
|
177
216
|
model: this.model,
|
|
178
217
|
messages: formattedMessages as any,
|
|
179
218
|
max_tokens: this.maxTokens,
|
|
@@ -183,15 +222,19 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
183
222
|
|
|
184
223
|
if (res?.stream) {
|
|
185
224
|
streamBody = res.stream as ReadableStream<Uint8Array>;
|
|
186
|
-
} else
|
|
187
|
-
|
|
188
|
-
|
|
225
|
+
} else {
|
|
226
|
+
const content = extractContent(res?.response);
|
|
227
|
+
if (content !== undefined) {
|
|
228
|
+
yield content;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
throw new Error(`[UniversalLLMAdapter] In-process dispatch stream returned empty response for model: ${this.model}`);
|
|
189
232
|
}
|
|
190
233
|
} catch (dispatchErr) {
|
|
191
|
-
// If in-process dispatch threw a real error (Rate limit, Auth, API error), rethrow it
|
|
192
|
-
// rather than silently falling back to external HTTP fetch which masks the error.
|
|
193
234
|
throw dispatchErr;
|
|
194
235
|
}
|
|
236
|
+
} else {
|
|
237
|
+
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.`);
|
|
195
238
|
}
|
|
196
239
|
}
|
|
197
240
|
|
|
@@ -233,7 +276,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
233
276
|
|
|
234
277
|
try {
|
|
235
278
|
const json = JSON.parse(trimmed.slice(5).trim());
|
|
236
|
-
const text = resolvePath(json, extractPath);
|
|
279
|
+
const text = (resolvePath(json, extractPath) as string | undefined) ?? extractContent(json);
|
|
237
280
|
if (text && typeof text === 'string') yield text;
|
|
238
281
|
} catch {
|
|
239
282
|
// Malformed SSE line — skip
|
|
@@ -246,7 +289,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
246
289
|
const jsonStr = buffer.replace(/^data:\s*/, '').trim();
|
|
247
290
|
try {
|
|
248
291
|
const json = JSON.parse(jsonStr);
|
|
249
|
-
const text = resolvePath(json, extractPath);
|
|
292
|
+
const text = (resolvePath(json, extractPath) as string | undefined) ?? extractContent(json);
|
|
250
293
|
if (text && typeof text === 'string') yield text;
|
|
251
294
|
} catch { /* ignore final partial line */ }
|
|
252
295
|
}
|
|
@@ -274,9 +317,21 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
274
317
|
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
275
318
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
276
319
|
const _g = globalThis as any;
|
|
277
|
-
|
|
320
|
+
let dispatch = _g.__retrivoraDispatchEmbedding;
|
|
321
|
+
if (typeof dispatch !== 'function') {
|
|
322
|
+
try {
|
|
323
|
+
const gateway = await import('../../../../../services/llm-gateway/router');
|
|
324
|
+
if (gateway?.dispatchEmbedding) {
|
|
325
|
+
_g.__retrivoraDispatchChat = gateway.dispatchChatCompletion;
|
|
326
|
+
_g.__retrivoraDispatchEmbedding = gateway.dispatchEmbedding;
|
|
327
|
+
dispatch = gateway.dispatchEmbedding;
|
|
328
|
+
}
|
|
329
|
+
} catch { /* proceed */ }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (typeof dispatch === 'function') {
|
|
278
333
|
try {
|
|
279
|
-
const res = await
|
|
334
|
+
const res = await dispatch({
|
|
280
335
|
model: this.model,
|
|
281
336
|
input: text,
|
|
282
337
|
}, this.apiKey);
|
|
@@ -284,9 +339,12 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
284
339
|
if (res?.data?.[0]?.embedding) {
|
|
285
340
|
return res.data[0].embedding;
|
|
286
341
|
}
|
|
342
|
+
throw new Error(`[UniversalLLMAdapter] In-process embedding dispatch returned invalid vector for model: ${this.model}`);
|
|
287
343
|
} catch (dispatchErr) {
|
|
288
344
|
throw dispatchErr;
|
|
289
345
|
}
|
|
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.`);
|
|
290
348
|
}
|
|
291
349
|
}
|
|
292
350
|
|
|
@@ -560,8 +560,7 @@ RULES:
|
|
|
560
560
|
): UITransformationResponse {
|
|
561
561
|
const products: CarouselProduct[] = data
|
|
562
562
|
.map(item => this.extractProductInfo(item, config, trainedSchema))
|
|
563
|
-
.filter((p): p is CarouselProduct => p !== null)
|
|
564
|
-
.slice(0, 15);
|
|
563
|
+
.filter((p): p is CarouselProduct => p !== null);
|
|
565
564
|
|
|
566
565
|
return {
|
|
567
566
|
type: 'product_carousel',
|
|
@@ -1035,7 +1034,7 @@ RULES:
|
|
|
1035
1034
|
}
|
|
1036
1035
|
|
|
1037
1036
|
private static profileData(data: VectorMatch[]): DataProfile {
|
|
1038
|
-
const records = data.map((item): DataRecord => {
|
|
1037
|
+
const records = (data || []).filter((item): item is VectorMatch => Boolean(item && typeof item === 'object')).map((item): DataRecord => {
|
|
1039
1038
|
const fields: Record<string, PrimitiveValue> = {};
|
|
1040
1039
|
Object.entries(item.metadata || {}).forEach(([key, value]) => {
|
|
1041
1040
|
const primitive = this.toPrimitive(value);
|
|
@@ -1044,8 +1043,8 @@ RULES:
|
|
|
1044
1043
|
if (!fields.content && item.content) fields.content = item.content.substring(0, 500);
|
|
1045
1044
|
return {
|
|
1046
1045
|
id: item.id,
|
|
1047
|
-
content: item.content,
|
|
1048
|
-
score: item.score,
|
|
1046
|
+
content: item.content ?? '',
|
|
1047
|
+
score: item.score ?? 0,
|
|
1049
1048
|
fields,
|
|
1050
1049
|
source: item,
|
|
1051
1050
|
};
|