@retrivora-ai/rag-engine 1.9.0 → 1.9.2
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-BOJFz3Na.d.mts → ILLMProvider-Bw2A28nU.d.mts} +12 -0
- package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-Bw2A28nU.d.ts} +12 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1874 -542
- package/dist/handlers/index.mjs +1873 -541
- package/dist/{index-D3V9Et2M.d.mts → index-B70ZLkfG.d.mts} +1 -1
- package/dist/{index-BwpcaziY.d.ts → index-DVu-mkAM.d.ts} +1 -1
- package/dist/index.css +83 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +330 -106
- package/dist/index.mjs +333 -107
- package/dist/server.d.mts +32 -5
- package/dist/server.d.ts +32 -5
- package/dist/server.js +1871 -736
- package/dist/server.mjs +1870 -735
- package/package.json +1 -1
- package/src/components/ChatWindow.tsx +24 -14
- package/src/components/MarkdownComponents.tsx +3 -3
- package/src/components/MessageBubble.tsx +89 -7
- package/src/components/ProductCard.tsx +29 -2
- package/src/components/UIDispatcher.tsx +1 -0
- package/src/components/VisualizationRenderer.tsx +143 -11
- package/src/config/EmbeddingStrategy.ts +5 -4
- package/src/config/RagConfig.ts +10 -0
- package/src/config/serverConfig.ts +16 -1
- package/src/core/LLMRouter.ts +79 -0
- package/src/core/Pipeline.ts +295 -51
- package/src/core/ProviderRegistry.ts +6 -0
- package/src/core/QueryProcessor.ts +108 -9
- package/src/handlers/index.ts +37 -11
- package/src/hooks/useRagChat.ts +77 -17
- package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
- package/src/providers/vectordb/ChromaDBProvider.ts +13 -2
- package/src/providers/vectordb/MilvusProvider.ts +18 -2
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +48 -16
- package/src/providers/vectordb/PostgreSQLProvider.ts +1 -1
- package/src/providers/vectordb/QdrantProvider.ts +1 -1
- package/src/providers/vectordb/RedisProvider.ts +3 -4
- package/src/providers/vectordb/WeaviateProvider.ts +41 -3
- package/src/types/chat.ts +2 -0
- package/src/types/index.ts +26 -0
- package/src/utils/ProductExtractor.ts +5 -3
- package/src/utils/SchemaMapper.ts +6 -4
- package/src/utils/UITransformer.ts +1350 -490
- package/src/utils/synonyms.ts +6 -4
|
@@ -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 ');
|
|
@@ -235,12 +241,9 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
235
241
|
console.log(`[MultiTablePostgresProvider] --- Starting Multi-Table Search ---`);
|
|
236
242
|
|
|
237
243
|
const queryText = _filter?.queryText as string | undefined;
|
|
238
|
-
const entityHints = Array.isArray(_filter?.
|
|
239
|
-
? (_filter.
|
|
240
|
-
.
|
|
241
|
-
typeof hint === 'object' && hint !== null && typeof (hint as Record<string, unknown>).value === 'string'
|
|
242
|
-
)
|
|
243
|
-
.map((hint) => hint.value.trim().toLowerCase())
|
|
244
|
+
const entityHints = Array.isArray(_filter?.keywords)
|
|
245
|
+
? (_filter.keywords as string[])
|
|
246
|
+
.map((k) => k.trim().toLowerCase())
|
|
244
247
|
.filter(Boolean)
|
|
245
248
|
: [];
|
|
246
249
|
|
|
@@ -252,15 +255,17 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
252
255
|
if (entityHints.length > 0) {
|
|
253
256
|
// Use extracted entities as high-signal keywords
|
|
254
257
|
return entityHints
|
|
255
|
-
.map(h => h.
|
|
258
|
+
.map(h => h.replace(/\s+/g, ' & ')) // Replace spaces with & for valid tsquery
|
|
256
259
|
.join(' & ');
|
|
257
260
|
}
|
|
258
261
|
|
|
259
262
|
if (queryText) {
|
|
260
263
|
// Fallback to cleaned raw text if no high-signal hints were extracted
|
|
264
|
+
// Added 'get', 'details', 'information', 'info' to stop words
|
|
261
265
|
return queryText
|
|
262
266
|
.toLowerCase()
|
|
263
|
-
.replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|of|category|find|list|browse|what|are|display|all|under|for|in|with|about)\b/g, '')
|
|
267
|
+
.replace(/\b(show|me|the|product[s]?|item[s]?|card[s]?|organization[s]?|comp(?:any|anies)|of|category|find|list|browse|what|are|display|all|whose|that|which|have|has|having|greater|than|more|less|equal|equals|above|below|over|under|at|least|most|for|in|with|about|get|details|information|info)\b/g, '')
|
|
268
|
+
.replace(/\b\d+(?:\.\d+)?\b/g, '')
|
|
264
269
|
.trim()
|
|
265
270
|
.replace(/\s+/g, ' & ');
|
|
266
271
|
}
|
|
@@ -268,13 +273,38 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
268
273
|
};
|
|
269
274
|
|
|
270
275
|
const dynamicKeywordQuery = getDynamicKeywordQuery();
|
|
276
|
+
const tableLimit = Math.max(topK, 50);
|
|
277
|
+
|
|
278
|
+
const metadataFilters = _filter?.metadata as Record<string, string> | undefined;
|
|
271
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
|
-
if (queryText) {
|
|
307
|
+
if (queryText && dynamicKeywordQuery) {
|
|
278
308
|
const hasEntityHints = entityHints.length > 0;
|
|
279
309
|
const exactNameScoreExpr = hasEntityHints
|
|
280
310
|
? `+ (
|
|
@@ -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
|
-
LIMIT
|
|
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
|
-
LIMIT
|
|
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);
|
|
@@ -170,7 +170,7 @@ export class PostgreSQLProvider extends BaseVectorProvider {
|
|
|
170
170
|
const filterConditions = Object.entries(publicFilter)
|
|
171
171
|
.map(([key, val]) => {
|
|
172
172
|
const paramIdx = params.length + 1;
|
|
173
|
-
params.push(
|
|
173
|
+
params.push(String(val));
|
|
174
174
|
return `metadata->>'${key}' = $${paramIdx}`;
|
|
175
175
|
})
|
|
176
176
|
.join(' AND ');
|
|
@@ -211,7 +211,7 @@ export class QdrantProvider extends BaseVectorProvider {
|
|
|
211
211
|
limit: topK,
|
|
212
212
|
with_payload: true,
|
|
213
213
|
params: {
|
|
214
|
-
hnsw_ef: (this.config.options
|
|
214
|
+
hnsw_ef: ((this.config.options as Record<string, unknown>)?.efSearch as number | undefined) || Math.max(topK * 20, 128),
|
|
215
215
|
exact: false
|
|
216
216
|
},
|
|
217
217
|
filter: must.length > 0 ? { must } : undefined,
|
|
@@ -90,16 +90,15 @@ export class RedisProvider extends BaseVectorProvider {
|
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
94
|
-
* Returns
|
|
93
|
+
* Check reachability via a PING command.
|
|
94
|
+
* Returns false on connection failure so health checks surface real problems.
|
|
95
95
|
*/
|
|
96
96
|
async ping(): Promise<boolean> {
|
|
97
97
|
try {
|
|
98
98
|
await this.http.post('/', ['PING']);
|
|
99
99
|
return true;
|
|
100
100
|
} catch {
|
|
101
|
-
|
|
102
|
-
return true;
|
|
101
|
+
return false;
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
|
|
@@ -40,6 +40,7 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
async upsert(doc: UpsertDocument, namespace?: string): Promise<void> {
|
|
43
|
+
const primitiveMetadata = this.extractPrimitiveMetadata(doc.metadata);
|
|
43
44
|
const payload = {
|
|
44
45
|
class: this.indexName,
|
|
45
46
|
id: doc.id,
|
|
@@ -47,6 +48,7 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
47
48
|
properties: {
|
|
48
49
|
content: doc.content,
|
|
49
50
|
metadata: JSON.stringify(doc.metadata || {}),
|
|
51
|
+
...primitiveMetadata,
|
|
50
52
|
namespace: namespace || '',
|
|
51
53
|
},
|
|
52
54
|
};
|
|
@@ -62,6 +64,7 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
62
64
|
properties: {
|
|
63
65
|
content: doc.content,
|
|
64
66
|
metadata: JSON.stringify(doc.metadata || {}),
|
|
67
|
+
...this.extractPrimitiveMetadata(doc.metadata),
|
|
65
68
|
namespace: namespace || '',
|
|
66
69
|
},
|
|
67
70
|
})),
|
|
@@ -70,12 +73,14 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
70
73
|
}
|
|
71
74
|
|
|
72
75
|
async query(vector: number[], topK: number, namespace?: string, _filter?: Record<string, unknown>): Promise<VectorMatch[]> {
|
|
76
|
+
// IMPORTANT: read queryText BEFORE calling sanitizeFilter() because sanitizeFilter strips it.
|
|
77
|
+
const queryText = _filter?.queryText as string | undefined;
|
|
73
78
|
const sanitizedFilter = this.sanitizeFilter(_filter);
|
|
74
|
-
const queryText = sanitizedFilter.queryText as string;
|
|
75
79
|
|
|
76
|
-
const searchParams = queryText
|
|
80
|
+
const searchParams = queryText
|
|
77
81
|
? `hybrid: { query: ${JSON.stringify(queryText)}, alpha: 0.5 }`
|
|
78
82
|
: `nearVector: { vector: ${JSON.stringify(vector)} }`;
|
|
83
|
+
const where = this.buildWhereFilter(namespace, sanitizedFilter);
|
|
79
84
|
|
|
80
85
|
const graphqlQuery = {
|
|
81
86
|
query: `
|
|
@@ -84,7 +89,7 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
84
89
|
${this.indexName}(
|
|
85
90
|
${searchParams}
|
|
86
91
|
limit: ${topK}
|
|
87
|
-
${
|
|
92
|
+
${where ? `where: ${where}` : ''}
|
|
88
93
|
) {
|
|
89
94
|
content
|
|
90
95
|
metadata
|
|
@@ -132,4 +137,37 @@ export class WeaviateProvider extends BaseVectorProvider {
|
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
async disconnect(): Promise<void> {}
|
|
140
|
+
|
|
141
|
+
private extractPrimitiveMetadata(metadata?: Record<string, unknown>): Record<string, string | number | boolean> {
|
|
142
|
+
const result: Record<string, string | number | boolean> = {};
|
|
143
|
+
Object.entries(metadata || {}).forEach(([key, value]) => {
|
|
144
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
145
|
+
result[key] = value;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private buildWhereFilter(namespace?: string, filter?: Record<string, unknown>): string | undefined {
|
|
152
|
+
const operands: string[] = [];
|
|
153
|
+
if (namespace) operands.push(this.weaviateOperand('namespace', namespace));
|
|
154
|
+
|
|
155
|
+
Object.entries(filter || {}).forEach(([key, value]) => {
|
|
156
|
+
if (key === 'namespace') return;
|
|
157
|
+
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
158
|
+
operands.push(this.weaviateOperand(key, value));
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (operands.length === 0) return undefined;
|
|
163
|
+
if (operands.length === 1) return operands[0];
|
|
164
|
+
return `{ operator: And, operands: [${operands.join(', ')}] }`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private weaviateOperand(key: string, value: string | number | boolean): string {
|
|
168
|
+
const path = `path: [${JSON.stringify(key)}], operator: Equal`;
|
|
169
|
+
if (typeof value === 'number') return `{ ${path}, valueNumber: ${value} }`;
|
|
170
|
+
if (typeof value === 'boolean') return `{ ${path}, valueBoolean: ${value} }`;
|
|
171
|
+
return `{ ${path}, valueString: ${JSON.stringify(value)} }`;
|
|
172
|
+
}
|
|
135
173
|
}
|
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
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -125,7 +125,12 @@ export type VisualizationType =
|
|
|
125
125
|
| 'pie_chart'
|
|
126
126
|
| 'bar_chart'
|
|
127
127
|
| 'line_chart'
|
|
128
|
+
| 'histogram'
|
|
129
|
+
| 'horizontal_bar'
|
|
130
|
+
| 'scatter_plot'
|
|
128
131
|
| 'radar_chart'
|
|
132
|
+
| 'metric_card'
|
|
133
|
+
| 'geo_map'
|
|
129
134
|
| 'table'
|
|
130
135
|
| 'product_carousel'
|
|
131
136
|
| 'carousel'
|
|
@@ -173,6 +178,27 @@ export interface LineChartDataPoint {
|
|
|
173
178
|
[key: string]: unknown;
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Scatter plot data point
|
|
183
|
+
*/
|
|
184
|
+
export interface ScatterPlotDataPoint {
|
|
185
|
+
x: number;
|
|
186
|
+
y: number;
|
|
187
|
+
label?: string;
|
|
188
|
+
[key: string]: unknown;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* KPI / metric card representation
|
|
193
|
+
*/
|
|
194
|
+
export interface MetricCardData {
|
|
195
|
+
label: string;
|
|
196
|
+
value: number;
|
|
197
|
+
operation?: 'sum' | 'average' | 'count' | 'min' | 'max' | 'median';
|
|
198
|
+
unit?: string;
|
|
199
|
+
details?: Record<string, string | number | boolean>;
|
|
200
|
+
}
|
|
201
|
+
|
|
176
202
|
/**
|
|
177
203
|
* Table representation
|
|
178
204
|
*/
|
|
@@ -199,11 +199,13 @@ export function extractProductsFromSources(sources: VectorMatch[] | undefined, i
|
|
|
199
199
|
.filter(s => {
|
|
200
200
|
const m = s.metadata ?? {};
|
|
201
201
|
const keys = Object.keys(m).map(k => k.toLowerCase());
|
|
202
|
-
const
|
|
203
|
-
['price', 'image', 'img', 'thumbnail', 'images', '
|
|
202
|
+
const hasStrongProductKey = keys.some(k =>
|
|
203
|
+
['price', 'image', 'img', 'thumbnail', 'images', 'product', 'sku', 'cost'].includes(k)
|
|
204
204
|
);
|
|
205
|
+
const hasProductIdentity = keys.some(k => ['brand', 'model'].includes(k)) &&
|
|
206
|
+
keys.some(k => ['name', 'title', 'product_name', 'product'].includes(k));
|
|
205
207
|
const hasPricePattern = /\$\s*\d+/.test(s.content);
|
|
206
|
-
return
|
|
208
|
+
return hasStrongProductKey || hasProductIdentity || hasPricePattern || m.type === 'product';
|
|
207
209
|
})
|
|
208
210
|
.map(s => {
|
|
209
211
|
const m = (s.metadata ?? {}) as Record<string, unknown>;
|
|
@@ -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
|
|