@retrivora-ai/rag-engine 1.7.9 → 1.8.1

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/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-wCRqMtdX.d.mts → ILLMProvider-BfRgI1Xh.d.mts} +59 -1
  4. package/dist/{index-wCRqMtdX.d.ts → ILLMProvider-BfRgI1Xh.d.ts} +59 -1
  5. package/dist/MultiTablePostgresProvider-YY7LPNJK.mjs +8 -0
  6. package/dist/{chunk-UHGYLTTY.mjs → chunk-BFYLQYQU.mjs} +846 -458
  7. package/dist/chunk-R3RGUMHE.mjs +218 -0
  8. package/dist/handlers/index.d.mts +2 -2
  9. package/dist/handlers/index.d.ts +2 -2
  10. package/dist/handlers/index.js +1033 -622
  11. package/dist/handlers/index.mjs +1 -1
  12. package/dist/{index-BDqz2_Yu.d.ts → index-1Z4GuYBi.d.ts} +7 -1
  13. package/dist/{index-DhsG2o5q.d.mts → index-BV0z5mb6.d.mts} +7 -1
  14. package/dist/index.d.mts +3 -3
  15. package/dist/index.d.ts +3 -3
  16. package/dist/index.js +507 -352
  17. package/dist/index.mjs +427 -253
  18. package/dist/server.d.mts +35 -5
  19. package/dist/server.d.ts +35 -5
  20. package/dist/server.js +1183 -795
  21. package/dist/server.mjs +163 -176
  22. package/package.json +4 -2
  23. package/src/app/page.tsx +1 -2
  24. package/src/components/DynamicChart.tsx +10 -35
  25. package/src/components/MessageBubble.tsx +223 -74
  26. package/src/components/ProductCard.tsx +2 -2
  27. package/src/components/VisualizationRenderer.tsx +347 -247
  28. package/src/config/RagConfig.ts +5 -0
  29. package/src/config/serverConfig.ts +3 -1
  30. package/src/core/Pipeline.ts +70 -209
  31. package/src/core/ProviderRegistry.ts +2 -2
  32. package/src/core/VectorPlugin.ts +9 -0
  33. package/src/handlers/index.ts +23 -7
  34. package/src/hooks/useRagChat.ts +2 -2
  35. package/src/llm/LLMFactory.ts +54 -2
  36. package/src/llm/providers/AnthropicProvider.ts +12 -8
  37. package/src/llm/providers/GeminiProvider.ts +188 -143
  38. package/src/llm/providers/OllamaProvider.ts +7 -3
  39. package/src/llm/providers/OpenAIProvider.ts +12 -8
  40. package/src/types/chat.ts +6 -0
  41. package/src/types/index.ts +81 -0
  42. package/src/utils/SchemaMapper.ts +129 -0
  43. package/src/utils/UITransformer.ts +524 -194
  44. package/dist/DocumentChunker-Bmscbh-X.d.ts +0 -93
  45. package/dist/DocumentChunker-DCuxrOdM.d.mts +0 -93
  46. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  47. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -1,77 +1,5 @@
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
- }
1
+ import { VectorMatch, UITransformationResponse, PieChartData, BarChartData, LineChartDataPoint, TableData, CarouselProduct, VisualizationType } from '../types';
2
+ import { ILLMProvider } from '../llm/ILLMProvider';
75
3
 
76
4
  /**
77
5
  * UITransformer — Converts vector database results into structured UI representations
@@ -80,109 +8,82 @@ export interface CarouselProduct {
80
8
  * format based on the content and structure of the data.
81
9
  */
