@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.
Files changed (47) hide show
  1. package/dist/{ILLMProvider-BOJFz3Na.d.mts → ILLMProvider-Bw2A28nU.d.mts} +12 -0
  2. package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-Bw2A28nU.d.ts} +12 -0
  3. package/dist/handlers/index.d.mts +2 -2
  4. package/dist/handlers/index.d.ts +2 -2
  5. package/dist/handlers/index.js +1874 -542
  6. package/dist/handlers/index.mjs +1873 -541
  7. package/dist/{index-D3V9Et2M.d.mts → index-B70ZLkfG.d.mts} +1 -1
  8. package/dist/{index-BwpcaziY.d.ts → index-DVu-mkAM.d.ts} +1 -1
  9. package/dist/index.css +83 -0
  10. package/dist/index.d.mts +2 -2
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.js +330 -106
  13. package/dist/index.mjs +333 -107
  14. package/dist/server.d.mts +32 -5
  15. package/dist/server.d.ts +32 -5
  16. package/dist/server.js +1871 -736
  17. package/dist/server.mjs +1870 -735
  18. package/package.json +1 -1
  19. package/src/components/ChatWindow.tsx +24 -14
  20. package/src/components/MarkdownComponents.tsx +3 -3
  21. package/src/components/MessageBubble.tsx +89 -7
  22. package/src/components/ProductCard.tsx +29 -2
  23. package/src/components/UIDispatcher.tsx +1 -0
  24. package/src/components/VisualizationRenderer.tsx +143 -11
  25. package/src/config/EmbeddingStrategy.ts +5 -4
  26. package/src/config/RagConfig.ts +10 -0
  27. package/src/config/serverConfig.ts +16 -1
  28. package/src/core/LLMRouter.ts +79 -0
  29. package/src/core/Pipeline.ts +295 -51
  30. package/src/core/ProviderRegistry.ts +6 -0
  31. package/src/core/QueryProcessor.ts +108 -9
  32. package/src/handlers/index.ts +37 -11
  33. package/src/hooks/useRagChat.ts +77 -17
  34. package/src/llm/providers/UniversalLLMAdapter.ts +110 -13
  35. package/src/providers/vectordb/ChromaDBProvider.ts +13 -2
  36. package/src/providers/vectordb/MilvusProvider.ts +18 -2
  37. package/src/providers/vectordb/MultiTablePostgresProvider.ts +48 -16
  38. package/src/providers/vectordb/PostgreSQLProvider.ts +1 -1
  39. package/src/providers/vectordb/QdrantProvider.ts +1 -1
  40. package/src/providers/vectordb/RedisProvider.ts +3 -4
  41. package/src/providers/vectordb/WeaviateProvider.ts +41 -3
  42. package/src/types/chat.ts +2 -0
  43. package/src/types/index.ts +26 -0
  44. package/src/utils/ProductExtractor.ts +5 -3
  45. package/src/utils/SchemaMapper.ts +6 -4
  46. package/src/utils/UITransformer.ts +1350 -490
  47. 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
- const systemKeys = ['fileName', 'fileSize', 'fileType', 'uploadedAt', 'dimension', 'chunkIndex', 'id', 'namespace', 'content', 'metadata', 'embedding'];
133
- const csvHeaders = Object.keys(firstMeta).filter(k => !systemKeys.includes(k) && !systemKeys.includes(k.toLowerCase()));
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?.__entityHints)
239
- ? (_filter.__entityHints as unknown[])
240
- .filter((hint): hint is { value: string; field?: string } =>
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.includes(' ') ? `"${h}"` : h) // Wrap multi-word hints in quotes
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 50
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
- (1 - (embedding <=> $1::vector)) AS hybrid_score
336
+ (1 - (embedding <=> $1::vector)) AS hybrid_score
306
337
  FROM "${table}" t
338
+ ${whereClause}
307
339
  ORDER BY hybrid_score DESC
308
- LIMIT 50
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(JSON.stringify(val));
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.efSearch as number) || Math.max(topK * 20, 128),
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
- * Redis is TCP-based and has no HTTP health endpoint.
94
- * Returns true; actual connectivity is validated on the first operation.
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
- // If REST ping fails, assume connectivity will be validated on first use
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
- ${namespace ? `where: { path: ["namespace"], operator: Equal, valueString: "${namespace}" }` : ''}
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
  }
@@ -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 hasProductKey = keys.some(k =>
203
- ['price', 'image', 'img', 'thumbnail', 'images', 'brand', 'product', 'sku', 'category', 'model', 'cost'].includes(k)
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 hasProductKey || hasPricePattern || m.type === 'product';
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": "Product_Title",
66
- "price": "MSRP_USD",
67
- "brand": "VendorName"
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