@retrivora-ai/rag-engine 1.7.1 → 1.7.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.
@@ -101,16 +101,15 @@ export class Pipeline {
101
101
 
102
102
  // Augment system prompt with a Universal Visualization Protocol
103
103
  // We use a unique marker to ensure this is only injected once but is ALWAYS present.
104
- const CHART_MARKER = '<!-- UI_PROTOCOL_V7 -->';
104
+ const CHART_MARKER = '<!-- UI_PROTOCOL_V6 -->';
105
105
  const chartInstruction = `\n\n${CHART_MARKER}
106
- ### UNIVERSAL UI PROTOCOL (MANDATORY)
107
- CRITICAL: When providing visual data (carousels, charts, tables), you MUST wrap the JSON payload in a \`\`\`ui\`\`\` code block.
108
- NEVER output naked JSON. If you provide a visualization, do not also provide a markdown table of the same data unless explicitly asked.
106
+ ### UNIVERSAL UI PROTOCOL
107
+ When the user asks for a visual representation, comparison, distribution, breakdown, table, list, catalog, products, carousel, stock view, or category summary, you MUST include exactly one \`\`\`ui\`\`\` block.
109
108
 
110
109
  1. VIEW SELECTION
111
- - Use "carousel" for browsing products, items, or catalogs. (e.g., "show me beauty products").
112
- - Use "chart" ONLY for numerical aggregations (counts, sums, percentages).
113
- - Use "table" for technical specs or row-heavy comparisons.
110
+ - Use "carousel" for browsing specific products, items, or a catalog. This is the DEFAULT for "show me", "what are", or "find me" requests for a specific category (e.g. "beauty").
111
+ - Use "chart" ONLY for numerical aggregations: distributions, percentages, counts, comparisons over time, or trends (e.g. "how many beauty products?").
112
+ - Use "table" for detailed technical specifications, row-heavy comparisons, or when the user explicitly asks for a list/table format.
114
113
 
115
114
  2. REQUIRED JSON SHAPE
116
115
  {
@@ -118,31 +117,52 @@ NEVER output naked JSON. If you provide a visualization, do not also provide a m
118
117
  "title": "Short heading",
119
118
  "description": "One sentence describing the view",
120
119
  "chartType": "pie" | "bar" | "line", // Required if view is "chart"
121
- "data": [] // Array of objects
120
+ "xAxisKey": "label",
121
+ "dataKeys": ["value"],
122
+ "columns": ["dynamicFieldKey1", "dynamicFieldKey2"],
123
+ "insights": ["Short takeaway 1", "Short takeaway 2"],
124
+ "data": []
122
125
  }
123
126
 
124
127
  3. DATA RULES
125
- - Build the UI payload from retrieved context and metadata.
126
- - For CAROUSEL: "data" items MUST have { id, name, brand, price, image, link }.
127
- - IMPORTANT: Extract "image" and "price" from Source metadata. Image URLs are often in fields like "images", "thumbnail", or "img".
128
+ - Build the UI payload from retrieved context only. Do not invent products or values.
129
+ - For CAROUSEL: "data" must be a list of product objects (id, name, brand, price, image, link).
130
+ - For CHART: "data" must be aggregated counts or sums. Never put raw product lists in a chart view.
131
+ - If the user asks for "products in beauty", do NOT return a pie chart of categories; return a carousel of the beauty products themselves.
128
132
 
129
133
  4. FORMAT RULES
130
- - Wrap the JSON in \`\`\`ui ... \`\`\` blocks.
131
- - Place natural language text OUTSIDE the block.
134
+ - Return valid JSON inside the \`\`\`ui\`\`\` block.
135
+ - Put natural language explanation before or after the block, not inside.
132
136
 
133
- 5. EXAMPLE
134
- User: "find me beauty products"
135
- Assistant: "I found several beauty products in our catalog:"
137
+ 5. EXAMPLES
138
+ User: "show me beauty products"
139
+ Assistant:
136
140
  \`\`\`ui
137
141
  {
138
142
  "view": "carousel",
139
143
  "title": "Beauty Products",
140
- "description": "Latest skincare and makeup items.",
144
+ "description": "Browse our latest skincare and makeup collection.",
141
145
  "data": [
142
- { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "https://placehold.co/600x400?text=Product", "link": "https://example.com/product" }
146
+ { "id": "1", "name": "Mascara", "brand": "Essence", "price": "$9.99", "image": "...", "link": "..." },
147
+ { "id": "2", "name": "Lipstick", "brand": "Chic", "price": "$12.99", "image": "...", "link": "..." }
143
148
  ]
144
149
  }
145
150
  \`\`\`
151
+
152
+ User: "breakdown of products by category"
153
+ Assistant:
154
+ \`\`\`ui
155
+ {
156
+ "view": "chart",
157
+ "title": "Category Breakdown",
158
+ "chartType": "pie",
159
+ "xAxisKey": "label",
160
+ "dataKeys": ["value"],
161
+ "data": [
162
+ { "label": "Beauty", "value": 15 },
163
+ { "label": "Fragrances", "value": 8 }
164
+ ]
165
+ }
146
166
  \`\`\``;
147
167
 
148
168
  if (!this.config.llm.systemPrompt?.includes(CHART_MARKER)) {
@@ -387,7 +407,7 @@ Assistant: "I found several beauty products in our catalog:"
387
407
 
388
408
  // 4. Context Augmentation
389
409
  let context = sources.length
390
- ? sources.map((m, i) => `[Source ${i + 1}]\nContent: ${m.content}\nMetadata: ${JSON.stringify(m.metadata)}`).join('\n\n---\n\n')
410
+ ? sources.map((m, i) => `[Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n')
391
411
  : 'No relevant context found.';
392
412
 
393
413
  if (graphData && graphData.nodes.length > 0) {
@@ -1,8 +1,9 @@
1
1
  import { NextRequest, NextResponse } from 'next/server';
2
2
  import { VectorPlugin } from '../core/VectorPlugin';
3
3
  import { RagConfig } from '../config/RagConfig';
4
- import { ChatMessage } from '../types';
4
+ import { ChatMessage, ChatResponse } from '../types';
5
5
  import { DocumentParser } from '../utils/DocumentParser';
6
+ import { UITransformer } from '../utils/UITransformer'
6
7
 
7
8
  // ─── SSE helpers ──────────────────────────────────────────────────────────────
8
9
 
@@ -19,11 +20,16 @@ function sseTextFrame(text: string): string {
19
20
  return `data: ${JSON.stringify({ type: 'text', text })}\n\n`;
20
21
  }
21
22
 
22
- /** Encode the retrieval metadata as an SSE frame. */
23
+ /** Encode the retrieval metadata with UI transformation as an SSE frame. */
23
24
  function sseMetaFrame(meta: unknown): string {
24
25
  return `data: ${JSON.stringify({ type: 'metadata', ...((meta as object) ?? {}) })}\n\n`;
25
26
  }
26
27
 
28
+ /** Encode the UI transformation result as an SSE frame. */
29
+ function sseUIFrame(uiTransformation: unknown): string {
30
+ return `data: ${JSON.stringify({ type: 'ui_transformation', ...((uiTransformation as object) ?? {}) })}\n\n`;
31
+ }
32
+
27
33
  /** Encode a stream error as an SSE frame. */
28
34
  function sseErrorFrame(message: string): string {
29
35
  return `data: ${JSON.stringify({ type: 'error', error: message })}\n\n`;
@@ -145,6 +151,18 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
145
151
  } else {
146
152
  // Retrieval metadata object — always the final frame
147
153
  enqueue(sseMetaFrame(chunk));
154
+
155
+ // Transform retrieved sources to UI representation
156
+ const sources = (chunk as ChatResponse)?.sources || [];
157
+ if (sources && sources.length > 0) {
158
+ try {
159
+ const uiTransformation = UITransformer.transform(message, sources);
160
+ enqueue(sseUIFrame(uiTransformation));
161
+ } catch (transformError) {
162
+ console.warn('[createStreamHandler] UI transformation warning:', transformError);
163
+ // Don't fail the stream if transformation fails, just skip it
164
+ }
165
+ }
148
166
  }
149
167
  }
150
168
  } catch (streamError) {
@@ -292,4 +310,4 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
292
310
  }
293
311
 
294
312
  // Re-export SSE helper so host apps can use it in custom handlers
295
- export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame };
313
+ export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame };
@@ -0,0 +1,535 @@
1
+ import { VectorMatch } from '../types';
2
+
3
+ /**
4
+ * Supported UI visualization types
5
+ */
6
+ export type VisualizationType =
7
+ | 'pie_chart'
8
+ | 'bar_chart'
9
+ | 'line_chart'
10
+ | 'table'
11
+ | 'product_carousel'
12
+ | 'text';
13
+
14
+ /**
15
+ * Base structure for all UI transformation responses
16
+ */
17
+ export interface UITransformationResponse {
18
+ type: VisualizationType;
19
+ title: string;
20
+ description?: string;
21
+ data: unknown;
22
+ }
23
+
24
+ /**
25
+ * Pie chart data structure
26
+ */
27
+ export interface PieChartData {
28
+ label: string;
29
+ value: number;
30
+ inStock?: boolean;
31
+ [key: string]: unknown;
32
+ }
33
+
34
+ /**
35
+ * Bar chart data structure
36
+ */
37
+ export interface BarChartData {
38
+ category: string;
39
+ value: number;
40
+ inStock?: boolean;
41
+ [key: string]: unknown;
42
+ }
43
+
44
+ /**
45
+ * Line chart data point
46
+ */
47
+ export interface LineChartDataPoint {
48
+ timestamp: string | number;
49
+ value: number;
50
+ label?: string;
51
+ [key: string]: unknown;
52
+ }
53
+
54
+ /**
55
+ * Table representation
56
+ */
57
+ export interface TableData {
58
+ columns: string[];
59
+ rows: (string | number | boolean)[][];
60
+ }
61
+
62
+ /**
63
+ * Product carousel item
64
+ */
65
+ export interface CarouselProduct {
66
+ id: string | number;
67
+ name: string;
68
+ price?: number | string;
69
+ image?: string;
70
+ inStock?: boolean;
71
+ brand?: string;
72
+ description?: string;
73
+ [key: string]: unknown;
74
+ }
75
+
76
+ /**
77
+ * UITransformer — Converts vector database results into structured UI representations
78
+ *
79
+ * This class analyzes retrieved data and automatically selects the best visualization
80
+ * format based on the content and structure of the data.
81
+ */
82
+ export class UITransformer {
83
+ /**
84
+ * Main transformation method
85
+ * Analyzes user query and retrieved data to determine the best visualization format
86
+ *
87
+ * @param userQuery - The original user question/query
88
+ * @param retrievedData - Vector database retrieval results
89
+ * @returns Structured JSON response for UI rendering
90
+ */
91
+ static transform(
92
+ userQuery: string,
93
+ retrievedData: VectorMatch[]
94
+ ): UITransformationResponse {
95
+ if (!retrievedData || retrievedData.length === 0) {
96
+ return this.createTextResponse('No data available', 'No relevant data found for your query.');
97
+ }
98
+
99
+ // Analyze the retrieved data to determine best format
100
+ const analysis = this.analyzeData(userQuery, retrievedData);
101
+
102
+ // Route to appropriate transformer based on analysis
103
+ switch (analysis.suggestedType) {
104
+ case 'product_carousel':
105
+ return this.transformToProductCarousel(retrievedData);
106
+ case 'pie_chart':
107
+ return this.transformToPieChart(retrievedData);
108
+ case 'bar_chart':
109
+ return this.transformToBarChart(retrievedData);
110
+ case 'line_chart':
111
+ return this.transformToLineChart(retrievedData);
112
+ case 'table':
113
+ return this.transformToTable(retrievedData);
114
+ default:
115
+ return this.transformToText(retrievedData);
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Analyzes the data and query to suggest the best visualization type
121
+ */
122
+ private static analyzeData(
123
+ query: string,
124
+ data: VectorMatch[]
125
+ ): {
126
+ suggestedType: VisualizationType;
127
+ confidence: number;
128
+ hasProducts: boolean;
129
+ hasTimeSeries: boolean;
130
+ hasCategories: boolean;
131
+ } {
132
+ const queryLower = query.toLowerCase();
133
+ const hasProducts = data.some(item =>
134
+ this.isProductData(item)
135
+ );
136
+ const hasTimeSeries = data.some(item =>
137
+ this.isTimeSeriesData(item)
138
+ );
139
+ const hasCategories = this.detectCategories(data).length > 0;
140
+ const isDistributionQuery =
141
+ queryLower.includes('distribution') ||
142
+ queryLower.includes('breakdown') ||
143
+ queryLower.includes('percentage') ||
144
+ queryLower.includes('proportion');
145
+ const isComparisonQuery =
146
+ queryLower.includes('compare') ||
147
+ queryLower.includes('vs') ||
148
+ queryLower.includes('difference');
149
+ const isTrendQuery =
150
+ queryLower.includes('trend') ||
151
+ queryLower.includes('over time') ||
152
+ queryLower.includes('growth') ||
153
+ queryLower.includes('decline');
154
+
155
+ // Decision logic
156
+ if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
157
+ return { suggestedType: 'product_carousel', confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
158
+ }
159
+
160
+ if (isTrendQuery && hasTimeSeries) {
161
+ return { suggestedType: 'line_chart', confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
162
+ }
163
+
164
+ if (isDistributionQuery && hasCategories) {
165
+ return { suggestedType: 'pie_chart', confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
166
+ }
167
+
168
+ if (isComparisonQuery && hasCategories && data.length > 2) {
169
+ return { suggestedType: 'bar_chart', confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
170
+ }
171
+
172
+ if (data.length > 5 && this.hasMultipleFields(data)) {
173
+ return { suggestedType: 'table', confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
174
+ }
175
+
176
+ return { suggestedType: 'text', confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
177
+ }
178
+
179
+ /**
180
+ * Transform data to product carousel format
181
+ */
182
+ private static transformToProductCarousel(data: VectorMatch[]): UITransformationResponse {
183
+ const products: CarouselProduct[] = data
184
+ .filter(item => this.isProductData(item))
185
+ .map(item => this.extractProductInfo(item))
186
+ .filter((p): p is CarouselProduct => p !== null);
187
+
188
+ return {
189
+ type: 'product_carousel',
190
+ title: 'Recommended Products',
191
+ description: `Found ${products.length} relevant products`,
192
+ data: products,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * Transform data to pie chart format
198
+ */
199
+ private static transformToPieChart(
200
+ data: VectorMatch[]
201
+ ): UITransformationResponse {
202
+ const categories = this.detectCategories(data);
203
+ const categoryData = this.aggregateByCategory(data, categories);
204
+
205
+ const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => ({
206
+ label,
207
+ value: count as number,
208
+ inStock: this.checkStockStatus(label, data),
209
+ }));
210
+
211
+ return {
212
+ type: 'pie_chart',
213
+ title: 'Distribution by Category',
214
+ description: `Showing breakdown across ${categories.length} categories`,
215
+ data: pieData,
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Transform data to bar chart format
221
+ */
222
+ private static transformToBarChart(
223
+ data: VectorMatch[]
224
+ ): UITransformationResponse {
225
+ const categories = this.detectCategories(data);
226
+ const categoryData = this.aggregateByCategory(data, categories);
227
+
228
+ const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => ({
229
+ category,
230
+ value: value as number,
231
+ inStock: this.checkStockStatus(category, data),
232
+ }));
233
+
234
+ return {
235
+ type: 'bar_chart',
236
+ title: 'Comparison by Category',
237
+ description: `Comparing ${categories.length} categories`,
238
+ data: barData,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Transform data to line chart format
244
+ */
245
+ private static transformToLineChart(data: VectorMatch[]): UITransformationResponse {
246
+ const timePoints = this.extractTimeSeriesData(data);
247
+
248
+ const lineData: LineChartDataPoint[] = timePoints.map(point => ({
249
+ timestamp: point.timestamp,
250
+ value: point.value,
251
+ label: point.label,
252
+ }));
253
+
254
+ return {
255
+ type: 'line_chart',
256
+ title: 'Trend Over Time',
257
+ description: `Showing ${lineData.length} data points`,
258
+ data: lineData,
259
+ };
260
+ }
261
+
262
+ /**
263
+ * Transform data to table format
264
+ */
265
+ private static transformToTable(data: VectorMatch[]): UITransformationResponse {
266
+ const columns = this.extractTableColumns(data);
267
+ const rows = data.map(item => this.extractTableRow(item, columns));
268
+
269
+ const tableData: TableData = {
270
+ columns,
271
+ rows,
272
+ };
273
+
274
+ return {
275
+ type: 'table',
276
+ title: 'Detailed Results',
277
+ description: `Showing ${data.length} results`,
278
+ data: tableData,
279
+ };
280
+ }
281
+
282
+ /**
283
+ * Transform data to text format (fallback)
284
+ */
285
+ private static transformToText(data: VectorMatch[]): UITransformationResponse {
286
+ const textContent = data
287
+ .map(item => item.content)
288
+ .join('\n\n');
289
+
290
+ return this.createTextResponse(
291
+ 'Search Results',
292
+ textContent,
293
+ `Found ${data.length} relevant results`
294
+ );
295
+ }
296
+
297
+ /**
298
+ * Helper: Create text response
299
+ */
300
+ private static createTextResponse(
301
+ title: string,
302
+ content: string,
303
+ description?: string
304
+ ): UITransformationResponse {
305
+ return {
306
+ type: 'text',
307
+ title,
308
+ description,
309
+ data: { content },
310
+ };
311
+ }
312
+
313
+ /**
314
+ * Helper: Check if data item is product-related
315
+ */
316
+ private static isProductData(item: VectorMatch): boolean {
317
+ const content = (item.content || '').toLowerCase();
318
+ const productKeywords = ['product', 'price', 'stock', 'item', 'sku', 'brand', 'model'];
319
+ return productKeywords.some(kw => content.includes(kw)) ||
320
+ Object.keys(item.metadata || {}).some(k =>
321
+ ['name', 'price', 'product', 'sku'].includes(k.toLowerCase())
322
+ );
323
+ }
324
+
325
+ /**
326
+ * Helper: Check if data contains time series
327
+ */
328
+ private static isTimeSeriesData(item: VectorMatch): boolean {
329
+ const content = (item.content || '').toLowerCase();
330
+ const timeKeywords = ['date', 'time', 'year', 'month', 'week', 'day', 'hour', 'trend', 'historical'];
331
+ return timeKeywords.some(kw => content.includes(kw)) ||
332
+ Object.keys(item.metadata || {}).some(k =>
333
+ ['date', 'timestamp', 'time', 'period'].includes(k.toLowerCase())
334
+ );
335
+ }
336
+
337
+ /**
338
+ * Helper: Extract product information from vector match
339
+ */
340
+ private static extractProductInfo(item: VectorMatch): CarouselProduct | null {
341
+ const meta = item.metadata || {};
342
+
343
+ // Try to extract product info from metadata first
344
+ if (meta.name || meta.product) {
345
+ return {
346
+ id: item.id,
347
+ name: (meta.name || meta.product) as string,
348
+ price: meta.price as string | number | undefined,
349
+ image: (meta.image || meta.imageUrl) as string | undefined,
350
+ brand: meta.brand as string | undefined,
351
+ description: item.content,
352
+ inStock: this.determineStockStatus(item),
353
+ };
354
+ }
355
+
356
+ // Fallback: try to parse from content
357
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
358
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d.]+)/i);
359
+
360
+ if (nameMatch) {
361
+ return {
362
+ id: item.id,
363
+ name: nameMatch[1],
364
+ price: priceMatch ? parseFloat(priceMatch[1]) : undefined,
365
+ description: item.content,
366
+ inStock: this.determineStockStatus(item),
367
+ };
368
+ }
369
+
370
+ return null;
371
+ }
372
+
373
+ /**
374
+ * Helper: Detect categories in data
375
+ */
376
+ private static detectCategories(data: VectorMatch[]): string[] {
377
+ const categories = new Set<string>();
378
+
379
+ data.forEach(item => {
380
+ const meta = item.metadata || {};
381
+
382
+ // Check for category field
383
+ if (meta.category) {
384
+ categories.add(String(meta.category));
385
+ }
386
+ if (meta.type) {
387
+ categories.add(String(meta.type));
388
+ }
389
+ if (meta.tag) {
390
+ const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
391
+ tags.forEach(t => categories.add(String(t)));
392
+ }
393
+
394
+ // Parse from content
395
+ const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
396
+ if (categoryMatch) {
397
+ categories.add(categoryMatch[1].trim());
398
+ }
399
+ });
400
+
401
+ return Array.from(categories);
402
+ }
403
+
404
+ /**
405
+ * Helper: Aggregate data by category
406
+ */
407
+ private static aggregateByCategory(
408
+ data: VectorMatch[],
409
+ categories: string[]
410
+ ): Record<string, number> {
411
+ const result: Record<string, number> = {};
412
+
413
+ categories.forEach(cat => {
414
+ result[cat] = 0;
415
+ });
416
+
417
+ data.forEach(item => {
418
+ const meta = item.metadata || {};
419
+ const itemCategory = (meta.category || meta.type || 'Other') as string;
420
+
421
+ if (result.hasOwnProperty(itemCategory)) {
422
+ result[itemCategory]++;
423
+ } else {
424
+ result['Other'] = (result['Other'] || 0) + 1;
425
+ }
426
+ });
427
+
428
+ return result;
429
+ }
430
+
431
+ /**
432
+ * Helper: Extract time series data
433
+ */
434
+ private static extractTimeSeriesData(
435
+ data: VectorMatch[]
436
+ ): Array<{ timestamp: string | number; value: number; label?: string }> {
437
+ return data.map(item => {
438
+ const meta = item.metadata || {};
439
+ return {
440
+ timestamp: (meta.timestamp || meta.date || new Date().toISOString()) as string | number,
441
+ value: (meta.value || item.score || 0) as number,
442
+ label: (meta.label || item.content.substring(0, 50)) as string,
443
+ };
444
+ });
445
+ }
446
+
447
+ /**
448
+ * Helper: Extract table columns
449
+ */
450
+ private static extractTableColumns(data: VectorMatch[]): string[] {
451
+ const columnSet = new Set<string>();
452
+ columnSet.add('Content');
453
+
454
+ data.forEach(item => {
455
+ Object.keys(item.metadata || {}).forEach(key => {
456
+ columnSet.add(key.charAt(0).toUpperCase() + key.slice(1));
457
+ });
458
+ });
459
+
460
+ return Array.from(columnSet);
461
+ }
462
+
463
+ /**
464
+ * Helper: Extract table row
465
+ */
466
+ private static extractTableRow(item: VectorMatch, columns: string[]): (string | number | boolean)[] {
467
+ const meta = item.metadata || {};
468
+ return columns.map(col => {
469
+ if (col === 'Content') {
470
+ return item.content.substring(0, 100);
471
+ }
472
+
473
+ const metaKey = col.charAt(0).toLowerCase() + col.slice(1);
474
+ const value = meta[metaKey];
475
+ return value !== undefined ? String(value) : '';
476
+ });
477
+ }
478
+
479
+ /**
480
+ * Helper: Check stock status
481
+ */
482
+ private static checkStockStatus(category: string, data: VectorMatch[]): boolean {
483
+ const item = data.find(d => {
484
+ const meta = d.metadata || {};
485
+ return meta.category === category || meta.type === category;
486
+ });
487
+
488
+ return this.determineStockStatus(item || ({} as VectorMatch));
489
+ }
490
+
491
+ /**
492
+ * Helper: Determine if item is in stock
493
+ */
494
+ private static determineStockStatus(item: VectorMatch): boolean {
495
+ const meta = item.metadata || {};
496
+
497
+ // Check metadata for stock status
498
+ if (meta.inStock !== undefined) {
499
+ return Boolean(meta.inStock);
500
+ }
501
+ if (meta.stock !== undefined) {
502
+ return Boolean(meta.stock);
503
+ }
504
+ if (meta.available !== undefined) {
505
+ return Boolean(meta.available);
506
+ }
507
+
508
+ // Check content
509
+ const content = (item.content || '').toLowerCase();
510
+ if (content.includes('out of stock') || content.includes('unavailable')) {
511
+ return false;
512
+ }
513
+ if (content.includes('in stock') || content.includes('available')) {
514
+ return true;
515
+ }
516
+
517
+ // Default to true if not specified
518
+ return true;
519
+ }
520
+
521
+ /**
522
+ * Helper: Check if data has multiple fields
523
+ */
524
+ private static hasMultipleFields(data: VectorMatch[]): boolean {
525
+ const fieldCount = new Set<string>();
526
+
527
+ data.forEach(item => {
528
+ Object.keys(item.metadata || {}).forEach(key => {
529
+ fieldCount.add(key);
530
+ });
531
+ });
532
+
533
+ return fieldCount.size > 2;
534
+ }
535
+ }