@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.
@@ -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 sources = hasNumericPredicates
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
- sources = await this.reranker.rerank(sources, question, topK);
509
+ fullSources = await this.reranker.rerank(fullSources, question, rerankLimit);
505
510
  } else if (!wantsExhaustiveList) {
506
- sources = sources.slice(0, topK);
511
+ fullSources = fullSources.slice(0, topK);
507
512
  }
508
513
  const rerankMs = performance.now() - rerankStart;
509
514
 
510
- // 4. Context Augmentation
511
- let context = sources.length
512
- ? sources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
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 plain text only. NEVER generate 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 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.)';
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
- let uiTransformation: unknown;
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
- await this.vectorDB.query(queryVector, retrievalLimit, ns, baseFilter),
808
- baseFilter,
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
  }
@@ -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) => controller.enqueue(encoder.encode(text));
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
- const errorMessage =
195
- streamError instanceof Error ? streamError.message : String(streamError);
196
- console.error('[createStreamHandler] Stream error:', streamError);
197
- enqueue(sseErrorFrame(errorMessage));
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
- controller.close();
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
  });
@@ -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
- id: res._id,
196
- content: res[this.contentKey] as string,
197
- metadata: (res[this.metadataKey] as Record<string, unknown>) || {},
198
- score: res.score,
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
- 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 ');
@@ -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": "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
 
@@ -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',
@@ -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