@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "2.0.7",
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",
@@ -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 = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
584
- const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
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 = wantsExhaustiveList ? retrievalLimit : topK;
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 - Review all retrieved/reranked sources for full comprehension
621
- let context = fullSources.length
622
- ? fullSources.map((m, i) => `[Source ${i + 1}]\n${m?.content ?? ''}`).join('\n\n---\n\n')
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
- // Count the records of relevant sources to pass into the slice dynamically
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
- sources: sources.map((s, idx) => ({
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 now that we have the full reply.
868
- // It runs in parallel with the remaining post-processing below.
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 isLocalProvider = this.config.llm.provider === 'ollama';
1007
- const disableLlmUiTransform = this.config.llm.options?.disableLlmUiTransform === true;
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
- if (typeof _g.__retrivoraDispatchChat === 'function') {
111
+ let dispatch = _g.__retrivoraDispatchChat;
112
+ if (typeof dispatch !== 'function') {
101
113
  try {
102
- const res = await _g.__retrivoraDispatchChat({
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
- if (res?.response?.choices?.[0]?.message?.content) {
110
- return res.response.choices[0].message.content;
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
- if (typeof _g.__retrivoraDispatchChat === 'function') {
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 _g.__retrivoraDispatchChat({
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 if (res?.response?.choices?.[0]?.message?.content) {
187
- yield res.response.choices[0].message.content;
188
- return;
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
- if (typeof _g.__retrivoraDispatchEmbedding === 'function') {
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 _g.__retrivoraDispatchEmbedding({
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
  };