@retrivora-ai/rag-engine 2.0.6 → 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 +863 -94
- package/dist/handlers/index.mjs +863 -94
- package/dist/index.js +84 -26
- package/dist/index.mjs +84 -26
- package/dist/server.js +863 -94
- package/dist/server.mjs +863 -94
- package/package.json +1 -1
- package/src/core/Pipeline.ts +67 -47
- package/src/core/QueryProcessor.ts +3 -3
- package/src/llm/providers/UniversalLLMAdapter.ts +86 -26
- package/src/rag/Reranker.ts +2 -2
- package/src/rendering/IntentClassifier.ts +1 -1
- package/src/rendering/RendererRegistry.ts +1 -1
- package/src/utils/ProductExtractor.ts +1 -1
- package/src/utils/UITransformer.ts +66 -22
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,58 +580,67 @@ 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
|
-
const rawSources = (strategyResult === 'vector' || strategyResult === 'both') && queryVector && queryVector.length > 0
|
|
586
|
+
const rawSources = ((strategyResult === 'vector' || strategyResult === 'both') && queryVector && queryVector.length > 0
|
|
587
587
|
? await this.vectorDB.query(queryVector, retrievalLimit, ns, filter)
|
|
588
|
-
: [];
|
|
588
|
+
: []).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
589
|
+
|
|
590
|
+
console.log('[Pipeline] Raw vectorDB.query response:', {
|
|
591
|
+
rawCount: rawSources.length,
|
|
592
|
+
sample: rawSources.slice(0, 2).map((s) => ({
|
|
593
|
+
id: s?.id,
|
|
594
|
+
score: s?.score,
|
|
595
|
+
contentType: typeof s?.content,
|
|
596
|
+
contentSnippet: String(s?.content ?? '').substring(0, 150),
|
|
597
|
+
metadataKeys: Object.keys(s?.metadata || {}),
|
|
598
|
+
})),
|
|
599
|
+
});
|
|
589
600
|
|
|
590
601
|
const retrieveEnd = performance.now();
|
|
591
602
|
const embedMs = retrieveEnd - embedStart;
|
|
592
603
|
const retrieveMs = retrieveEnd - embedStart;
|
|
593
604
|
|
|
594
|
-
// 3. Reranking
|
|
605
|
+
// 3. Reranking & Filtering across ALL matching resources
|
|
595
606
|
const rerankStart = performance.now();
|
|
596
|
-
const structuredSources = this.applyStructuredFilters(rawSources, filter);
|
|
607
|
+
const structuredSources = this.applyStructuredFilters(rawSources, filter).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
597
608
|
let fullSources = hasNumericPredicates
|
|
598
609
|
? structuredSources
|
|
599
|
-
: structuredSources.filter((m) => m
|
|
600
|
-
const rerankLimit =
|
|
610
|
+
: structuredSources.filter((m) => (m?.score ?? 0) >= scoreThreshold);
|
|
611
|
+
const rerankLimit = Math.max(retrievalLimit, fullSources.length);
|
|
601
612
|
const useReranking = arch !== 'naive' && (arch === 'retrieve-and-rerank' || this.config.rag?.useReranking);
|
|
602
613
|
if (!hasNumericPredicates && useReranking) {
|
|
603
|
-
fullSources = await this.reranker.rerank(fullSources, question, rerankLimit);
|
|
604
|
-
} else if (!wantsExhaustiveList) {
|
|
605
|
-
fullSources = fullSources.slice(0, topK);
|
|
614
|
+
fullSources = (await this.reranker.rerank(fullSources, question, rerankLimit)).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
606
615
|
}
|
|
607
616
|
const rerankMs = performance.now() - rerankStart;
|
|
608
617
|
|
|
609
|
-
// 4. Context Augmentation
|
|
610
|
-
|
|
611
|
-
|
|
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')
|
|
612
625
|
: 'No relevant context found.';
|
|
613
626
|
|
|
614
|
-
//
|
|
615
|
-
let displayCount = 15; // default fallback
|
|
616
|
-
|
|
617
|
-
if (hasMetadataFilter) {
|
|
618
|
-
// If a metadata filter matches (e.g. brand, category), all of those matches are highly relevant
|
|
619
|
-
displayCount = fullSources.length;
|
|
620
|
-
} else {
|
|
621
|
-
// For general semantic queries, count the number of matches meeting high relevance threshold
|
|
622
|
-
const baseThreshold = Math.max(scoreThreshold, 0.4);
|
|
623
|
-
const highlyRelevant = fullSources.filter(m => m.score >= baseThreshold);
|
|
624
|
-
displayCount = Math.max(highlyRelevant.length, topK);
|
|
625
|
-
// Cap non-metadata semantic searches to a maximum of 15 to keep the UI clean
|
|
626
|
-
if (displayCount > 15) {
|
|
627
|
-
displayCount = 15;
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
// 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
|
|
632
628
|
const sources = [...fullSources]
|
|
633
|
-
.
|
|
634
|
-
.
|
|
629
|
+
.filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'))
|
|
630
|
+
.sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0));
|
|
631
|
+
|
|
632
|
+
console.log('[Pipeline] Final sources for prompt & UI:', {
|
|
633
|
+
count: sources.length,
|
|
634
|
+
totalRawCount: rawSources.length,
|
|
635
|
+
sources: sources.slice(0, 10).map((s, idx) => ({
|
|
636
|
+
rank: idx + 1,
|
|
637
|
+
id: s?.id,
|
|
638
|
+
score: s?.score,
|
|
639
|
+
contentType: typeof s?.content,
|
|
640
|
+
contentSnippet: String(s?.content ?? '').substring(0, 150),
|
|
641
|
+
metadata: s?.metadata,
|
|
642
|
+
})),
|
|
643
|
+
});
|
|
635
644
|
|
|
636
645
|
if (graphData && graphData.nodes.length > 0) {
|
|
637
646
|
const graphContext = graphData.nodes.map(n =>
|
|
@@ -653,17 +662,17 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
653
662
|
// Yield sources immediately to support early UI citation rendering (highly responsive)
|
|
654
663
|
yield {
|
|
655
664
|
reply: '',
|
|
656
|
-
sources: sources.map((s) => ({
|
|
665
|
+
sources: (sources || []).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object')).map((s) => ({
|
|
657
666
|
id: s.id,
|
|
658
|
-
score: s.score,
|
|
659
|
-
content: s
|
|
660
|
-
metadata: s
|
|
667
|
+
score: s.score ?? 0,
|
|
668
|
+
content: s?.content ?? '',
|
|
669
|
+
metadata: s?.metadata ?? {},
|
|
661
670
|
namespace: ns,
|
|
662
671
|
} satisfies RetrievedChunk)),
|
|
663
672
|
} as ChatResponse;
|
|
664
673
|
|
|
665
674
|
// 4.5 Schema Training — true fire-and-forget, never gates anything.
|
|
666
|
-
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s
|
|
675
|
+
const allMetadataKeys = Array.from(new Set(sources.filter(Boolean).flatMap(s => Object.keys(s?.metadata || {}))));
|
|
667
676
|
const cachedSchema = allMetadataKeys.length > 0
|
|
668
677
|
? SchemaMapper.getCached(ns, allMetadataKeys)
|
|
669
678
|
: undefined;
|
|
@@ -840,10 +849,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
840
849
|
yield fullReply;
|
|
841
850
|
}
|
|
842
851
|
|
|
843
|
-
// Kick off hallucination scoring
|
|
844
|
-
|
|
845
|
-
const runHallucination = this.config.llm.options?.hallucinationScoring === true ||
|
|
846
|
-
(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;
|
|
847
854
|
if (runHallucination) {
|
|
848
855
|
hallucinationScoringPromise = scoreHallucination(this.llmProvider, fullReply, context)
|
|
849
856
|
.catch(() => undefined);
|
|
@@ -979,9 +986,8 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
979
986
|
);
|
|
980
987
|
}
|
|
981
988
|
|
|
982
|
-
const
|
|
983
|
-
|
|
984
|
-
if (forceDeterministic || isLocalProvider || disableLlmUiTransform) {
|
|
989
|
+
const enableLlmUiTransform = this.config.llm.options?.enableLlmUiTransform === true;
|
|
990
|
+
if (forceDeterministic || !enableLlmUiTransform) {
|
|
985
991
|
return UITransformer.transform(question, sources, this.config, cachedSchema);
|
|
986
992
|
}
|
|
987
993
|
|
|
@@ -1015,6 +1021,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1015
1021
|
}
|
|
1016
1022
|
|
|
1017
1023
|
private resolveNumericPredicateValue(source: VectorMatch, predicate: NumericPredicate): number | null {
|
|
1024
|
+
if (!source) return null;
|
|
1018
1025
|
const meta = source.metadata || {};
|
|
1019
1026
|
const field = predicate.field;
|
|
1020
1027
|
const entries = Object.entries(meta)
|
|
@@ -1032,7 +1039,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1032
1039
|
if (value !== null) return value;
|
|
1033
1040
|
}
|
|
1034
1041
|
|
|
1035
|
-
const contentValue = this.extractNumericValueFromContent(source.content, field);
|
|
1042
|
+
const contentValue = this.extractNumericValueFromContent(source.content ?? '', field);
|
|
1036
1043
|
if (contentValue !== null) return contentValue;
|
|
1037
1044
|
}
|
|
1038
1045
|
|
|
@@ -1046,6 +1053,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1046
1053
|
}
|
|
1047
1054
|
|
|
1048
1055
|
private extractNumericValueFromContent(content: string, field: string): number | null {
|
|
1056
|
+
if (!content || typeof content !== 'string') return null;
|
|
1049
1057
|
const escapedWords = field
|
|
1050
1058
|
.split(/\s+|_+|-+/)
|
|
1051
1059
|
.filter(Boolean)
|
|
@@ -1164,6 +1172,18 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1164
1172
|
}
|
|
1165
1173
|
}
|
|
1166
1174
|
|
|
1175
|
+
console.log('[Pipeline] retrieve() response:', {
|
|
1176
|
+
query,
|
|
1177
|
+
namespace: ns,
|
|
1178
|
+
count: resolvedSources.length,
|
|
1179
|
+
sample: resolvedSources.slice(0, 2).map((s) => ({
|
|
1180
|
+
id: s?.id,
|
|
1181
|
+
score: s?.score,
|
|
1182
|
+
contentSnippet: String(s?.content ?? '').substring(0, 150),
|
|
1183
|
+
metadata: s?.metadata,
|
|
1184
|
+
})),
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1167
1187
|
return { sources: resolvedSources, graphData };
|
|
1168
1188
|
}
|
|
1169
1189
|
|
|
@@ -360,10 +360,10 @@ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
|
|
|
360
360
|
{ temperature: 0, maxTokens: 10 }
|
|
361
361
|
);
|
|
362
362
|
|
|
363
|
-
const cleanResponse = response.trim().toLowerCase();
|
|
364
|
-
if (cleanResponse.includes('vector')) return 'vector';
|
|
365
|
-
if (cleanResponse.includes('graph')) return 'graph';
|
|
363
|
+
const cleanResponse = (response ?? '').trim().toLowerCase();
|
|
366
364
|
if (cleanResponse.includes('both')) return 'both';
|
|
365
|
+
if (cleanResponse.includes('graph')) return 'graph';
|
|
366
|
+
if (cleanResponse.includes('vector')) return 'vector';
|
|
367
367
|
} catch (error) {
|
|
368
368
|
console.warn('[QueryProcessor] LLM strategy classification failed, falling back to keywords:', error);
|
|
369
369
|
}
|
|
@@ -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;
|
|
@@ -96,29 +107,45 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
96
107
|
|
|
97
108
|
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
98
109
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
110
|
+
const _g = globalThis as any;
|
|
111
|
+
let dispatch = _g.__retrivoraDispatchChat;
|
|
112
|
+
if (typeof dispatch !== 'function') {
|
|
113
|
+
try {
|
|
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}`);
|
|
137
|
+
} catch (dispatchErr) {
|
|
138
|
+
throw dispatchErr;
|
|
112
139
|
}
|
|
113
|
-
}
|
|
114
|
-
|
|
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.`);
|
|
@@ -170,10 +197,22 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
170
197
|
let streamBody: ReadableStream<Uint8Array> | null = null;
|
|
171
198
|
|
|
172
199
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
200
|
+
const _g = globalThis as any;
|
|
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') {
|
|
214
|
+
try {
|
|
215
|
+
const res = await dispatch({
|
|
177
216
|
model: this.model,
|
|
178
217
|
messages: formattedMessages as any,
|
|
179
218
|
max_tokens: this.maxTokens,
|
|
@@ -183,13 +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
|
}
|
|
233
|
+
} catch (dispatchErr) {
|
|
234
|
+
throw dispatchErr;
|
|
190
235
|
}
|
|
191
|
-
}
|
|
192
|
-
|
|
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.`);
|
|
193
238
|
}
|
|
194
239
|
}
|
|
195
240
|
|
|
@@ -231,7 +276,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
231
276
|
|
|
232
277
|
try {
|
|
233
278
|
const json = JSON.parse(trimmed.slice(5).trim());
|
|
234
|
-
const text = resolvePath(json, extractPath);
|
|
279
|
+
const text = (resolvePath(json, extractPath) as string | undefined) ?? extractContent(json);
|
|
235
280
|
if (text && typeof text === 'string') yield text;
|
|
236
281
|
} catch {
|
|
237
282
|
// Malformed SSE line — skip
|
|
@@ -244,7 +289,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
244
289
|
const jsonStr = buffer.replace(/^data:\s*/, '').trim();
|
|
245
290
|
try {
|
|
246
291
|
const json = JSON.parse(jsonStr);
|
|
247
|
-
const text = resolvePath(json, extractPath);
|
|
292
|
+
const text = (resolvePath(json, extractPath) as string | undefined) ?? extractContent(json);
|
|
248
293
|
if (text && typeof text === 'string') yield text;
|
|
249
294
|
} catch { /* ignore final partial line */ }
|
|
250
295
|
}
|
|
@@ -271,10 +316,22 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
271
316
|
|
|
272
317
|
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
273
318
|
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
319
|
+
const _g = globalThis as any;
|
|
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') {
|
|
333
|
+
try {
|
|
334
|
+
const res = await dispatch({
|
|
278
335
|
model: this.model,
|
|
279
336
|
input: text,
|
|
280
337
|
}, this.apiKey);
|
|
@@ -282,9 +339,12 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
282
339
|
if (res?.data?.[0]?.embedding) {
|
|
283
340
|
return res.data[0].embedding;
|
|
284
341
|
}
|
|
342
|
+
throw new Error(`[UniversalLLMAdapter] In-process embedding dispatch returned invalid vector for model: ${this.model}`);
|
|
343
|
+
} catch (dispatchErr) {
|
|
344
|
+
throw dispatchErr;
|
|
285
345
|
}
|
|
286
|
-
}
|
|
287
|
-
|
|
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.`);
|
|
288
348
|
}
|
|
289
349
|
}
|
|
290
350
|
|
package/src/rag/Reranker.ts
CHANGED
|
@@ -25,7 +25,7 @@ export class Reranker {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const scoredMatches = matches.map(match => {
|
|
28
|
-
const contentLower = match
|
|
28
|
+
const contentLower = (match?.content ?? '').toLowerCase();
|
|
29
29
|
let keywordScore = 0;
|
|
30
30
|
|
|
31
31
|
keywords.forEach(keyword => {
|
|
@@ -57,7 +57,7 @@ export class Reranker {
|
|
|
57
57
|
const topN = matches.slice(0, 10);
|
|
58
58
|
|
|
59
59
|
const response = await llm.chat(
|
|
60
|
-
[{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${m
|
|
60
|
+
[{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${(m?.content ?? '').replace(/\n/g, ' ')}`).join('\n')}` }],
|
|
61
61
|
'',
|
|
62
62
|
{
|
|
63
63
|
systemPrompt: 'You are a relevance ranking expert. Given the user query and a list of retrieved document chunks, rank the chunks by relevance to the query. Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant. Use the indices provided in brackets like [0], [1], etc. Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.',
|
|
@@ -107,7 +107,7 @@ export class IntentClassifier {
|
|
|
107
107
|
numericFieldCount = numericKeys.size;
|
|
108
108
|
categoricalFieldCount = catKeys.size;
|
|
109
109
|
|
|
110
|
-
if (productKeyMatches >= 2 || docs.some(d => d
|
|
110
|
+
if (productKeyMatches >= 2 || docs.some(d => (d?.content ?? '').toLowerCase().includes('price') || (d?.content ?? '').toLowerCase().includes('brand'))) {
|
|
111
111
|
isProductLike = true;
|
|
112
112
|
}
|
|
113
113
|
}
|
|
@@ -13,7 +13,7 @@ export class TextRendererStrategy implements IRendererStrategy {
|
|
|
13
13
|
return { type: 'text', title, data: { content: data } };
|
|
14
14
|
}
|
|
15
15
|
const docs = Array.isArray(data) ? (data as VectorMatch[]) : [];
|
|
16
|
-
const text = docs.map(d => d
|
|
16
|
+
const text = docs.map(d => d?.content ?? '').join('\n\n');
|
|
17
17
|
return { type: 'text', title, data: { content: text || 'No detailed content available.' } };
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -216,7 +216,7 @@ export function extractProductsFromSources(sources: VectorMatch[] | undefined, i
|
|
|
216
216
|
|
|
217
217
|
return {
|
|
218
218
|
id: s.id,
|
|
219
|
-
name: name ?? s
|
|
219
|
+
name: name ?? (s?.content ?? '').split('\n')[0] ?? 'Unknown Product',
|
|
220
220
|
brand: brand,
|
|
221
221
|
price: price,
|
|
222
222
|
image: resolveImage(m),
|
|
@@ -128,6 +128,17 @@ export class UITransformer {
|
|
|
128
128
|
return this.createTextResponse('No data available', 'No relevant data found for your query.');
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
console.log('[UITransformer.transform] Processing retrievedData:', {
|
|
132
|
+
userQuery,
|
|
133
|
+
retrievedCount: retrievedData.length,
|
|
134
|
+
sample: retrievedData.slice(0, 2).map((item) => ({
|
|
135
|
+
id: item?.id,
|
|
136
|
+
contentType: typeof item?.content,
|
|
137
|
+
contentSnippet: String(item?.content ?? '').substring(0, 150),
|
|
138
|
+
metadataKeys: Object.keys(item?.metadata || {}),
|
|
139
|
+
})),
|
|
140
|
+
});
|
|
141
|
+
|
|
131
142
|
const resolvedIntent = intent ?? this.detectIntentHeuristic(userQuery);
|
|
132
143
|
|
|
133
144
|
const filteredData = resolvedIntent.filterInStockOnly
|
|
@@ -549,8 +560,7 @@ RULES:
|
|
|
549
560
|
): UITransformationResponse {
|
|
550
561
|
const products: CarouselProduct[] = data
|
|
551
562
|
.map(item => this.extractProductInfo(item, config, trainedSchema))
|
|
552
|
-
.filter((p): p is CarouselProduct => p !== null)
|
|
553
|
-
.slice(0, 15);
|
|
563
|
+
.filter((p): p is CarouselProduct => p !== null);
|
|
554
564
|
|
|
555
565
|
return {
|
|
556
566
|
type: 'product_carousel',
|
|
@@ -760,7 +770,7 @@ RULES:
|
|
|
760
770
|
type: 'radar_chart' as VisualizationType,
|
|
761
771
|
title: 'Product Comparison',
|
|
762
772
|
description: `Comparing ${data.length} items across ${radarData.length} attributes`,
|
|
763
|
-
data: radarData.length > 0 ? radarData : data.map(d => ({ attribute: d
|
|
773
|
+
data: radarData.length > 0 ? radarData : data.map(d => ({ attribute: (d?.content ?? '').substring(0, 40) })),
|
|
764
774
|
};
|
|
765
775
|
}
|
|
766
776
|
|
|
@@ -780,7 +790,7 @@ RULES:
|
|
|
780
790
|
private static transformToText(data: VectorMatch[]): UITransformationResponse {
|
|
781
791
|
return this.createTextResponse(
|
|
782
792
|
'Retrieved Context',
|
|
783
|
-
data.map(item => item
|
|
793
|
+
data.map(item => item?.content ?? '').join('\n\n'),
|
|
784
794
|
`Found ${data.length} relevant results`,
|
|
785
795
|
);
|
|
786
796
|
}
|
|
@@ -983,6 +993,7 @@ RULES:
|
|
|
983
993
|
|
|
984
994
|
|
|
985
995
|
private static isProductData(item: VectorMatch): boolean {
|
|
996
|
+
if (!item) return false;
|
|
986
997
|
const content = (item.content || '').toLowerCase();
|
|
987
998
|
const productKeywords = [
|
|
988
999
|
'product', 'price', 'stock', 'sku', 'brand', 'msrp',
|
|
@@ -1023,7 +1034,7 @@ RULES:
|
|
|
1023
1034
|
}
|
|
1024
1035
|
|
|
1025
1036
|
private static profileData(data: VectorMatch[]): DataProfile {
|
|
1026
|
-
const records = data.map((item): DataRecord => {
|
|
1037
|
+
const records = (data || []).filter((item): item is VectorMatch => Boolean(item && typeof item === 'object')).map((item): DataRecord => {
|
|
1027
1038
|
const fields: Record<string, PrimitiveValue> = {};
|
|
1028
1039
|
Object.entries(item.metadata || {}).forEach(([key, value]) => {
|
|
1029
1040
|
const primitive = this.toPrimitive(value);
|
|
@@ -1032,8 +1043,8 @@ RULES:
|
|
|
1032
1043
|
if (!fields.content && item.content) fields.content = item.content.substring(0, 500);
|
|
1033
1044
|
return {
|
|
1034
1045
|
id: item.id,
|
|
1035
|
-
content: item.content,
|
|
1036
|
-
score: item.score,
|
|
1046
|
+
content: item.content ?? '',
|
|
1047
|
+
score: item.score ?? 0,
|
|
1037
1048
|
fields,
|
|
1038
1049
|
source: item,
|
|
1039
1050
|
};
|
|
@@ -1297,7 +1308,7 @@ RULES:
|
|
|
1297
1308
|
return {
|
|
1298
1309
|
timestamp: (meta.timestamp ?? meta.date ?? new Date().toISOString()) as string | number,
|
|
1299
1310
|
value: (meta.value ?? item.score ?? 0) as number,
|
|
1300
|
-
label: (meta.label ?? item
|
|
1311
|
+
label: (meta.label ?? (item?.content ?? '').substring(0, 50)) as string,
|
|
1301
1312
|
};
|
|
1302
1313
|
});
|
|
1303
1314
|
}
|
|
@@ -1429,7 +1440,7 @@ RULES:
|
|
|
1429
1440
|
}
|
|
1430
1441
|
|
|
1431
1442
|
private static resolveTableCellValue(item: VectorMatch, column: string): string | number | boolean {
|
|
1432
|
-
if (column === 'Content') return item
|
|
1443
|
+
if (column === 'Content') return (item?.content ?? '').substring(0, 100);
|
|
1433
1444
|
|
|
1434
1445
|
const meta = item.metadata || {};
|
|
1435
1446
|
const normalizedColumn = this.normalizeComparableField(column);
|
|
@@ -1444,10 +1455,10 @@ RULES:
|
|
|
1444
1455
|
const aliasValue = this.resolveAliasedTableCell(meta, column);
|
|
1445
1456
|
if (aliasValue !== undefined) return this.toDisplayValue(aliasValue);
|
|
1446
1457
|
|
|
1447
|
-
const contentValue = this.extractContentFieldValue(item
|
|
1458
|
+
const contentValue = this.extractContentFieldValue(item?.content ?? '', column);
|
|
1448
1459
|
if (contentValue !== null) return contentValue;
|
|
1449
1460
|
|
|
1450
|
-
const aliasContentValue = this.extractAliasedContentFieldValue(item
|
|
1461
|
+
const aliasContentValue = this.extractAliasedContentFieldValue(item?.content ?? '', column);
|
|
1451
1462
|
return aliasContentValue ?? '';
|
|
1452
1463
|
}
|
|
1453
1464
|
|
|
@@ -1519,6 +1530,7 @@ RULES:
|
|
|
1519
1530
|
}
|
|
1520
1531
|
|
|
1521
1532
|
private static determineStockStatus(item: VectorMatch): boolean {
|
|
1533
|
+
if (!item) return true;
|
|
1522
1534
|
const meta = item.metadata || {};
|
|
1523
1535
|
const stockValue = resolveMetadataValue(meta, 'stock');
|
|
1524
1536
|
if (stockValue !== undefined) {
|
|
@@ -1583,42 +1595,74 @@ RULES:
|
|
|
1583
1595
|
config?: import('../config/RagConfig').RagConfig,
|
|
1584
1596
|
trainedSchema?: import('./SchemaMapper').SchemaMap,
|
|
1585
1597
|
): CarouselProduct | null {
|
|
1598
|
+
if (!item) return null;
|
|
1586
1599
|
const meta = item.metadata || {};
|
|
1600
|
+
const content = item.content || '';
|
|
1587
1601
|
const name = this.getDynamicVal(meta, 'name', config, trainedSchema);
|
|
1588
1602
|
const price = this.getDynamicVal(meta, 'price', config, trainedSchema);
|
|
1589
1603
|
const brand = this.getDynamicVal(meta, 'brand', config, trainedSchema);
|
|
1590
1604
|
const description = this.cleanProductDescription(
|
|
1591
|
-
this.extractProductDescriptionFromContent(
|
|
1605
|
+
this.extractProductDescriptionFromContent(content) ??
|
|
1592
1606
|
this.getProductDescriptionValue(meta, config, trainedSchema),
|
|
1593
1607
|
);
|
|
1594
1608
|
|
|
1595
1609
|
if (name || this.isProductData(item)) {
|
|
1596
1610
|
let finalName = name ? String(name) : undefined;
|
|
1611
|
+
if (!finalName && content) {
|
|
1612
|
+
const nameMatch = content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
1613
|
+
finalName = nameMatch ? nameMatch[1].trim() : content.split('\n')[0]?.substring(0, 60);
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1597
1616
|
if (!finalName) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1617
|
+
console.warn(
|
|
1618
|
+
`[UITransformer] Unable to read product name from metadata or content for item ${item.id}. Defaulting to 'Unnamed Product'.`,
|
|
1619
|
+
{ metadataKeys: Object.keys(meta), contentLength: content.length },
|
|
1620
|
+
);
|
|
1600
1621
|
}
|
|
1601
1622
|
|
|
1602
1623
|
let finalPrice: string | number | undefined =
|
|
1603
1624
|
typeof price === 'number' || typeof price === 'string' ? price : undefined;
|
|
1604
|
-
if (!finalPrice) {
|
|
1625
|
+
if (!finalPrice && content) {
|
|
1605
1626
|
const priceMatch =
|
|
1606
|
-
|
|
1607
|
-
|
|
1627
|
+
content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) ||
|
|
1628
|
+
content.match(/\$\s*([\d,.]+)/);
|
|
1608
1629
|
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, '');
|
|
1609
1630
|
}
|
|
1610
1631
|
|
|
1632
|
+
if (finalPrice === undefined) {
|
|
1633
|
+
console.warn(
|
|
1634
|
+
`[UITransformer] Unable to read product price from metadata or content for item ${item.id}.`,
|
|
1635
|
+
{ metadataKeys: Object.keys(meta), contentSnippet: content.substring(0, 100) },
|
|
1636
|
+
);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1611
1639
|
const imageValue = this.getDynamicVal(meta, 'image', config, trainedSchema);
|
|
1640
|
+
if (!imageValue) {
|
|
1641
|
+
console.debug(
|
|
1642
|
+
`[UITransformer] Unable to read product image URL from metadata for item ${item.id}.`,
|
|
1643
|
+
);
|
|
1644
|
+
}
|
|
1645
|
+
|
|
1646
|
+
if (!description) {
|
|
1647
|
+
console.debug(
|
|
1648
|
+
`[UITransformer] Unable to read product description from metadata or content for item ${item.id}.`,
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1612
1651
|
|
|
1613
1652
|
return {
|
|
1614
1653
|
id: item.id,
|
|
1615
|
-
name: finalName,
|
|
1654
|
+
name: finalName || 'Unnamed Product',
|
|
1616
1655
|
price: finalPrice,
|
|
1617
1656
|
image: typeof imageValue === 'string' ? imageValue : undefined,
|
|
1618
1657
|
brand: brand ? String(brand) : undefined,
|
|
1619
1658
|
description,
|
|
1620
1659
|
inStock: this.determineStockStatus(item),
|
|
1621
1660
|
};
|
|
1661
|
+
} else {
|
|
1662
|
+
console.warn(
|
|
1663
|
+
`[UITransformer] Item ${item.id} skipped: unable to read name or identify as product data from metadata/content.`,
|
|
1664
|
+
{ metadataKeys: Object.keys(meta), contentSnippet: content.substring(0, 100) },
|
|
1665
|
+
);
|
|
1622
1666
|
}
|
|
1623
1667
|
|
|
1624
1668
|
return null;
|
|
@@ -1733,11 +1777,11 @@ RULES:
|
|
|
1733
1777
|
}
|
|
1734
1778
|
|
|
1735
1779
|
private static buildContextSummary(sources: VectorMatch[], maxChars = 6000): string {
|
|
1736
|
-
const items = sources.map((s, i) => ({
|
|
1780
|
+
const items = (sources || []).filter(Boolean).map((s, i) => ({
|
|
1737
1781
|
index: i + 1,
|
|
1738
|
-
content: s
|
|
1739
|
-
metadata: s
|
|
1740
|
-
score: s
|
|
1782
|
+
content: s?.content?.substring(0, 400) ?? '',
|
|
1783
|
+
metadata: s?.metadata ?? {},
|
|
1784
|
+
score: s?.score ?? 0,
|
|
1741
1785
|
}));
|
|
1742
1786
|
|
|
1743
1787
|
const full = JSON.stringify(items, null, 2);
|