@retrivora-ai/rag-engine 1.9.1 → 1.9.3
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/{ILLMProvider-BQyKZLnd.d.mts → ILLMProvider-Bw2A28nU.d.mts} +2 -0
- package/dist/{ILLMProvider-BQyKZLnd.d.ts → ILLMProvider-Bw2A28nU.d.ts} +2 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +193 -109
- package/dist/handlers/index.mjs +193 -109
- package/dist/{index-CDftK3qs.d.mts → index-B70ZLkfG.d.mts} +1 -1
- package/dist/{index-A0GqPsaG.d.ts → index-DVu-mkAM.d.ts} +1 -1
- package/dist/index.css +57 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +90 -36
- package/dist/index.mjs +91 -37
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/server.js +193 -109
- package/dist/server.mjs +193 -109
- package/package.json +1 -1
- package/src/components/ChatWindow.tsx +24 -14
- package/src/components/MessageBubble.tsx +41 -6
- package/src/components/ProductCard.tsx +4 -4
- package/src/core/Pipeline.ts +53 -25
- package/src/core/QueryProcessor.ts +10 -9
- package/src/handlers/index.ts +31 -8
- package/src/hooks/useRagChat.ts +24 -0
- package/src/providers/vectordb/MongoDBProvider.ts +13 -6
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +36 -4
- package/src/types/chat.ts +2 -0
- package/src/utils/SchemaMapper.ts +6 -4
- package/src/utils/UITransformer.ts +2 -1
- package/src/utils/synonyms.ts +5 -5
package/src/core/Pipeline.ts
CHANGED
|
@@ -354,7 +354,10 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
354
354
|
);
|
|
355
355
|
|
|
356
356
|
if (upsertResult.errors.length > 0) {
|
|
357
|
-
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed
|
|
357
|
+
console.warn(`[Pipeline] Ingestion: ${upsertResult.errors.length} batches failed. Error details:`);
|
|
358
|
+
upsertResult.errors.forEach((err, idx) => {
|
|
359
|
+
console.warn(` Batch ${idx + 1} Error: ${err.error.message || String(err.error)}`);
|
|
360
|
+
});
|
|
358
361
|
}
|
|
359
362
|
|
|
360
363
|
return upsertResult.totalProcessed;
|
|
@@ -482,8 +485,9 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
482
485
|
? await this.graphDB.query(searchQuery)
|
|
483
486
|
: undefined;
|
|
484
487
|
|
|
488
|
+
const hasMetadataFilter = filter.metadata && Object.keys(filter.metadata).length > 0;
|
|
485
489
|
const hasNumericPredicates = Array.isArray(filter.__numericPredicates) && filter.__numericPredicates.length > 0;
|
|
486
|
-
const wantsExhaustiveList = hasNumericPredicates || /\b(list|all|show|provide|give|get)\b/i.test(question);
|
|
490
|
+
const wantsExhaustiveList = hasNumericPredicates || hasMetadataFilter || /\b(list|all|show|provide|give|get|browse|find|search|display)\b/i.test(question);
|
|
487
491
|
const retrievalLimit = wantsExhaustiveList ? Math.max(topK * 20, 100) : topK * 2;
|
|
488
492
|
|
|
489
493
|
const rawSources = (strategyResult === 'vector' || strategyResult === 'both') && queryVector && queryVector.length > 0
|
|
@@ -497,21 +501,44 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
497
501
|
// 3. Reranking
|
|
498
502
|
const rerankStart = performance.now();
|
|
499
503
|
const structuredSources = this.applyStructuredFilters(rawSources, filter);
|
|
500
|
-
let
|
|
504
|
+
let fullSources = hasNumericPredicates
|
|
501
505
|
? structuredSources
|
|
502
506
|
: structuredSources.filter((m) => m.score >= scoreThreshold);
|
|
507
|
+
const rerankLimit = wantsExhaustiveList ? retrievalLimit : topK;
|
|
503
508
|
if (!hasNumericPredicates && this.config.rag?.useReranking) {
|
|
504
|
-
|
|
509
|
+
fullSources = await this.reranker.rerank(fullSources, question, rerankLimit);
|
|
505
510
|
} else if (!wantsExhaustiveList) {
|
|
506
|
-
|
|
511
|
+
fullSources = fullSources.slice(0, topK);
|
|
507
512
|
}
|
|
508
513
|
const rerankMs = performance.now() - rerankStart;
|
|
509
514
|
|
|
510
|
-
// 4. Context Augmentation
|
|
511
|
-
let context =
|
|
512
|
-
?
|
|
515
|
+
// 4. Context Augmentation - Review all retrieved/reranked sources for full comprehension
|
|
516
|
+
let context = fullSources.length
|
|
517
|
+
? fullSources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
|
|
513
518
|
: 'No relevant context found.';
|
|
514
519
|
|
|
520
|
+
// Count the records of relevant sources to pass into the slice dynamically
|
|
521
|
+
let displayCount = 15; // default fallback
|
|
522
|
+
|
|
523
|
+
if (hasMetadataFilter) {
|
|
524
|
+
// If a metadata filter matches (e.g. brand, category), all of those matches are highly relevant
|
|
525
|
+
displayCount = fullSources.length;
|
|
526
|
+
} else {
|
|
527
|
+
// For general semantic queries, count the number of matches meeting high relevance threshold
|
|
528
|
+
const baseThreshold = Math.max(scoreThreshold, 0.4);
|
|
529
|
+
const highlyRelevant = fullSources.filter(m => m.score >= baseThreshold);
|
|
530
|
+
displayCount = Math.max(highlyRelevant.length, topK);
|
|
531
|
+
// Cap non-metadata semantic searches to a maximum of 15 to keep the UI clean
|
|
532
|
+
if (displayCount > 15) {
|
|
533
|
+
displayCount = 15;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Ensure they are sorted from highest to lowest score, then slice using our dynamic count
|
|
538
|
+
const sources = [...fullSources]
|
|
539
|
+
.sort((a, b) => b.score - a.score)
|
|
540
|
+
.slice(0, displayCount);
|
|
541
|
+
|
|
515
542
|
if (graphData && graphData.nodes.length > 0) {
|
|
516
543
|
const graphContext = graphData.nodes.map(n =>
|
|
517
544
|
`Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
|
|
@@ -525,8 +552,21 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
525
552
|
? SchemaMapper.train(this.llmProvider, ns, allMetadataKeys).catch(() => undefined)
|
|
526
553
|
: Promise.resolve(undefined);
|
|
527
554
|
|
|
555
|
+
// 4.6 Start UI Transformation concurrently with generation to drastically reduce latency
|
|
556
|
+
const uiTransformationPromise = trainedSchemaPromise.then(trainedSchema =>
|
|
557
|
+
this.generateUiTransformation(
|
|
558
|
+
question,
|
|
559
|
+
sources,
|
|
560
|
+
trainedSchema,
|
|
561
|
+
hasNumericPredicates,
|
|
562
|
+
)
|
|
563
|
+
).catch(uiError => {
|
|
564
|
+
console.warn('[Pipeline] UI transformation failed concurrently:', uiError);
|
|
565
|
+
return UITransformer.transform(question, sources, this.config);
|
|
566
|
+
});
|
|
567
|
+
|
|
528
568
|
// 5. Generation (Streaming)
|
|
529
|
-
const restrictionSuffix = '\n\n(IMPORTANT: Use
|
|
569
|
+
const restrictionSuffix = '\n\n(IMPORTANT: Format your response beautifully using rich Markdown! Use bolding for emphasis, headings (##) for structure, bullet points for lists, and proper line breaks between paragraphs to make it highly readable. However, NEVER generate Markdown tables, HTML figures, or text charts (the UI renders requested charts separately, so NEVER say you cannot create, display, draw, render, or provide a chart/visualization). If listing products, use simple markdown bullet points or comma-separated names. NEVER use plus signs (+) to separate product names, category names, or list items. For product description/detail questions, provide customer-facing prose only and do NOT list internal catalog fields such as Handle, Body (HTML), Vendor, Type, Tags, Published, Option, Variant, SKU, or raw metadata labels. You CAN use numbers for years/counts like 2006 or 5800, but NEVER put a dollar sign ($) before them.)';
|
|
530
570
|
const hardenedHistory = [...history];
|
|
531
571
|
const userQuestion = { role: 'user', content: question + restrictionSuffix } as ChatMessage;
|
|
532
572
|
const messages: ChatMessage[] = [...hardenedHistory, userQuestion];
|
|
@@ -592,19 +632,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
592
632
|
timestamp: new Date().toISOString(),
|
|
593
633
|
};
|
|
594
634
|
|
|
595
|
-
|
|
596
|
-
try {
|
|
597
|
-
const trainedSchema = await trainedSchemaPromise;
|
|
598
|
-
uiTransformation = await this.generateUiTransformation(
|
|
599
|
-
question,
|
|
600
|
-
sources,
|
|
601
|
-
trainedSchema,
|
|
602
|
-
hasNumericPredicates,
|
|
603
|
-
);
|
|
604
|
-
} catch (uiError) {
|
|
605
|
-
console.warn('[Pipeline] UI transformation failed before metadata yield:', uiError);
|
|
606
|
-
uiTransformation = UITransformer.transform(question, sources, this.config);
|
|
607
|
-
}
|
|
635
|
+
const uiTransformation = await uiTransformationPromise;
|
|
608
636
|
|
|
609
637
|
yield {
|
|
610
638
|
reply: '',
|
|
@@ -804,9 +832,9 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
804
832
|
|
|
805
833
|
const sources = (strategy === 'vector' || strategy === 'both') && queryVector && queryVector.length > 0
|
|
806
834
|
? this.applyStructuredFilters(
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
835
|
+
await this.vectorDB.query(queryVector, retrievalLimit, ns, baseFilter),
|
|
836
|
+
baseFilter,
|
|
837
|
+
)
|
|
810
838
|
: [];
|
|
811
839
|
|
|
812
840
|
// Multi-Vector Support: Check for parent_id in metadata
|
|
@@ -156,6 +156,7 @@ export class QueryProcessor {
|
|
|
156
156
|
new RegExp(`\\b${fieldPattern}\\s*(?:=|:)\\s*["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
157
157
|
new RegExp(`\\b${fieldPattern}\\s+(?:is|are|was|were|equals?|equal to|named|called)\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
158
158
|
new RegExp(`\\bwith\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
159
|
+
new RegExp(`\\b(?:under|in|for|from|of)\\s+${fieldPattern}\\s+["']?${valuePattern}["']?(?=[?.!,]|$)`, 'gi'),
|
|
159
160
|
];
|
|
160
161
|
|
|
161
162
|
for (const pattern of fieldValuePatterns) {
|
|
@@ -301,12 +302,12 @@ export class QueryProcessor {
|
|
|
301
302
|
* Determines the retrieval strategy based on the query content.
|
|
302
303
|
*/
|
|
303
304
|
static async determineRetrievalStrategy(
|
|
304
|
-
query: string,
|
|
305
|
+
query: string,
|
|
305
306
|
llm?: import('../llm/ILLMProvider').ILLMProvider,
|
|
306
307
|
customGraphKeywords?: string[],
|
|
307
308
|
customVectorKeywords?: string[]
|
|
308
309
|
): Promise<'vector' | 'graph' | 'both'> {
|
|
309
|
-
|
|
310
|
+
|
|
310
311
|
if (llm) {
|
|
311
312
|
try {
|
|
312
313
|
const prompt = `You are a routing expert in a RAG system.
|
|
@@ -336,29 +337,29 @@ Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
|
|
|
336
337
|
|
|
337
338
|
// Fallback to keywords
|
|
338
339
|
const normalized = query.toLowerCase();
|
|
339
|
-
|
|
340
|
+
|
|
340
341
|
// Graph keywords
|
|
341
342
|
const graphKeywords = customGraphKeywords || [
|
|
342
|
-
'relationship', 'connect', 'link', 'network', 'friend', 'colleague',
|
|
343
|
+
'relationship', 'connect', 'link', 'network', 'friend', 'colleague',
|
|
343
344
|
'manager', 'hierarchy', 'multi-hop', 'shortest path', 'between',
|
|
344
345
|
'related to', 'associated with', 'belongs to', 'part of'
|
|
345
346
|
];
|
|
346
|
-
|
|
347
|
+
|
|
347
348
|
const hasGraphKeyword = graphKeywords.some(kw => normalized.includes(kw));
|
|
348
|
-
|
|
349
|
+
|
|
349
350
|
// Vector keywords (usually generic search or content specific)
|
|
350
351
|
const vectorKeywords = customVectorKeywords || [
|
|
351
352
|
'find', 'search', 'tell me about', 'what is', 'how to', 'documents about'
|
|
352
353
|
];
|
|
353
|
-
|
|
354
|
+
|
|
354
355
|
const hasVectorKeyword = vectorKeywords.some(kw => normalized.includes(kw));
|
|
355
|
-
|
|
356
|
+
|
|
356
357
|
if (hasGraphKeyword && !hasVectorKeyword) {
|
|
357
358
|
return 'graph';
|
|
358
359
|
} else if (hasGraphKeyword && hasVectorKeyword) {
|
|
359
360
|
return 'both';
|
|
360
361
|
}
|
|
361
|
-
|
|
362
|
+
|
|
362
363
|
return 'vector';
|
|
363
364
|
}
|
|
364
365
|
}
|
package/src/handlers/index.ts
CHANGED
|
@@ -142,15 +142,24 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const encoder = new TextEncoder();
|
|
145
|
+
let isActive = true;
|
|
145
146
|
|
|
146
147
|
const stream = new ReadableStream({
|
|
147
148
|
async start(controller) {
|
|
148
|
-
const enqueue = (text: string) =>
|
|
149
|
+
const enqueue = (text: string) => {
|
|
150
|
+
if (!isActive) return;
|
|
151
|
+
try {
|
|
152
|
+
controller.enqueue(encoder.encode(text));
|
|
153
|
+
} catch (err) {
|
|
154
|
+
console.warn('[createStreamHandler] Failed to enqueue (stream already closed):', err);
|
|
155
|
+
}
|
|
156
|
+
};
|
|
149
157
|
|
|
150
158
|
try {
|
|
151
159
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
152
160
|
|
|
153
161
|
for await (const chunk of pipelineStream) {
|
|
162
|
+
if (!isActive) break;
|
|
154
163
|
if (typeof chunk === 'string') {
|
|
155
164
|
enqueue(sseTextFrame(chunk));
|
|
156
165
|
} else {
|
|
@@ -166,7 +175,6 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
166
175
|
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
167
176
|
}
|
|
168
177
|
|
|
169
|
-
|
|
170
178
|
if (sources.length > 0) {
|
|
171
179
|
try {
|
|
172
180
|
// The pipeline already computed ui_transformation inside askStream().
|
|
@@ -187,18 +195,31 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
187
195
|
} catch { /* silent */ }
|
|
188
196
|
}
|
|
189
197
|
}
|
|
190
|
-
|
|
191
198
|
}
|
|
192
199
|
}
|
|
193
200
|
} catch (streamError) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
// Do not log or enqueue errors if the client intentionally cancelled/aborted
|
|
202
|
+
if (isActive) {
|
|
203
|
+
const errorMessage =
|
|
204
|
+
streamError instanceof Error ? streamError.message : String(streamError);
|
|
205
|
+
console.error('[createStreamHandler] Stream error:', streamError);
|
|
206
|
+
try {
|
|
207
|
+
enqueue(sseErrorFrame(errorMessage));
|
|
208
|
+
} catch { /* silent */ }
|
|
209
|
+
}
|
|
198
210
|
} finally {
|
|
199
|
-
|
|
211
|
+
if (isActive) {
|
|
212
|
+
isActive = false;
|
|
213
|
+
try {
|
|
214
|
+
controller.close();
|
|
215
|
+
} catch { /* silent */ }
|
|
216
|
+
}
|
|
200
217
|
}
|
|
201
218
|
},
|
|
219
|
+
cancel(reason) {
|
|
220
|
+
isActive = false;
|
|
221
|
+
console.log('[createStreamHandler] Stream connection closed by client:', reason);
|
|
222
|
+
}
|
|
202
223
|
});
|
|
203
224
|
|
|
204
225
|
return new Response(stream, { headers: SSE_HEADERS });
|
|
@@ -292,6 +313,7 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
292
313
|
if (parsed.data && parsed.data.length > 0) {
|
|
293
314
|
let i = 0;
|
|
294
315
|
let lastRowData: Record<string, string> | null = null;
|
|
316
|
+
const csvCols = Object.keys(parsed.data[0] as Record<string, string>);
|
|
295
317
|
|
|
296
318
|
for (const row of parsed.data) {
|
|
297
319
|
i++;
|
|
@@ -328,6 +350,7 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
328
350
|
fileType: file.type,
|
|
329
351
|
uploadedAt: new Date().toISOString(),
|
|
330
352
|
...(dimension ? { dimension } : {}),
|
|
353
|
+
csvHeaders: csvCols,
|
|
331
354
|
...rowData
|
|
332
355
|
}
|
|
333
356
|
});
|
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -74,6 +74,7 @@ export function useRagChat(
|
|
|
74
74
|
const [error, setError] = useState<string | null>(null);
|
|
75
75
|
const lastInputRef = useRef<string>('');
|
|
76
76
|
const messagesRef = useRef<RagMessage[]>(messages);
|
|
77
|
+
const abortControllerRef = useRef<AbortController | null>(null);
|
|
77
78
|
|
|
78
79
|
useEffect(() => {
|
|
79
80
|
messagesRef.current = messages;
|
|
@@ -100,12 +101,21 @@ export function useRagChat(
|
|
|
100
101
|
setError(null);
|
|
101
102
|
setIsLoading(true);
|
|
102
103
|
|
|
104
|
+
// Abort any existing call
|
|
105
|
+
if (abortControllerRef.current) {
|
|
106
|
+
abortControllerRef.current.abort();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
abortControllerRef.current = controller;
|
|
111
|
+
|
|
103
112
|
try {
|
|
104
113
|
const history = messagesRef.current.map(({ role, content }) => ({ role, content }));
|
|
105
114
|
|
|
106
115
|
const response = await fetch(apiUrl, {
|
|
107
116
|
method: 'POST',
|
|
108
117
|
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
signal: controller.signal,
|
|
109
119
|
body: JSON.stringify({
|
|
110
120
|
message: trimmed,
|
|
111
121
|
history,
|
|
@@ -245,11 +255,16 @@ export function useRagChat(
|
|
|
245
255
|
);
|
|
246
256
|
onReply?.(finalMsg);
|
|
247
257
|
} catch (err) {
|
|
258
|
+
if (err instanceof Error && err.name === 'AbortError') {
|
|
259
|
+
console.log('[useRagChat] Streaming stopped by user.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
248
262
|
const msg = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
|
|
249
263
|
setError(msg);
|
|
250
264
|
onError?.(msg);
|
|
251
265
|
} finally {
|
|
252
266
|
setIsLoading(false);
|
|
267
|
+
abortControllerRef.current = null;
|
|
253
268
|
}
|
|
254
269
|
},
|
|
255
270
|
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages]
|
|
@@ -270,6 +285,14 @@ export function useRagChat(
|
|
|
270
285
|
}
|
|
271
286
|
}, [sendMessage]);
|
|
272
287
|
|
|
288
|
+
const stop = useCallback(() => {
|
|
289
|
+
if (abortControllerRef.current) {
|
|
290
|
+
abortControllerRef.current.abort();
|
|
291
|
+
abortControllerRef.current = null;
|
|
292
|
+
}
|
|
293
|
+
setIsLoading(false);
|
|
294
|
+
}, []);
|
|
295
|
+
|
|
273
296
|
return {
|
|
274
297
|
messages,
|
|
275
298
|
isLoading,
|
|
@@ -278,5 +301,6 @@ export function useRagChat(
|
|
|
278
301
|
clear,
|
|
279
302
|
retry,
|
|
280
303
|
setMessages,
|
|
304
|
+
stop,
|
|
281
305
|
};
|
|
282
306
|
}
|
|
@@ -191,12 +191,19 @@ export class MongoDBProvider extends BaseVectorProvider {
|
|
|
191
191
|
|
|
192
192
|
const results = await this.collection!.aggregate(pipeline).toArray();
|
|
193
193
|
|
|
194
|
-
return (results as unknown as Array<{ _id: string; score: number; [key: string]: unknown }>).map((res) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
return (results as unknown as Array<{ _id: string; score: number; [key: string]: unknown }>).map((res) => {
|
|
195
|
+
// Normalize MongoDB's vectorSearchScore (which is shifted to [0, 1] e.g. (1+cos)/2)
|
|
196
|
+
// back to the standard [-1, 1] cosine space. This ensures the global relevance
|
|
197
|
+
// threshold (e.g. 0.4) properly filters out random orthogonal matches.
|
|
198
|
+
const normalizedScore = (res.score * 2) - 1;
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
id: res._id,
|
|
202
|
+
content: res[this.contentKey] as string,
|
|
203
|
+
metadata: (res[this.metadataKey] as Record<string, unknown>) || {},
|
|
204
|
+
score: normalizedScore,
|
|
205
|
+
};
|
|
206
|
+
});
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
async delete(id: string | number, namespace?: string): Promise<void> {
|
|
@@ -5,6 +5,7 @@ import { Pool, PoolClient } from 'pg';
|
|
|
5
5
|
import { BaseVectorProvider } from './BaseVectorProvider';
|
|
6
6
|
import { VectorMatch, UpsertDocument } from '../../types';
|
|
7
7
|
import { VectorDBConfig } from '../../config/RagConfig';
|
|
8
|
+
import { FIELD_SYNONYMS } from '../../utils/synonyms';
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
@@ -129,8 +130,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
129
130
|
|
|
130
131
|
// Extract headers from the first doc's metadata
|
|
131
132
|
const firstMeta = fileDocs[0].metadata || {};
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
let csvHeaders: string[] = [];
|
|
134
|
+
if (Array.isArray(firstMeta.csvHeaders)) {
|
|
135
|
+
csvHeaders = firstMeta.csvHeaders;
|
|
136
|
+
} else {
|
|
137
|
+
const systemKeys = ['fileName', 'fileSize', 'fileType', 'uploadedAt', 'dimension', 'chunkIndex', 'id', 'namespace', 'content', 'metadata', 'embedding', 'docId', 'docid', 'chunkId', 'chunkid', 'csvHeaders', 'csvheaders'];
|
|
138
|
+
csvHeaders = Object.keys(firstMeta).filter(k => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
|
|
139
|
+
}
|
|
134
140
|
|
|
135
141
|
// 1. Create Table Dynamically
|
|
136
142
|
const columnDefs = csvHeaders.map(h => `"${h}" TEXT`).join(',\n ');
|
|
@@ -269,10 +275,34 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
269
275
|
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
270
276
|
const tableLimit = Math.max(topK, 50);
|
|
271
277
|
|
|
278
|
+
const metadataFilters = _filter?.metadata as Record<string, string> | undefined;
|
|
279
|
+
|
|
272
280
|
const queryPromises = this.tables.map(async (table) => {
|
|
273
281
|
try {
|
|
274
282
|
let sqlQuery = '';
|
|
275
283
|
let params: unknown[] = [];
|
|
284
|
+
let whereClause = '';
|
|
285
|
+
const filterParams: unknown[] = [];
|
|
286
|
+
|
|
287
|
+
// Build case-insensitive SQL metadata filter conditions with synonym-awareness
|
|
288
|
+
if (metadataFilters && Object.keys(metadataFilters).length > 0) {
|
|
289
|
+
const conditions = Object.entries(metadataFilters)
|
|
290
|
+
.map(([key, val]) => {
|
|
291
|
+
filterParams.push(String(val));
|
|
292
|
+
// Offset based on whether it is vector + keyword or vector only
|
|
293
|
+
const baseOffset = queryText && dynamicKeywordQuery ? 2 : 1;
|
|
294
|
+
const paramIdx = baseOffset + filterParams.length;
|
|
295
|
+
|
|
296
|
+
// Check if we have synonyms for this key (e.g. 'brand' -> 'vendor', 'manufacturer')
|
|
297
|
+
const synonyms = FIELD_SYNONYMS[key] ?? [];
|
|
298
|
+
const keysToCheck = [key, ...synonyms];
|
|
299
|
+
|
|
300
|
+
// Map each candidate key to LOWER(metadata->>'candidate')
|
|
301
|
+
const coalesceExprs = keysToCheck.map(k => `LOWER(metadata->>'${k}')`);
|
|
302
|
+
return `COALESCE(${coalesceExprs.join(', ')}) = LOWER($${paramIdx})`;
|
|
303
|
+
});
|
|
304
|
+
whereClause = `WHERE ${conditions.join(' AND ')}`;
|
|
305
|
+
}
|
|
276
306
|
|
|
277
307
|
if (queryText && dynamicKeywordQuery) {
|
|
278
308
|
const hasEntityHints = entityHints.length > 0;
|
|
@@ -294,20 +324,22 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
294
324
|
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
295
325
|
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
296
326
|
FROM "${table}" t
|
|
327
|
+
${whereClause}
|
|
297
328
|
ORDER BY hybrid_score DESC
|
|
298
329
|
LIMIT ${tableLimit}
|
|
299
330
|
`;
|
|
300
|
-
params = [vectorLiteral, dynamicKeywordQuery];
|
|
331
|
+
params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
|
|
301
332
|
} else {
|
|
302
333
|
// Fallback to Vector-only Search
|
|
303
334
|
sqlQuery = `
|
|
304
335
|
SELECT *,
|
|
305
336
|
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
306
337
|
FROM "${table}" t
|
|
338
|
+
${whereClause}
|
|
307
339
|
ORDER BY hybrid_score DESC
|
|
308
340
|
LIMIT ${tableLimit}
|
|
309
341
|
`;
|
|
310
|
-
params = [vectorLiteral];
|
|
342
|
+
params = [vectorLiteral, ...filterParams];
|
|
311
343
|
}
|
|
312
344
|
|
|
313
345
|
const result = await this.pool.query(sqlQuery, params);
|
package/src/types/chat.ts
CHANGED
|
@@ -59,4 +59,6 @@ export interface UseRagChatReturn {
|
|
|
59
59
|
retry: () => Promise<void>;
|
|
60
60
|
/** Programmatically set the conversation (e.g. to restore from a DB) */
|
|
61
61
|
setMessages: React.Dispatch<React.SetStateAction<RagMessage[]>>;
|
|
62
|
+
/** Abort the current active stream request */
|
|
63
|
+
stop: () => void;
|
|
62
64
|
}
|
|
@@ -57,14 +57,16 @@ Given these metadata keys from a database: [${keys.join(', ')}]
|
|
|
57
57
|
Identify which keys best correspond to these standard UI properties:
|
|
58
58
|
${propertyList}
|
|
59
59
|
|
|
60
|
-
Return ONLY a JSON object where the keys are the UI properties and the values are the matching database keys.
|
|
60
|
+
Return ONLY a valid JSON object where the keys are the UI properties and the values are the EXACT matching database keys from the list above.
|
|
61
61
|
If no good match is found for a property, omit it.
|
|
62
62
|
|
|
63
63
|
Example:
|
|
64
64
|
{
|
|
65
|
-
"name": "
|
|
66
|
-
"price": "
|
|
67
|
-
"brand": "
|
|
65
|
+
"name": "Title",
|
|
66
|
+
"price": "Variant Price",
|
|
67
|
+
"brand": "Vendor",
|
|
68
|
+
"image": "Image Src",
|
|
69
|
+
"stock": "Variant Inventory Qty"
|
|
68
70
|
}
|
|
69
71
|
`;
|
|
70
72
|
|
|
@@ -483,7 +483,8 @@ RULES:
|
|
|
483
483
|
): UITransformationResponse {
|
|
484
484
|
const products: CarouselProduct[] = data
|
|
485
485
|
.map(item => this.extractProductInfo(item, config, trainedSchema))
|
|
486
|
-
.filter((p): p is CarouselProduct => p !== null)
|
|
486
|
+
.filter((p): p is CarouselProduct => p !== null)
|
|
487
|
+
.slice(0, 15);
|
|
487
488
|
|
|
488
489
|
return {
|
|
489
490
|
type: 'product_carousel',
|
package/src/utils/synonyms.ts
CHANGED
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export const FIELD_SYNONYMS: Record<string, string[]> = {
|
|
6
6
|
name: ['product', 'item', 'title', 'label', 'heading', 'subject', 'product name', 'item name'],
|
|
7
|
-
price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd'],
|
|
7
|
+
price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd', 'variant price'],
|
|
8
8
|
brand: ['manufacturer', 'vendor', 'make', 'company', 'brand_name', 'supplier'],
|
|
9
9
|
image: ['imageUrl', 'thumbnail', 'img', 'url', 'photo', 'picture', 'media', 'image_url',
|
|
10
|
-
'main_image', 'product_image', 'thumb'],
|
|
10
|
+
'main_image', 'product_image', 'thumb', 'image src', 'variant image'],
|
|
11
11
|
stock: ['inventory', 'quantity', 'count', 'availability', 'stock_level', 'inStock',
|
|
12
|
-
'is_available', 'in stock', 'status'],
|
|
12
|
+
'is_available', 'in stock', 'status', 'variant inventory qty'],
|
|
13
13
|
category: ['product_category', 'product category', 'category_name', 'category name',
|
|
14
|
-
'department', 'collection'],
|
|
15
|
-
description: ['summary', 'content', 'body', 'text', 'info', 'details'],
|
|
14
|
+
'department', 'collection', 'type'],
|
|
15
|
+
description: ['summary', 'content', 'body', 'text', 'info', 'details', 'body (html)', 'seo description'],
|
|
16
16
|
link: ['url', 'href', 'product_url', 'page_url', 'link'],
|
|
17
17
|
};
|
|
18
18
|
|