@retrivora-ai/rag-engine 2.0.5 → 2.0.7
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 +268 -66
- package/dist/handlers/index.mjs +268 -66
- package/dist/index.js +79 -22
- package/dist/index.mjs +79 -22
- package/dist/server.js +268 -66
- package/dist/server.mjs +268 -66
- package/package.json +1 -1
- package/src/core/DatabaseStorage.ts +26 -4
- package/src/core/Pipeline.ts +61 -18
- package/src/core/QueryProcessor.ts +3 -3
- package/src/llm/providers/UniversalLLMAdapter.ts +83 -11
- 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 +62 -17
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
3
4
|
import { RagConfig } from '../config/RagConfig';
|
|
4
5
|
import { LicenseVerifier } from './LicenseVerifier';
|
|
5
6
|
|
|
@@ -35,7 +36,10 @@ export class DatabaseStorage {
|
|
|
35
36
|
private feedbackTableName: string;
|
|
36
37
|
|
|
37
38
|
// Filesystem fallback configuration
|
|
38
|
-
private
|
|
39
|
+
private isServerless = Boolean(process.env.VERCEL || process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.LAMBDA_TASK_ROOT);
|
|
40
|
+
private fallbackDir = this.isServerless
|
|
41
|
+
? path.join(os.tmpdir(), '.retrivora')
|
|
42
|
+
: path.join(process.cwd(), '.retrivora');
|
|
39
43
|
private historyFile = path.join(this.fallbackDir, 'history.json');
|
|
40
44
|
private feedbackFile = path.join(this.fallbackDir, 'feedback.json');
|
|
41
45
|
|
|
@@ -191,10 +195,28 @@ export class DatabaseStorage {
|
|
|
191
195
|
|
|
192
196
|
// Helper for FS fallback writes
|
|
193
197
|
private writeLocalFile(filePath: string, data: any[]): void {
|
|
194
|
-
|
|
195
|
-
|
|
198
|
+
try {
|
|
199
|
+
const dir = path.dirname(filePath);
|
|
200
|
+
if (!fs.existsSync(dir)) {
|
|
201
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
202
|
+
}
|
|
203
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
204
|
+
} catch (err: any) {
|
|
205
|
+
if (err.code === 'EROFS') {
|
|
206
|
+
try {
|
|
207
|
+
const tmpDir = path.join(os.tmpdir(), '.retrivora');
|
|
208
|
+
if (!fs.existsSync(tmpDir)) {
|
|
209
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
210
|
+
}
|
|
211
|
+
const tmpFilePath = path.join(tmpDir, path.basename(filePath));
|
|
212
|
+
fs.writeFileSync(tmpFilePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
213
|
+
} catch {
|
|
214
|
+
/* ignore write failure in read-only environment */
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
console.warn(`[DatabaseStorage] Failed to write local fallback file: ${err.message}`);
|
|
218
|
+
}
|
|
196
219
|
}
|
|
197
|
-
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
198
220
|
}
|
|
199
221
|
|
|
200
222
|
// ─── Message History Operations ──────────────────────────────────────────
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -583,9 +583,20 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
583
583
|
const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
|
|
584
584
|
const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
|
|
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;
|
|
@@ -593,14 +604,14 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
593
604
|
|
|
594
605
|
// 3. Reranking
|
|
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
|
|
610
|
+
: structuredSources.filter((m) => (m?.score ?? 0) >= scoreThreshold);
|
|
600
611
|
const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
|
|
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);
|
|
614
|
+
fullSources = (await this.reranker.rerank(fullSources, question, rerankLimit)).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'));
|
|
604
615
|
} else if (!wantsExhaustiveList) {
|
|
605
616
|
fullSources = fullSources.slice(0, topK);
|
|
606
617
|
}
|
|
@@ -608,7 +619,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
608
619
|
|
|
609
620
|
// 4. Context Augmentation - Review all retrieved/reranked sources for full comprehension
|
|
610
621
|
let context = fullSources.length
|
|
611
|
-
? fullSources.map((m, i) => `[Source ${i + 1}]\n${m
|
|
622
|
+
? fullSources.map((m, i) => `[Source ${i + 1}]\n${m?.content ?? ''}`).join('\n\n---\n\n')
|
|
612
623
|
: 'No relevant context found.';
|
|
613
624
|
|
|
614
625
|
// Count the records of relevant sources to pass into the slice dynamically
|
|
@@ -620,7 +631,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
620
631
|
} else {
|
|
621
632
|
// For general semantic queries, count the number of matches meeting high relevance threshold
|
|
622
633
|
const baseThreshold = Math.max(scoreThreshold, 0.4);
|
|
623
|
-
const highlyRelevant = fullSources.filter(m => m
|
|
634
|
+
const highlyRelevant = fullSources.filter(m => (m?.score ?? 0) >= baseThreshold);
|
|
624
635
|
displayCount = Math.max(highlyRelevant.length, topK);
|
|
625
636
|
// Cap non-metadata semantic searches to a maximum of 15 to keep the UI clean
|
|
626
637
|
if (displayCount > 15) {
|
|
@@ -630,9 +641,22 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
630
641
|
|
|
631
642
|
// Ensure they are sorted from highest to lowest score, then slice using our dynamic count
|
|
632
643
|
const sources = [...fullSources]
|
|
633
|
-
.
|
|
644
|
+
.filter((s): s is VectorMatch => Boolean(s && typeof s === 'object'))
|
|
645
|
+
.sort((a, b) => (b?.score ?? 0) - (a?.score ?? 0))
|
|
634
646
|
.slice(0, displayCount);
|
|
635
647
|
|
|
648
|
+
console.log('[Pipeline] Final sources for prompt & UI:', {
|
|
649
|
+
count: sources.length,
|
|
650
|
+
sources: sources.map((s, idx) => ({
|
|
651
|
+
rank: idx + 1,
|
|
652
|
+
id: s?.id,
|
|
653
|
+
score: s?.score,
|
|
654
|
+
contentType: typeof s?.content,
|
|
655
|
+
contentSnippet: String(s?.content ?? '').substring(0, 150),
|
|
656
|
+
metadata: s?.metadata,
|
|
657
|
+
})),
|
|
658
|
+
});
|
|
659
|
+
|
|
636
660
|
if (graphData && graphData.nodes.length > 0) {
|
|
637
661
|
const graphContext = graphData.nodes.map(n =>
|
|
638
662
|
`Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
|
|
@@ -653,17 +677,17 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
653
677
|
// Yield sources immediately to support early UI citation rendering (highly responsive)
|
|
654
678
|
yield {
|
|
655
679
|
reply: '',
|
|
656
|
-
sources: sources.map((s) => ({
|
|
680
|
+
sources: (sources || []).filter((s): s is VectorMatch => Boolean(s && typeof s === 'object')).map((s) => ({
|
|
657
681
|
id: s.id,
|
|
658
|
-
score: s.score,
|
|
659
|
-
content: s
|
|
660
|
-
metadata: s
|
|
682
|
+
score: s.score ?? 0,
|
|
683
|
+
content: s?.content ?? '',
|
|
684
|
+
metadata: s?.metadata ?? {},
|
|
661
685
|
namespace: ns,
|
|
662
686
|
} satisfies RetrievedChunk)),
|
|
663
687
|
} as ChatResponse;
|
|
664
688
|
|
|
665
689
|
// 4.5 Schema Training — true fire-and-forget, never gates anything.
|
|
666
|
-
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s
|
|
690
|
+
const allMetadataKeys = Array.from(new Set(sources.filter(Boolean).flatMap(s => Object.keys(s?.metadata || {}))));
|
|
667
691
|
const cachedSchema = allMetadataKeys.length > 0
|
|
668
692
|
? SchemaMapper.getCached(ns, allMetadataKeys)
|
|
669
693
|
: undefined;
|
|
@@ -722,7 +746,12 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
722
746
|
finalRestrictionSuffix += '\n\n(IMPORTANT: You must think step-by-step before answering. Write your thinking process inside a `<think>...</think>` block. Write the `<think>` block first, then follow it with your final response. Keep the thinking process thorough and detailed. Example: `<think>Thinking details here...</think>Final answer text here.`)';
|
|
723
747
|
}
|
|
724
748
|
|
|
725
|
-
const hardenedHistory = [
|
|
749
|
+
const hardenedHistory = (Array.isArray(history) ? history : [])
|
|
750
|
+
.filter((m): m is ChatMessage => Boolean(m && typeof m === 'object'))
|
|
751
|
+
.map((m) => ({
|
|
752
|
+
role: m.role || 'user',
|
|
753
|
+
content: typeof m.content === 'string' ? m.content : (m.content != null ? String(m.content) : ''),
|
|
754
|
+
}));
|
|
726
755
|
const userQuestion = { role: 'user', content: question + finalRestrictionSuffix } as ChatMessage;
|
|
727
756
|
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
728
757
|
|
|
@@ -880,11 +909,11 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
880
909
|
rewrittenQuery,
|
|
881
910
|
systemPrompt,
|
|
882
911
|
userPrompt: question + finalRestrictionSuffix,
|
|
883
|
-
chunks: sources.map((s) => ({
|
|
912
|
+
chunks: (sources || []).filter(Boolean).map((s) => ({
|
|
884
913
|
id: s.id,
|
|
885
914
|
score: s.score,
|
|
886
|
-
content: s
|
|
887
|
-
metadata: s
|
|
915
|
+
content: s?.content ?? '',
|
|
916
|
+
metadata: s?.metadata ?? {},
|
|
888
917
|
namespace: ns,
|
|
889
918
|
} satisfies RetrievedChunk)),
|
|
890
919
|
latency,
|
|
@@ -1010,6 +1039,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1010
1039
|
}
|
|
1011
1040
|
|
|
1012
1041
|
private resolveNumericPredicateValue(source: VectorMatch, predicate: NumericPredicate): number | null {
|
|
1042
|
+
if (!source) return null;
|
|
1013
1043
|
const meta = source.metadata || {};
|
|
1014
1044
|
const field = predicate.field;
|
|
1015
1045
|
const entries = Object.entries(meta)
|
|
@@ -1027,7 +1057,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1027
1057
|
if (value !== null) return value;
|
|
1028
1058
|
}
|
|
1029
1059
|
|
|
1030
|
-
const contentValue = this.extractNumericValueFromContent(source.content, field);
|
|
1060
|
+
const contentValue = this.extractNumericValueFromContent(source.content ?? '', field);
|
|
1031
1061
|
if (contentValue !== null) return contentValue;
|
|
1032
1062
|
}
|
|
1033
1063
|
|
|
@@ -1041,6 +1071,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1041
1071
|
}
|
|
1042
1072
|
|
|
1043
1073
|
private extractNumericValueFromContent(content: string, field: string): number | null {
|
|
1074
|
+
if (!content || typeof content !== 'string') return null;
|
|
1044
1075
|
const escapedWords = field
|
|
1045
1076
|
.split(/\s+|_+|-+/)
|
|
1046
1077
|
.filter(Boolean)
|
|
@@ -1159,6 +1190,18 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
1159
1190
|
}
|
|
1160
1191
|
}
|
|
1161
1192
|
|
|
1193
|
+
console.log('[Pipeline] retrieve() response:', {
|
|
1194
|
+
query,
|
|
1195
|
+
namespace: ns,
|
|
1196
|
+
count: resolvedSources.length,
|
|
1197
|
+
sample: resolvedSources.slice(0, 2).map((s) => ({
|
|
1198
|
+
id: s?.id,
|
|
1199
|
+
score: s?.score,
|
|
1200
|
+
contentSnippet: String(s?.content ?? '').substring(0, 150),
|
|
1201
|
+
metadata: s?.metadata,
|
|
1202
|
+
})),
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1162
1205
|
return { sources: resolvedSources, graphData };
|
|
1163
1206
|
}
|
|
1164
1207
|
|
|
@@ -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
|
}
|
|
@@ -94,6 +94,27 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
94
94
|
};
|
|
95
95
|
}
|
|
96
96
|
|
|
97
|
+
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
98
|
+
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
99
|
+
const _g = globalThis as any;
|
|
100
|
+
if (typeof _g.__retrivoraDispatchChat === 'function') {
|
|
101
|
+
try {
|
|
102
|
+
const res = await _g.__retrivoraDispatchChat({
|
|
103
|
+
model: this.model,
|
|
104
|
+
messages: formattedMessages as any,
|
|
105
|
+
max_tokens: this.maxTokens,
|
|
106
|
+
temperature: this.temperature,
|
|
107
|
+
}, this.apiKey);
|
|
108
|
+
|
|
109
|
+
if (res?.response?.choices?.[0]?.message?.content) {
|
|
110
|
+
return res.response.choices[0].message.content;
|
|
111
|
+
}
|
|
112
|
+
} catch (dispatchErr) {
|
|
113
|
+
throw dispatchErr;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
97
118
|
const { data } = await this.http.post(path, payload);
|
|
98
119
|
|
|
99
120
|
const extractPath = this.opts.responseExtractPath ?? 'choices[0].message.content';
|
|
@@ -145,22 +166,54 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
145
166
|
};
|
|
146
167
|
}
|
|
147
168
|
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
headers: this.resolvedHeaders,
|
|
151
|
-
body: JSON.stringify(payload),
|
|
152
|
-
});
|
|
169
|
+
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
170
|
+
let streamBody: ReadableStream<Uint8Array> | null = null;
|
|
153
171
|
|
|
154
|
-
if (
|
|
155
|
-
const
|
|
156
|
-
|
|
172
|
+
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
173
|
+
const _g = globalThis as any;
|
|
174
|
+
if (typeof _g.__retrivoraDispatchChat === 'function') {
|
|
175
|
+
try {
|
|
176
|
+
const res = await _g.__retrivoraDispatchChat({
|
|
177
|
+
model: this.model,
|
|
178
|
+
messages: formattedMessages as any,
|
|
179
|
+
max_tokens: this.maxTokens,
|
|
180
|
+
temperature: this.temperature,
|
|
181
|
+
stream: true,
|
|
182
|
+
}, this.apiKey);
|
|
183
|
+
|
|
184
|
+
if (res?.stream) {
|
|
185
|
+
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;
|
|
189
|
+
}
|
|
190
|
+
} 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
|
+
throw dispatchErr;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
157
196
|
}
|
|
158
197
|
|
|
159
|
-
if (!
|
|
160
|
-
|
|
198
|
+
if (!streamBody) {
|
|
199
|
+
const response = await fetch(url, {
|
|
200
|
+
method: 'POST',
|
|
201
|
+
headers: this.resolvedHeaders,
|
|
202
|
+
body: JSON.stringify(payload),
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
if (!response.ok) {
|
|
206
|
+
const errorText = await response.text().catch(() => response.statusText);
|
|
207
|
+
throw new Error(`[UniversalLLMAdapter] Streaming request failed (${response.status}): ${errorText}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!response.body) {
|
|
211
|
+
throw new Error('[UniversalLLMAdapter] Response body is null — server did not send a streaming response.');
|
|
212
|
+
}
|
|
213
|
+
streamBody = response.body;
|
|
161
214
|
}
|
|
162
215
|
|
|
163
|
-
const reader =
|
|
216
|
+
const reader = streamBody.getReader();
|
|
164
217
|
const decoder = new TextDecoder('utf-8');
|
|
165
218
|
let buffer = '';
|
|
166
219
|
|
|
@@ -218,6 +271,25 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
218
271
|
};
|
|
219
272
|
}
|
|
220
273
|
|
|
274
|
+
const isSelfHost = this.baseUrl.includes('retrivora.com') || this.baseUrl.includes('localhost');
|
|
275
|
+
if (isSelfHost || Boolean(process.env.VERCEL)) {
|
|
276
|
+
const _g = globalThis as any;
|
|
277
|
+
if (typeof _g.__retrivoraDispatchEmbedding === 'function') {
|
|
278
|
+
try {
|
|
279
|
+
const res = await _g.__retrivoraDispatchEmbedding({
|
|
280
|
+
model: this.model,
|
|
281
|
+
input: text,
|
|
282
|
+
}, this.apiKey);
|
|
283
|
+
|
|
284
|
+
if (res?.data?.[0]?.embedding) {
|
|
285
|
+
return res.data[0].embedding;
|
|
286
|
+
}
|
|
287
|
+
} catch (dispatchErr) {
|
|
288
|
+
throw dispatchErr;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
221
293
|
const { data } = await this.http.post(path, payload);
|
|
222
294
|
|
|
223
295
|
const extractPath = this.opts.embedExtractPath ?? 'data[0].embedding';
|
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
|
|
@@ -760,7 +771,7 @@ RULES:
|
|
|
760
771
|
type: 'radar_chart' as VisualizationType,
|
|
761
772
|
title: 'Product Comparison',
|
|
762
773
|
description: `Comparing ${data.length} items across ${radarData.length} attributes`,
|
|
763
|
-
data: radarData.length > 0 ? radarData : data.map(d => ({ attribute: d
|
|
774
|
+
data: radarData.length > 0 ? radarData : data.map(d => ({ attribute: (d?.content ?? '').substring(0, 40) })),
|
|
764
775
|
};
|
|
765
776
|
}
|
|
766
777
|
|
|
@@ -780,7 +791,7 @@ RULES:
|
|
|
780
791
|
private static transformToText(data: VectorMatch[]): UITransformationResponse {
|
|
781
792
|
return this.createTextResponse(
|
|
782
793
|
'Retrieved Context',
|
|
783
|
-
data.map(item => item
|
|
794
|
+
data.map(item => item?.content ?? '').join('\n\n'),
|
|
784
795
|
`Found ${data.length} relevant results`,
|
|
785
796
|
);
|
|
786
797
|
}
|
|
@@ -983,6 +994,7 @@ RULES:
|
|
|
983
994
|
|
|
984
995
|
|
|
985
996
|
private static isProductData(item: VectorMatch): boolean {
|
|
997
|
+
if (!item) return false;
|
|
986
998
|
const content = (item.content || '').toLowerCase();
|
|
987
999
|
const productKeywords = [
|
|
988
1000
|
'product', 'price', 'stock', 'sku', 'brand', 'msrp',
|
|
@@ -1297,7 +1309,7 @@ RULES:
|
|
|
1297
1309
|
return {
|
|
1298
1310
|
timestamp: (meta.timestamp ?? meta.date ?? new Date().toISOString()) as string | number,
|
|
1299
1311
|
value: (meta.value ?? item.score ?? 0) as number,
|
|
1300
|
-
label: (meta.label ?? item
|
|
1312
|
+
label: (meta.label ?? (item?.content ?? '').substring(0, 50)) as string,
|
|
1301
1313
|
};
|
|
1302
1314
|
});
|
|
1303
1315
|
}
|
|
@@ -1429,7 +1441,7 @@ RULES:
|
|
|
1429
1441
|
}
|
|
1430
1442
|
|
|
1431
1443
|
private static resolveTableCellValue(item: VectorMatch, column: string): string | number | boolean {
|
|
1432
|
-
if (column === 'Content') return item
|
|
1444
|
+
if (column === 'Content') return (item?.content ?? '').substring(0, 100);
|
|
1433
1445
|
|
|
1434
1446
|
const meta = item.metadata || {};
|
|
1435
1447
|
const normalizedColumn = this.normalizeComparableField(column);
|
|
@@ -1444,10 +1456,10 @@ RULES:
|
|
|
1444
1456
|
const aliasValue = this.resolveAliasedTableCell(meta, column);
|
|
1445
1457
|
if (aliasValue !== undefined) return this.toDisplayValue(aliasValue);
|
|
1446
1458
|
|
|
1447
|
-
const contentValue = this.extractContentFieldValue(item
|
|
1459
|
+
const contentValue = this.extractContentFieldValue(item?.content ?? '', column);
|
|
1448
1460
|
if (contentValue !== null) return contentValue;
|
|
1449
1461
|
|
|
1450
|
-
const aliasContentValue = this.extractAliasedContentFieldValue(item
|
|
1462
|
+
const aliasContentValue = this.extractAliasedContentFieldValue(item?.content ?? '', column);
|
|
1451
1463
|
return aliasContentValue ?? '';
|
|
1452
1464
|
}
|
|
1453
1465
|
|
|
@@ -1519,6 +1531,7 @@ RULES:
|
|
|
1519
1531
|
}
|
|
1520
1532
|
|
|
1521
1533
|
private static determineStockStatus(item: VectorMatch): boolean {
|
|
1534
|
+
if (!item) return true;
|
|
1522
1535
|
const meta = item.metadata || {};
|
|
1523
1536
|
const stockValue = resolveMetadataValue(meta, 'stock');
|
|
1524
1537
|
if (stockValue !== undefined) {
|
|
@@ -1583,42 +1596,74 @@ RULES:
|
|
|
1583
1596
|
config?: import('../config/RagConfig').RagConfig,
|
|
1584
1597
|
trainedSchema?: import('./SchemaMapper').SchemaMap,
|
|
1585
1598
|
): CarouselProduct | null {
|
|
1599
|
+
if (!item) return null;
|
|
1586
1600
|
const meta = item.metadata || {};
|
|
1601
|
+
const content = item.content || '';
|
|
1587
1602
|
const name = this.getDynamicVal(meta, 'name', config, trainedSchema);
|
|
1588
1603
|
const price = this.getDynamicVal(meta, 'price', config, trainedSchema);
|
|
1589
1604
|
const brand = this.getDynamicVal(meta, 'brand', config, trainedSchema);
|
|
1590
1605
|
const description = this.cleanProductDescription(
|
|
1591
|
-
this.extractProductDescriptionFromContent(
|
|
1606
|
+
this.extractProductDescriptionFromContent(content) ??
|
|
1592
1607
|
this.getProductDescriptionValue(meta, config, trainedSchema),
|
|
1593
1608
|
);
|
|
1594
1609
|
|
|
1595
1610
|
if (name || this.isProductData(item)) {
|
|
1596
1611
|
let finalName = name ? String(name) : undefined;
|
|
1612
|
+
if (!finalName && content) {
|
|
1613
|
+
const nameMatch = content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
|
|
1614
|
+
finalName = nameMatch ? nameMatch[1].trim() : content.split('\n')[0]?.substring(0, 60);
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1597
1617
|
if (!finalName) {
|
|
1598
|
-
|
|
1599
|
-
|
|
1618
|
+
console.warn(
|
|
1619
|
+
`[UITransformer] Unable to read product name from metadata or content for item ${item.id}. Defaulting to 'Unnamed Product'.`,
|
|
1620
|
+
{ metadataKeys: Object.keys(meta), contentLength: content.length },
|
|
1621
|
+
);
|
|
1600
1622
|
}
|
|
1601
1623
|
|
|
1602
1624
|
let finalPrice: string | number | undefined =
|
|
1603
1625
|
typeof price === 'number' || typeof price === 'string' ? price : undefined;
|
|
1604
|
-
if (!finalPrice) {
|
|
1626
|
+
if (!finalPrice && content) {
|
|
1605
1627
|
const priceMatch =
|
|
1606
|
-
|
|
1607
|
-
|
|
1628
|
+
content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) ||
|
|
1629
|
+
content.match(/\$\s*([\d,.]+)/);
|
|
1608
1630
|
if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, '');
|
|
1609
1631
|
}
|
|
1610
1632
|
|
|
1633
|
+
if (finalPrice === undefined) {
|
|
1634
|
+
console.warn(
|
|
1635
|
+
`[UITransformer] Unable to read product price from metadata or content for item ${item.id}.`,
|
|
1636
|
+
{ metadataKeys: Object.keys(meta), contentSnippet: content.substring(0, 100) },
|
|
1637
|
+
);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1611
1640
|
const imageValue = this.getDynamicVal(meta, 'image', config, trainedSchema);
|
|
1641
|
+
if (!imageValue) {
|
|
1642
|
+
console.debug(
|
|
1643
|
+
`[UITransformer] Unable to read product image URL from metadata for item ${item.id}.`,
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
if (!description) {
|
|
1648
|
+
console.debug(
|
|
1649
|
+
`[UITransformer] Unable to read product description from metadata or content for item ${item.id}.`,
|
|
1650
|
+
);
|
|
1651
|
+
}
|
|
1612
1652
|
|
|
1613
1653
|
return {
|
|
1614
1654
|
id: item.id,
|
|
1615
|
-
name: finalName,
|
|
1655
|
+
name: finalName || 'Unnamed Product',
|
|
1616
1656
|
price: finalPrice,
|
|
1617
1657
|
image: typeof imageValue === 'string' ? imageValue : undefined,
|
|
1618
1658
|
brand: brand ? String(brand) : undefined,
|
|
1619
1659
|
description,
|
|
1620
1660
|
inStock: this.determineStockStatus(item),
|
|
1621
1661
|
};
|
|
1662
|
+
} else {
|
|
1663
|
+
console.warn(
|
|
1664
|
+
`[UITransformer] Item ${item.id} skipped: unable to read name or identify as product data from metadata/content.`,
|
|
1665
|
+
{ metadataKeys: Object.keys(meta), contentSnippet: content.substring(0, 100) },
|
|
1666
|
+
);
|
|
1622
1667
|
}
|
|
1623
1668
|
|
|
1624
1669
|
return null;
|
|
@@ -1733,11 +1778,11 @@ RULES:
|
|
|
1733
1778
|
}
|
|
1734
1779
|
|
|
1735
1780
|
private static buildContextSummary(sources: VectorMatch[], maxChars = 6000): string {
|
|
1736
|
-
const items = sources.map((s, i) => ({
|
|
1781
|
+
const items = (sources || []).filter(Boolean).map((s, i) => ({
|
|
1737
1782
|
index: i + 1,
|
|
1738
|
-
content: s
|
|
1739
|
-
metadata: s
|
|
1740
|
-
score: s
|
|
1783
|
+
content: s?.content?.substring(0, 400) ?? '',
|
|
1784
|
+
metadata: s?.metadata ?? {},
|
|
1785
|
+
score: s?.score ?? 0,
|
|
1741
1786
|
}));
|
|
1742
1787
|
|
|
1743
1788
|
const full = JSON.stringify(items, null, 2);
|