82
10
  export class UITransformer {
11
+ /**
12
+ * Central dictionary of common synonyms for UI properties.
13
+ * This allows the system to be schema-agnostic by guessing field names.
14
+ */
15
+ private static readonly SYNONYMS: Record<string, string[]> = {
16
+ name: ['product', 'item', 'title', 'label', 'heading', 'subject'],
17
+ price: ['cost', 'amount', 'msrp', 'price', 'rate', 'value', 'price_usd'],
18
+ brand: ['manufacturer', 'vendor', 'make', 'company', 'brand_name', 'supplier'],
19
+ image: ['imageUrl', 'thumbnail', 'img', 'url', 'photo', 'picture', 'media', 'image_url', 'main_image', 'product_image', 'thumb'],
20
+ stock: ['inventory', 'quantity', 'count', 'availability', 'stock_level', 'inStock', 'is_available'],
21
+ description: ['summary', 'content', 'body', 'text', 'info', 'details'],
22
+ };
23
+
83
24
  /**
84
25
  * 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
26
+ * Analyzes user query and retrieved data to determine if a product carousel is needed.
90
27
  */
91
28
  static transform(
92
29
  userQuery: string,
93
- retrievedData: VectorMatch[]
30
+ retrievedData: VectorMatch[],
31
+ config?: import('../config/RagConfig').RagConfig,
32
+ trainedSchema?: import('./SchemaMapper').SchemaMap
94
33
  ): UITransformationResponse {
95
34
  if (!retrievedData || retrievedData.length === 0) {
96
35
  return this.createTextResponse('No data available', 'No relevant data found for your query.');
97
36
  }
98
37
 
99
- // Analyze the retrieved data to determine best format
100
- const analysis = this.analyzeData(userQuery, retrievedData);
38
+ const isStockRequest = this.isStockQuery(userQuery);
39
+ const filteredData = isStockRequest
40
+ ? retrievedData.filter(item => this.determineStockStatus(item))
41
+ : retrievedData;
101
42
 
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
- }
43
+ const categories = this.detectCategories(filteredData);
44
+ const hasProducts = filteredData.some(item => this.isProductData(item));
45
+ const isTimeSeries = filteredData.some(item => this.isTimeSeriesData(item));
46
+ const isTrendQuery = this.isTrendQuery(userQuery);
118
47
 
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 };
48
+ // 1. High Priority: Specific visualizations for trends
49
+ if (isTrendQuery && isTimeSeries) {
50
+ return this.transformToLineChart(filteredData);
158
51
  }
159
52
 
160
- if (isTrendQuery && hasTimeSeries) {
161
- return { suggestedType: 'line_chart', confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
53
+ // 2. High Priority: Products (Always prefer carousel for products unless it's a specific breakdown request)
54
+ if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
55
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
162
56
  }
163
57
 
164
- if (isDistributionQuery && hasCategories) {
165
- return { suggestedType: 'pie_chart', confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
58
+ // 3. Category breakdowns
59
+ if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
60
+ return this.transformToPieChart(filteredData);
166
61
  }
167
62
 
168
- if (isComparisonQuery && hasCategories && data.length > 2) {
169
- return { suggestedType: 'bar_chart', confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
63
+ // 4. Secondary: Products as carousel if not already handled
64
+ if (hasProducts) {
65
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
170
66
  }
171
67
 
172
- if (data.length > 5 && this.hasMultipleFields(data)) {
173
- return { suggestedType: 'table', confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
68
+ // 5. Detailed data without clear product structure -> Table
69
+ if (this.hasMultipleFields(filteredData)) {
70
+ return this.transformToTable(filteredData);
174
71
  }
175
72
 
176
- return { suggestedType: 'text', confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
73
+ return this.transformToText(filteredData);
177
74
  }
178
75
 
179
76
  /**
180
77
  * Transform data to product carousel format
181
78
  */
182
- private static transformToProductCarousel(data: VectorMatch[]): UITransformationResponse {
79
+ private static transformToProductCarousel(
80
+ data: VectorMatch[],
81
+ config?: import('../config/RagConfig').RagConfig,
82
+ trainedSchema?: import('./SchemaMapper').SchemaMap
83
+ ): UITransformationResponse {
183
84
  const products: CarouselProduct[] = data
184
85
  .filter(item => this.isProductData(item))
185
- .map(item => this.extractProductInfo(item))
86
+ .map(item => this.extractProductInfo(item, config, trainedSchema))
186
87
  .filter((p): p is CarouselProduct => p !== null);
187
88
 
188
89
  return {
@@ -202,11 +103,15 @@ export class UITransformer {
202
103
  const categories = this.detectCategories(data);
203
104
  const categoryData = this.aggregateByCategory(data, categories);
204
105
 
205
- const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => ({
206
- label,
207
- value: count as number,
208
- inStock: this.checkStockStatus(label, data),
209
- }));
106
+ const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => {
107
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
108
+ return {
109
+ label,
110
+ value: count as number,
111
+ inStockCount,
112
+ outOfStockCount,
113
+ };
114
+ });
210
115
 
211
116
  return {
212
117
  type: 'pie_chart',
@@ -225,11 +130,15 @@ export class UITransformer {
225
130
  const categories = this.detectCategories(data);
226
131
  const categoryData = this.aggregateByCategory(data, categories);
227
132
 
228
- const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => ({
229
- category,
230
- value: value as number,
231
- inStock: this.checkStockStatus(category, data),
232
- }));
133
+ const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => {
134
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(category, data);
135
+ return {
136
+ category,
137
+ value: value as number,
138
+ inStockCount,
139
+ outOfStockCount,
140
+ };
141
+ });
233
142
 
234
143
  return {
235
144
  type: 'bar_chart',
@@ -310,16 +219,171 @@ export class UITransformer {
310
219
  };
311
220
  }
312
221
 
222
+ static parseTransformationResponse(raw: string): UITransformationResponse | null {
223
+ const payloadText = this.extractJsonCandidate(raw);
224
+ if (!payloadText) return null;
225
+
226
+ try {
227
+ const parsed = JSON.parse(payloadText);
228
+ return this.normalizeTransformation(parsed);
229
+ } catch {
230
+ return null;
231
+ }
232
+ }
233
+
234
+ private static extractJsonCandidate(raw: string): string | null {
235
+ if (!raw) return null;
236
+
237
+ const cleaned = raw
238
+ .replace(/```(?:json|chart|ui)?\s*/gi, '')
239
+ .replace(/```/g, '')
240
+ .trim();
241
+
242
+ const start = cleaned.indexOf('{');
243
+ if (start === -1) return null;
244
+
245
+ let depth = 0;
246
+ let inString = false;
247
+ let escape = false;
248
+
249
+ for (let i = start; i < cleaned.length; i += 1) {
250
+ const char = cleaned[i];
251
+ if (escape) {
252
+ escape = false;
253
+ continue;
254
+ }
255
+ if (char === '\\') {
256
+ escape = true;
257
+ continue;
258
+ }
259
+ if (char === '"') {
260
+ inString = !inString;
261
+ continue;
262
+ }
263
+ if (inString) continue;
264
+
265
+ if (char === '{') depth += 1;
266
+ if (char === '}') {
267
+ depth -= 1;
268
+ if (depth === 0) {
269
+ return cleaned.slice(start, i + 1);
270
+ }
271
+ }
272
+ }
273
+
274
+ return null;
275
+ }
276
+
277
+ private static normalizeTransformation(payload: unknown): UITransformationResponse | null {
278
+ if (!payload || typeof payload !== 'object') return null;
279
+
280
+ const payloadObj = payload as Record<string, unknown>;
281
+ const type = this.normalizeVisualizationType(String(payloadObj.type ?? payloadObj.view ?? payloadObj.chartType ?? ''));
282
+ if (!type) return null;
283
+
284
+ const title = String(payloadObj.title ?? payloadObj.heading ?? 'Visualization');
285
+ const description = payloadObj.description ? String(payloadObj.description) : undefined;
286
+ const rawData = payloadObj.data ?? payloadObj.table ?? payloadObj.rows ?? payloadObj.items ?? payloadObj.content ?? null;
287
+
288
+ const data = type === 'text' && typeof rawData === 'string'
289
+ ? { content: rawData }
290
+ : rawData;
291
+
292
+ const transformation: UITransformationResponse = {
293
+ type,
294
+ title,
295
+ description,
296
+ data,
297
+ };
298
+
299
+ return this.validateTransformation(transformation) ? transformation : null;
300
+ }
301
+
302
+ private static normalizeVisualizationType(type: string): VisualizationType | null {
303
+ const mapping: Record<string, VisualizationType> = {
304
+ pie: 'pie_chart',
305
+ pie_chart: 'pie_chart',
306
+ bar: 'bar_chart',
307
+ bar_chart: 'bar_chart',
308
+ line: 'line_chart',
309
+ line_chart: 'line_chart',
310
+ radar: 'radar_chart',
311
+ radar_chart: 'radar_chart',
312
+ table: 'table',
313
+ text: 'text',
314
+ product_carousel: 'product_carousel',
315
+ carousel: 'carousel',
316
+ };
317
+
318
+ return mapping[type.toLowerCase()] ?? null;
319
+ }
320
+
321
+ private static validateTransformation(transformation: UITransformationResponse): boolean {
322
+ const { type, data } = transformation;
323
+
324
+ switch (type) {
325
+ case 'pie_chart':
326
+ case 'bar_chart':
327
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
328
+ item !== null && typeof item === 'object' &&
329
+ (typeof item.value === 'number' || !Number.isNaN(Number(item.value)))
330
+ );
331
+ case 'line_chart':
332
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
333
+ item !== null && typeof item === 'object' &&
334
+ 'timestamp' in item &&
335
+ (typeof item.value === 'number' || !Number.isNaN(Number(item.value)))
336
+ );
337
+ case 'radar_chart':
338
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
339
+ item !== null && typeof item === 'object' &&
340
+ 'attribute' in item
341
+ );
342
+ case 'table':
343
+ return (
344
+ typeof data === 'object' &&
345
+ data !== null &&
346
+ Array.isArray((data as Record<string, unknown>).columns) &&
347
+ Array.isArray((data as Record<string, unknown>).rows)
348
+ );
349
+ case 'text':
350
+ return (
351
+ typeof data === 'object' &&
352
+ data !== null &&
353
+ typeof (data as Record<string, unknown>).content === 'string'
354
+ );
355
+ case 'product_carousel':
356
+ case 'carousel':
357
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
358
+ item !== null && typeof item === 'object' && (('id' in item) || ('name' in item))
359
+ );
360
+ default:
361
+ return false;
362
+ }
363
+ }
364
+
313
365
  /**
314
366
  * Helper: Check if data item is product-related
315
367
  */
316
368
  private static isProductData(item: VectorMatch): boolean {
317
369
  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
- );
370
+ const productKeywords = [
371
+ 'product', 'price', 'stock', 'item', 'sku', 'brand', 'model', 'msrp', 'inventory', 'buy', 'shop',
372
+ 'beauty', 'care', 'cosmetic', 'facial', 'cream', 'serum', 'mask', 'makeup', 'fragrance'
373
+ ];
374
+
375
+ // Check content for keywords
376
+ const hasKeywords = productKeywords.some(kw => content.includes(kw));
377
+
378
+ // Check metadata keys
379
+ const hasMetadataKey = Object.keys(item.metadata || {}).some(k =>
380
+ ['name', 'price', 'product', 'sku', 'brand', 'model', 'cost', 'item'].includes(k.toLowerCase())
381
+ );
382
+
383
+ // Failsafe: if we have a dollar sign followed by a number, it's likely a product
384
+ const hasPricePattern = /\$\s*\d+/.test(content);
385
+
386
+ return hasKeywords || hasMetadataKey || hasPricePattern;
323
387
  }
324
388
 
325
389
  /**
@@ -327,41 +391,156 @@ export class UITransformer {
327
391
  */
328
392
  private static isTimeSeriesData(item: VectorMatch): boolean {
329
393
  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
- );
394
+ const timeKeywords = ['trend', 'historical', 'growth', 'decline', 'change', 'increase', 'decrease'];
395
+ const hasTimeKeyword = timeKeywords.some(kw => content.includes(kw));
396
+
397
+ const metadata = item.metadata || {};
398
+ const maybeDateKeys = Object.keys(metadata).filter((k) =>
399
+ ['date', 'timestamp', 'time', 'period'].includes(k.toLowerCase())
400
+ );
401
+ const hasValidDateValue = maybeDateKeys.some((key) => {
402
+ const value = metadata[key];
403
+ if (typeof value === 'string') {
404
+ return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
405
+ }
406
+ return typeof value === 'number';
407
+ });
408
+
409
+ return hasTimeKeyword || hasValidDateValue;
410
+ }
411
+
412
+ private static shouldShowCategoryChart(query: string, categories: string[]): boolean {
413
+ if (categories.length < 2) {
414
+ return false;
415
+ }
416
+
417
+ const normalized = query.toLowerCase();
418
+ const chartKeywords = [
419
+ 'distribution',
420
+ 'breakdown',
421
+ 'by category',
422
+ 'by type',
423
+ 'compare',
424
+ 'share',
425
+ 'percentage',
426
+ 'segmentation',
427
+ 'split',
428
+ 'category breakdown',
429
+ 'category distribution',
430
+ ];
431
+
432
+ return chartKeywords.some(keyword => normalized.includes(keyword));
433
+ }
434
+
435
+ private static isTrendQuery(query: string): boolean {
436
+ const normalized = query.toLowerCase();
437
+ const trendKeywords = [
438
+ 'trend',
439
+ 'over time',
440
+ 'historical',
441
+ 'growth',
442
+ 'decline',
443
+ 'increase',
444
+ 'decrease',
445
+ 'year',
446
+ 'month',
447
+ 'week',
448
+ 'day',
449
+ 'comparison',
450
+ 'compare',
451
+ 'changes',
452
+ 'timeline',
453
+ ];
454
+
455
+ return trendKeywords.some(keyword => normalized.includes(keyword));
456
+ }
457
+
458
+ private static isStockQuery(query: string): boolean {
459
+ const normalized = query.toLowerCase();
460
+ return (
461
+ normalized.includes('in stock') ||
462
+ normalized.includes('available') ||
463
+ normalized.includes('availability') ||
464
+ normalized.includes('inventory') ||
465
+ normalized.includes('stock status')
466
+ );
335
467
  }
336
468
 
337
469
  /**
338
- * Helper: Extract product information from vector match
470
+ * Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
339
471
  */
340
- private static extractProductInfo(item: VectorMatch): CarouselProduct | null {
341
- const meta = item.metadata || {};
472
+ private static getDynamicVal(
473
+ meta: Record<string, unknown> | undefined,
474
+ uiKey: string,
475
+ config?: import('../config/RagConfig').RagConfig,
476
+ trainedSchema?: import('./SchemaMapper').SchemaMap
477
+ ): unknown {
478
+ if (!meta) return undefined;
479
+
480
+ const mapping = config?.rag?.uiMapping;
481
+ if (mapping && mapping[uiKey]) {
482
+ const mappedKey = mapping[uiKey];
483
+ if (meta[mappedKey] !== undefined) return meta[mappedKey];
484
+ }
342
485
 
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
- };
486
+ if (trainedSchema && typeof trainedSchema === 'object' && trainedSchema !== null) {
487
+ const trainedKey = (trainedSchema as Record<string, unknown>)[uiKey];
488
+ if (trainedKey && meta[trainedKey as string] !== undefined) return meta[trainedKey as string];
354
489
  }
355
490
 
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);
491
+ const synonyms = this.SYNONYMS[uiKey] || [];
492
+ const searchKeys = [uiKey, ...synonyms].map((k) => k.toLowerCase());
493
+
494
+ const foundDirectKey = Object.keys(meta).find((k) => searchKeys.includes(k.toLowerCase()));
495
+ if (foundDirectKey) return meta[foundDirectKey];
496
+
497
+ const foundPartialKey = Object.keys(meta).find((k) => {
498
+ const keyLow = k.toLowerCase();
499
+ return searchKeys.some((sk) => keyLow.includes(sk) || sk.includes(keyLow));
500
+ });
501
+
502
+ return foundPartialKey ? meta[foundPartialKey] : undefined;
503
+ }
504
+
505
+ private static extractProductInfo(
506
+ item: VectorMatch,
507
+ config?: import('../config/RagConfig').RagConfig,
508
+ trainedSchema?: import('./SchemaMapper').SchemaMap
509
+ ): CarouselProduct | null {
510
+ const meta = item.metadata || {};
511
+
512
+ const name = this.getDynamicVal(meta, 'name', config, trainedSchema);
513
+ const price = this.getDynamicVal(meta, 'price', config, trainedSchema);
514
+ const brand = this.getDynamicVal(meta, 'brand', config, trainedSchema);
515
+
516
+ // If we have at least a name or a clear product-like structure in content
517
+ if (name || this.isProductData(item)) {
518
+ // Robust name extraction: metadata > regex > content snippet
519
+ let finalName = name ? String(name) : undefined;
520
+ if (!finalName) {
521
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
522
+ finalName = nameMatch ? nameMatch[1].trim() : item.content.split('\n')[0].substring(0, 60);
523
+ }
524
+
525
+ // Robust price extraction: metadata > regex
526
+ let finalPrice: string | number | undefined =
527
+ typeof price === 'number' || typeof price === 'string'
528
+ ? price
529
+ : undefined;
530
+
531
+ if (!finalPrice) {
532
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
533
+ if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, '');
534
+ }
535
+
536
+ const imageValue = this.getDynamicVal(meta, 'image', config, trainedSchema);
359
537
 
360
- if (nameMatch) {
361
538
  return {
362
539
  id: item.id,
363
- name: nameMatch[1],
364
- price: priceMatch ? parseFloat(priceMatch[1]) : undefined,
540
+ name: finalName,
541
+ price: finalPrice,
542
+ image: typeof imageValue === 'string' ? imageValue : undefined,
543
+ brand: brand ? String(brand) : undefined,
365
544
  description: item.content,
366
545
  inStock: this.determineStockStatus(item),
367
546
  };
@@ -391,7 +570,15 @@ export class UITransformer {
391
570
  tags.forEach(t => categories.add(String(t)));
392
571
  }
393
572
 
394
- // Parse from content
573
+ // Parse category strings from content lines like "Shoes & Footwear: • Product"
574
+ const contentCategories = Array.from(new Set(
575
+ Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm))
576
+ .map(match => match[1].trim())
577
+ .filter(Boolean)
578
+ ));
579
+ contentCategories.forEach((category) => categories.add(category));
580
+
581
+ // Parse from structured category hints in content
395
582
  const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
396
583
  if (categoryMatch) {
397
584
  categories.add(categoryMatch[1].trim());
@@ -477,15 +664,25 @@ export class UITransformer {
477
664
  }
478
665
 
479
666
  /**
480
- * Helper: Check stock status
667
+ * Helper: Calculate stock counts by category
481
668
  */
482
- private static checkStockStatus(category: string, data: VectorMatch[]): boolean {
483
- const item = data.find(d => {
669
+ private static calculateStockCounts(category: string, data: VectorMatch[]): { inStockCount: number; outOfStockCount: number } {
670
+ let inStock = 0;
671
+ let outOfStock = 0;
672
+
673
+ data.forEach(d => {
484
674
  const meta = d.metadata || {};
485
- return meta.category === category || meta.type === category;
675
+ const itemCategory = (meta.category || meta.type || 'Other') as string;
676
+ if (itemCategory === category) {
677
+ if (this.determineStockStatus(d)) {
678
+ inStock++;
679
+ } else {
680
+ outOfStock++;
681
+ }
682
+ }
486
683
  });
487
684
 
488
- return this.determineStockStatus(item || ({} as VectorMatch));
685
+ return { inStockCount: inStock, outOfStockCount: outOfStock };
489
686
  }
490
687
 
491
688
  /**
@@ -532,4 +729,137 @@ export class UITransformer {
532
729
 
533
730
  return fieldCount.size > 2;
534
731
  }
732
+
733
+ // ─── LLM-Driven Visualization Decision ────────────────────────────────────
734
+
735
+ /**
736
+ * analyzeAndDecide — sends user question + RAG data to the LLM with a
737
+ * structured system prompt and parses the JSON response into a
738
+ * UITransformationResponse.
739
+ *
740
+ * This is the recommended entry point for production use. The heuristic
741
+ * `transform()` method is used as a fallback if the LLM call fails.
742
+ *
743
+ * System prompt instructs the LLM to:
744
+ * - Analyze the question and retrieved data
745
+ * - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
746
+ * - Return a strict JSON object — no prose, no markdown fences
747
+ *
748
+ * @param query - the original user question
749
+ * @param sources - vector DB matches returned by RAG retrieval
750
+ * @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
751
+ * @returns - a validated UITransformationResponse (type + title + description + data)
752
+ */
753
+ static async analyzeAndDecide(
754
+ query: string,
755
+ sources: VectorMatch[],
756
+ llm: ILLMProvider,
757
+ ): Promise<UITransformationResponse> {
758
+ try {
759
+ const context = this.buildContextSummary(sources);
760
+ const systemPrompt = this.buildVisualizationSystemPrompt();
761
+
762
+ const userPrompt = [
763
+ `USER QUESTION: ${query}`,
764
+ '',
765
+ 'RETRIEVED DATA (JSON):',
766
+ context,
767
+ ].join('\n');
768
+
769
+ const rawResponse = await llm.chat(
770
+ [{ role: 'user', content: userPrompt }],
771
+ '',
772
+ { systemPrompt, temperature: 0 },
773
+ );
774
+
775
+ const parsed = this.parseTransformationResponse(rawResponse);
776
+ if (parsed) {
777
+ console.debug('[UITransformer] LLM chose visualization type:', parsed.type);
778
+ return parsed;
779
+ }
780
+
781
+ console.warn('[UITransformer] LLM returned unparseable response; falling back to heuristic.');
782
+ } catch (err) {
783
+ console.warn('[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.', err);
784
+ }
785
+
786
+ // Fallback: keyword-heuristic transform
787
+ return this.transform(query, sources);
788
+ }
789
+
790
+ /**
791
+ * Build the system prompt that instructs the LLM to return a visualization JSON.
792
+ */
793
+ private static buildVisualizationSystemPrompt(): string {
794
+ return `You are a data visualization expert embedded in a RAG chat system.
795
+ You will receive a user question and structured data retrieved from a vector database.
796
+ Your ONLY job is to analyze this information and return a single JSON object that tells
797
+ the frontend how to visualize it.
798
+
799
+ Return ONLY a valid JSON object — no markdown code fences, no explanation, no prose.
800
+
801
+ The JSON must have this exact shape:
802
+ {
803
+ "type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
804
+ "title": "A concise, descriptive title for the visualization",
805
+ "description": "One sentence describing what the visualization shows",
806
+ "data": <structured data — see rules below>
807
+ }
808
+
809
+ DATA SHAPE per type:
810
+ - bar_chart: array of { "category": string, "value": number }
811
+ - line_chart: array of { "timestamp": string, "value": number, "label": string }
812
+ - pie_chart: array of { "label": string, "value": number }
813
+ - radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
814
+ Example for radar_chart:
815
+ [
816
+ { "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
817
+ { "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
818
+ { "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
819
+ ]
820
+ - table: { "columns": string[], "rows": (string|number)[][] }
821
+ - text: { "content": "<prose answer>" }
822
+
823
+ DECISION RULES (follow strictly):
824
+ 1. bar_chart → comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
825
+ 2. line_chart → trends or changes over time (dates, months, years, sequential events)
826
+ 3. pie_chart → proportional breakdown or percentage distribution
827
+ 4. radar_chart → comparing 2 or more products across MULTIPLE attributes (e.g. comparing features, ratings, or characteristics of 2 specific products). If the user asks to "compare" products and the data contains multiple dimensions or ratings for them, you MUST use this.
828
+ 5. table → multi-field structured records where each item has ≥ 3 attributes
829
+ 6. text → conversational or free-form answers where no chart adds value
830
+
831
+ IMPORTANT:
832
+ - Aggregate numeric values from the raw data — do not pass raw object arrays as data.
833
+ - Ensure all "value" fields are numbers, not strings.
834
+ - For bar/line/pie, keep at most 12 data points for readability.
835
+ - Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
836
+ }
837
+
838
+ /**
839
+ * Serialize retrieved vector matches into a compact JSON context string.
840
+ * Limits the total character count to avoid exceeding LLM context windows.
841
+ */
842
+ private static buildContextSummary(sources: VectorMatch[], maxChars = 6000): string {
843
+ const items = sources.map((s, i) => ({
844
+ index: i + 1,
845
+ content: s.content?.substring(0, 400) ?? '',
846
+ metadata: s.metadata ?? {},
847
+ score: s.score ?? 0,
848
+ }));
849
+
850
+ const full = JSON.stringify(items, null, 2);
851
+ if (full.length <= maxChars) return full;
852
+
853
+ // Truncate: include as many items as fit
854
+ const partial: typeof items = [];
855
+ let chars = 2; // for []
856
+ for (const item of items) {
857
+ const chunk = JSON.stringify(item);
858
+ if (chars + chunk.length + 2 > maxChars) break;
859
+ partial.push(item);
860
+ chars += chunk.length + 2;
861
+ }
862
+ return JSON.stringify(partial, null, 2) + '\n// ... truncated';
863
+ }
535
864
  }
865
+