@retrivora-ai/rag-engine 1.8.0 → 1.8.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 (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -1,79 +1,6 @@
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
- inStockCount?: number;
31
- outOfStockCount?: number;
32
- [key: string]: unknown;
33
- }
34
-
35
- /**
36
- * Bar chart data structure
37
- */
38
- export interface BarChartData {
39
- category: string;
40
- value: number;
41
- inStockCount?: number;
42
- outOfStockCount?: number;
43
- [key: string]: unknown;
44
- }
45
-
46
- /**
47
- * Line chart data point
48
- */
49
- export interface LineChartDataPoint {
50
- timestamp: string | number;
51
- value: number;
52
- label?: string;
53
- [key: string]: unknown;
54
- }
55
-
56
- /**
57
- * Table representation
58
- */
59
- export interface TableData {
60
- columns: string[];
61
- rows: (string | number | boolean)[][];
62
- }
63
-
64
- /**
65
- * Product carousel item
66
- */
67
- export interface CarouselProduct {
68
- id: string | number;
69
- name: string;
70
- price?: number | string;
71
- image?: string;
72
- inStock?: boolean;
73
- brand?: string;
74
- description?: string;
75
- [key: string]: unknown;
76
- }
1
+ import { VectorMatch, UITransformationResponse, PieChartData, LineChartDataPoint, TableData, CarouselProduct, VisualizationType } from '../types';
2
+ import { ILLMProvider } from '../llm/ILLMProvider';
3
+ import { resolveMetadataValue } from './synonyms';
77
4
 
78
5
  /**
79
6
  * UITransformer — Converts vector database results into structured UI representations
@@ -82,109 +9,70 @@ export interface CarouselProduct {
82
9
  * format based on the content and structure of the data.
83
10
  */
84
11
  export class UITransformer {
12
+
85
13
  /**
86
14
  * Main transformation method
87
- * Analyzes user query and retrieved data to determine the best visualization format
88
- *
89
- * @param userQuery - The original user question/query
90
- * @param retrievedData - Vector database retrieval results
91
- * @returns Structured JSON response for UI rendering
15
+ * Analyzes user query and retrieved data to determine if a product carousel is needed.
92
16
  */
93
17
  static transform(
94
18
  userQuery: string,
95
- retrievedData: VectorMatch[]
19
+ retrievedData: VectorMatch[],
20
+ config?: import('../config/RagConfig').RagConfig,
21
+ trainedSchema?: import('./SchemaMapper').SchemaMap
96
22
  ): UITransformationResponse {
97
23
  if (!retrievedData || retrievedData.length === 0) {
98
24
  return this.createTextResponse('No data available', 'No relevant data found for your query.');
99
25
  }
100
26
 
101
- // Analyze the retrieved data to determine best format
102
- const analysis = this.analyzeData(userQuery, retrievedData);
27
+ const isStockRequest = this.isStockQuery(userQuery);
28
+ const filteredData = isStockRequest
29
+ ? retrievedData.filter(item => this.determineStockStatus(item))
30
+ : retrievedData;
103
31
 
104
- // Route to appropriate transformer based on analysis
105
- switch (analysis.suggestedType) {
106
- case 'product_carousel':
107
- return this.transformToProductCarousel(retrievedData);
108
- case 'pie_chart':
109
- return this.transformToPieChart(retrievedData);
110
- case 'bar_chart':
111
- return this.transformToBarChart(retrievedData);
112
- case 'line_chart':
113
- return this.transformToLineChart(retrievedData);
114
- case 'table':
115
- return this.transformToTable(retrievedData);
116
- default:
117
- return this.transformToText(retrievedData);
118
- }
119
- }
32
+ const categories = this.detectCategories(filteredData);
33
+ const hasProducts = filteredData.some(item => this.isProductData(item));
34
+ const isTimeSeries = filteredData.some(item => this.isTimeSeriesData(item));
35
+ const isTrendQuery = this.isTrendQuery(userQuery);
120
36
 
121
- /**
122
- * Analyzes the data and query to suggest the best visualization type
123
- */
124
- private static analyzeData(
125
- query: string,
126
- data: VectorMatch[]
127
- ): {
128
- suggestedType: VisualizationType;
129
- confidence: number;
130
- hasProducts: boolean;
131
- hasTimeSeries: boolean;
132
- hasCategories: boolean;
133
- } {
134
- const queryLower = query.toLowerCase();
135
- const hasProducts = data.some(item =>
136
- this.isProductData(item)
137
- );
138
- const hasTimeSeries = data.some(item =>
139
- this.isTimeSeriesData(item)
140
- );
141
- const hasCategories = this.detectCategories(data).length > 0;
142
- const isDistributionQuery =
143
- queryLower.includes('distribution') ||
144
- queryLower.includes('breakdown') ||
145
- queryLower.includes('percentage') ||
146
- queryLower.includes('proportion');
147
- const isComparisonQuery =
148
- queryLower.includes('compare') ||
149
- queryLower.includes('vs') ||
150
- queryLower.includes('difference');
151
- const isTrendQuery =
152
- queryLower.includes('trend') ||
153
- queryLower.includes('over time') ||
154
- queryLower.includes('growth') ||
155
- queryLower.includes('decline');
156
-
157
- // Decision logic
158
- if (hasProducts && !hasTimeSeries && !isDistributionQuery) {
159
- return { suggestedType: 'product_carousel', confidence: 0.95, hasProducts, hasTimeSeries, hasCategories };
37
+ // 1. High Priority: Specific visualizations for trends
38
+ if (isTrendQuery && isTimeSeries) {
39
+ return this.transformToLineChart(filteredData);
160
40
  }
161
41
 
162
- if (isTrendQuery && hasTimeSeries) {
163
- return { suggestedType: 'line_chart', confidence: 0.9, hasProducts, hasTimeSeries, hasCategories };
42
+ // 2. High Priority: Products (Always prefer carousel for products unless it's a specific breakdown request)
43
+ if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
44
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
164
45
  }
165
46
 
166
- if (isDistributionQuery && hasCategories) {
167
- return { suggestedType: 'pie_chart', confidence: 0.85, hasProducts, hasTimeSeries, hasCategories };
47
+ // 3. Category breakdowns
48
+ if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
49
+ return this.transformToPieChart(filteredData);
168
50
  }
169
51
 
170
- if (isComparisonQuery && hasCategories && data.length > 2) {
171
- return { suggestedType: 'bar_chart', confidence: 0.8, hasProducts, hasTimeSeries, hasCategories };
52
+ // 4. Secondary: Products as carousel if not already handled
53
+ if (hasProducts) {
54
+ return this.transformToProductCarousel(filteredData, config, trainedSchema);
172
55
  }
173
56
 
174
- if (data.length > 5 && this.hasMultipleFields(data)) {
175
- return { suggestedType: 'table', confidence: 0.75, hasProducts, hasTimeSeries, hasCategories };
57
+ // 5. Detailed data without clear product structure -> Table
58
+ if (this.hasMultipleFields(filteredData)) {
59
+ return this.transformToTable(filteredData);
176
60
  }
177
61
 
178
- return { suggestedType: 'text', confidence: 0.5, hasProducts, hasTimeSeries, hasCategories };
62
+ return this.transformToText(filteredData);
179
63
  }
180
64
 
181
65
  /**
182
66
  * Transform data to product carousel format
183
67
  */
184
- private static transformToProductCarousel(data: VectorMatch[]): UITransformationResponse {
68
+ private static transformToProductCarousel(
69
+ data: VectorMatch[],
70
+ config?: import('../config/RagConfig').RagConfig,
71
+ trainedSchema?: import('./SchemaMapper').SchemaMap
72
+ ): UITransformationResponse {
185
73
  const products: CarouselProduct[] = data
186
74
  .filter(item => this.isProductData(item))
187
- .map(item => this.extractProductInfo(item))
75
+ .map(item => this.extractProductInfo(item, config, trainedSchema))
188
76
  .filter((p): p is CarouselProduct => p !== null);
189
77
 
190
78
  return {
@@ -222,33 +110,6 @@ export class UITransformer {
222
110
  };
223
111
  }
224
112
 
225
- /**
226
- * Transform data to bar chart format
227
- */
228
- private static transformToBarChart(
229
- data: VectorMatch[]
230
- ): UITransformationResponse {
231
- const categories = this.detectCategories(data);
232
- const categoryData = this.aggregateByCategory(data, categories);
233
-
234
- const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => {
235
- const { inStockCount, outOfStockCount } = this.calculateStockCounts(category, data);
236
- return {
237
- category,
238
- value: value as number,
239
- inStockCount,
240
- outOfStockCount,
241
- };
242
- });
243
-
244
- return {
245
- type: 'bar_chart',
246
- title: 'Comparison by Category',
247
- description: `Comparing ${categories.length} categories`,
248
- data: barData,
249
- };
250
- }
251
-
252
113
  /**
253
114
  * Transform data to line chart format
254
115
  */
@@ -320,16 +181,171 @@ export class UITransformer {
320
181
  };
321
182
  }
322
183
 
184
+ static parseTransformationResponse(raw: string): UITransformationResponse | null {
185
+ const payloadText = this.extractJsonCandidate(raw);
186
+ if (!payloadText) return null;
187
+
188
+ try {
189
+ const parsed = JSON.parse(payloadText);
190
+ return this.normalizeTransformation(parsed);
191
+ } catch {
192
+ return null;
193
+ }
194
+ }
195
+
196
+ private static extractJsonCandidate(raw: string): string | null {
197
+ if (!raw) return null;
198
+
199
+ const cleaned = raw
200
+ .replace(/```(?:json|chart|ui)?\s*/gi, '')
201
+ .replace(/```/g, '')
202
+ .trim();
203
+
204
+ const start = cleaned.indexOf('{');
205
+ if (start === -1) return null;
206
+
207
+ let depth = 0;
208
+ let inString = false;
209
+ let escape = false;
210
+
211
+ for (let i = start; i < cleaned.length; i += 1) {
212
+ const char = cleaned[i];
213
+ if (escape) {
214
+ escape = false;
215
+ continue;
216
+ }
217
+ if (char === '\\') {
218
+ escape = true;
219
+ continue;
220
+ }
221
+ if (char === '"') {
222
+ inString = !inString;
223
+ continue;
224
+ }
225
+ if (inString) continue;
226
+
227
+ if (char === '{') depth += 1;
228
+ if (char === '}') {
229
+ depth -= 1;
230
+ if (depth === 0) {
231
+ return cleaned.slice(start, i + 1);
232
+ }
233
+ }
234
+ }
235
+
236
+ return null;
237
+ }
238
+
239
+ private static normalizeTransformation(payload: unknown): UITransformationResponse | null {
240
+ if (!payload || typeof payload !== 'object') return null;
241
+
242
+ const payloadObj = payload as Record<string, unknown>;
243
+ const type = this.normalizeVisualizationType(String(payloadObj.type ?? payloadObj.view ?? payloadObj.chartType ?? ''));
244
+ if (!type) return null;
245
+
246
+ const title = String(payloadObj.title ?? payloadObj.heading ?? 'Visualization');
247
+ const description = payloadObj.description ? String(payloadObj.description) : undefined;
248
+ const rawData = payloadObj.data ?? payloadObj.table ?? payloadObj.rows ?? payloadObj.items ?? payloadObj.content ?? null;
249
+
250
+ const data = type === 'text' && typeof rawData === 'string'
251
+ ? { content: rawData }
252
+ : rawData;
253
+
254
+ const transformation: UITransformationResponse = {
255
+ type,
256
+ title,
257
+ description,
258
+ data,
259
+ };
260
+
261
+ return this.validateTransformation(transformation) ? transformation : null;
262
+ }
263
+
264
+ private static normalizeVisualizationType(type: string): VisualizationType | null {
265
+ const mapping: Record<string, VisualizationType> = {
266
+ pie: 'pie_chart',
267
+ pie_chart: 'pie_chart',
268
+ bar: 'bar_chart',
269
+ bar_chart: 'bar_chart',
270
+ line: 'line_chart',
271
+ line_chart: 'line_chart',
272
+ radar: 'radar_chart',
273
+ radar_chart: 'radar_chart',
274
+ table: 'table',
275
+ text: 'text',
276
+ product_carousel: 'product_carousel',
277
+ carousel: 'carousel',
278
+ };
279
+
280
+ return mapping[type.toLowerCase()] ?? null;
281
+ }
282
+
283
+ private static validateTransformation(transformation: UITransformationResponse): boolean {
284
+ const { type, data } = transformation;
285
+
286
+ switch (type) {
287
+ case 'pie_chart':
288
+ case 'bar_chart':
289
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
290
+ item !== null && typeof item === 'object' &&
291
+ (typeof item.value === 'number' || !Number.isNaN(Number(item.value)))
292
+ );
293
+ case 'line_chart':
294
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
295
+ item !== null && typeof item === 'object' &&
296
+ 'timestamp' in item &&
297
+ (typeof item.value === 'number' || !Number.isNaN(Number(item.value)))
298
+ );
299
+ case 'radar_chart':
300
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
301
+ item !== null && typeof item === 'object' &&
302
+ 'attribute' in item
303
+ );
304
+ case 'table':
305
+ return (
306
+ typeof data === 'object' &&
307
+ data !== null &&
308
+ Array.isArray((data as Record<string, unknown>).columns) &&
309
+ Array.isArray((data as Record<string, unknown>).rows)
310
+ );
311
+ case 'text':
312
+ return (
313
+ typeof data === 'object' &&
314
+ data !== null &&
315
+ typeof (data as Record<string, unknown>).content === 'string'
316
+ );
317
+ case 'product_carousel':
318
+ case 'carousel':
319
+ return Array.isArray(data) && data.every((item): item is Record<string, unknown> =>
320
+ item !== null && typeof item === 'object' && (('id' in item) || ('name' in item))
321
+ );
322
+ default:
323
+ return false;
324
+ }
325
+ }
326
+
323
327
  /**
324
328
  * Helper: Check if data item is product-related
325
329
  */
326
330
  private static isProductData(item: VectorMatch): boolean {
327
331
  const content = (item.content || '').toLowerCase();
328
- const productKeywords = ['product', 'price', 'stock', 'item', 'sku', 'brand', 'model'];
329
- return productKeywords.some(kw => content.includes(kw)) ||
330
- Object.keys(item.metadata || {}).some(k =>
331
- ['name', 'price', 'product', 'sku'].includes(k.toLowerCase())
332
- );
332
+ const productKeywords = [
333
+ 'product', 'price', 'stock', 'item', 'sku', 'brand', 'model', 'msrp', 'inventory', 'buy', 'shop',
334
+ 'beauty', 'care', 'cosmetic', 'facial', 'cream', 'serum', 'mask', 'makeup', 'fragrance'
335
+ ];
336
+
337
+ // Check content for keywords
338
+ const hasKeywords = productKeywords.some(kw => content.includes(kw));
339
+
340
+ // Check metadata keys
341
+ const hasMetadataKey = Object.keys(item.metadata || {}).some(k =>
342
+ ['name', 'price', 'product', 'sku', 'brand', 'model', 'cost', 'item'].includes(k.toLowerCase())
343
+ );
344
+
345
+ // Failsafe: if we have a dollar sign followed by a number, it's likely a product
346
+ const hasPricePattern = /\$\s*\d+/.test(content);
347
+
348
+ return hasKeywords || hasMetadataKey || hasPricePattern;
333
349
  }
334
350
 
335
351
  /**
@@ -337,41 +353,145 @@ export class UITransformer {
337
353
  */
338
354
  private static isTimeSeriesData(item: VectorMatch): boolean {
339
355
  const content = (item.content || '').toLowerCase();
340
- const timeKeywords = ['date', 'time', 'year', 'month', 'week', 'day', 'hour', 'trend', 'historical'];
341
- return timeKeywords.some(kw => content.includes(kw)) ||
342
- Object.keys(item.metadata || {}).some(k =>
343
- ['date', 'timestamp', 'time', 'period'].includes(k.toLowerCase())
344
- );
356
+ const timeKeywords = ['trend', 'historical', 'growth', 'decline', 'change', 'increase', 'decrease'];
357
+ const hasTimeKeyword = timeKeywords.some(kw => content.includes(kw));
358
+
359
+ const metadata = item.metadata || {};
360
+ const maybeDateKeys = Object.keys(metadata).filter((k) =>
361
+ ['date', 'timestamp', 'time', 'period'].includes(k.toLowerCase())
362
+ );
363
+ const hasValidDateValue = maybeDateKeys.some((key) => {
364
+ const value = metadata[key];
365
+ if (typeof value === 'string') {
366
+ return !Number.isNaN(Date.parse(value)) && value.trim().length > 0;
367
+ }
368
+ return typeof value === 'number';
369
+ });
370
+
371
+ return hasTimeKeyword || hasValidDateValue;
372
+ }
373
+
374
+ private static shouldShowCategoryChart(query: string, categories: string[]): boolean {
375
+ if (categories.length < 2) {
376
+ return false;
377
+ }
378
+
379
+ const normalized = query.toLowerCase();
380
+ const chartKeywords = [
381
+ 'distribution',
382
+ 'breakdown',
383
+ 'by category',
384
+ 'by type',
385
+ 'compare',
386
+ 'share',
387
+ 'percentage',
388
+ 'segmentation',
389
+ 'split',
390
+ 'category breakdown',
391
+ 'category distribution',
392
+ ];
393
+
394
+ return chartKeywords.some(keyword => normalized.includes(keyword));
395
+ }
396
+
397
+ private static isTrendQuery(query: string): boolean {
398
+ const normalized = query.toLowerCase();
399
+ const trendKeywords = [
400
+ 'trend',
401
+ 'over time',
402
+ 'historical',
403
+ 'growth',
404
+ 'decline',
405
+ 'increase',
406
+ 'decrease',
407
+ 'year',
408
+ 'month',
409
+ 'week',
410
+ 'day',
411
+ 'comparison',
412
+ 'compare',
413
+ 'changes',
414
+ 'timeline',
415
+ ];
416
+
417
+ return trendKeywords.some(keyword => normalized.includes(keyword));
418
+ }
419
+
420
+ private static isStockQuery(query: string): boolean {
421
+ const normalized = query.toLowerCase();
422
+ return (
423
+ normalized.includes('in stock') ||
424
+ normalized.includes('available') ||
425
+ normalized.includes('availability') ||
426
+ normalized.includes('inventory') ||
427
+ normalized.includes('stock status')
428
+ );
345
429
  }
346
430
 
347
431
  /**
348
- * Helper: Extract product information from vector match
432
+ * Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
349
433
  */
350
- private static extractProductInfo(item: VectorMatch): CarouselProduct | null {
351
- const meta = item.metadata || {};
434
+ private static getDynamicVal(
435
+ meta: Record<string, unknown> | undefined,
436
+ uiKey: string,
437
+ config?: import('../config/RagConfig').RagConfig,
438
+ trainedSchema?: import('./SchemaMapper').SchemaMap
439
+ ): unknown {
440
+ if (!meta) return undefined;
441
+
442
+ const mapping = config?.rag?.uiMapping;
443
+ if (mapping && mapping[uiKey]) {
444
+ const mappedKey = mapping[uiKey];
445
+ if (meta[mappedKey] !== undefined) return meta[mappedKey];
446
+ }
352
447
 
353
- // Try to extract product info from metadata first
354
- if (meta.name || meta.product) {
355
- return {
356
- id: item.id,
357
- name: (meta.name || meta.product) as string,
358
- price: meta.price as string | number | undefined,
359
- image: (meta.image || meta.imageUrl) as string | undefined,
360
- brand: meta.brand as string | undefined,
361
- description: item.content,
362
- inStock: this.determineStockStatus(item),
363
- };
448
+ if (trainedSchema && typeof trainedSchema === 'object' && trainedSchema !== null) {
449
+ const trainedKey = (trainedSchema as Record<string, unknown>)[uiKey];
450
+ if (trainedKey && meta[trainedKey as string] !== undefined) return meta[trainedKey as string];
364
451
  }
365
452
 
366
- // Fallback: try to parse from content
367
- const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
368
- const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d.]+)/i);
453
+ return resolveMetadataValue(meta, uiKey);
454
+ }
455
+
456
+ private static extractProductInfo(
457
+ item: VectorMatch,
458
+ config?: import('../config/RagConfig').RagConfig,
459
+ trainedSchema?: import('./SchemaMapper').SchemaMap
460
+ ): CarouselProduct | null {
461
+ const meta = item.metadata || {};
462
+
463
+ const name = this.getDynamicVal(meta, 'name', config, trainedSchema);
464
+ const price = this.getDynamicVal(meta, 'price', config, trainedSchema);
465
+ const brand = this.getDynamicVal(meta, 'brand', config, trainedSchema);
466
+
467
+ // If we have at least a name or a clear product-like structure in content
468
+ if (name || this.isProductData(item)) {
469
+ // Robust name extraction: metadata > regex > content snippet
470
+ let finalName = name ? String(name) : undefined;
471
+ if (!finalName) {
472
+ const nameMatch = item.content.match(/(?:Product|Item|Name):\s*([^\n]+)/i);
473
+ finalName = nameMatch ? nameMatch[1].trim() : item.content.split('\n')[0].substring(0, 60);
474
+ }
475
+
476
+ // Robust price extraction: metadata > regex
477
+ let finalPrice: string | number | undefined =
478
+ typeof price === 'number' || typeof price === 'string'
479
+ ? price
480
+ : undefined;
481
+
482
+ if (!finalPrice) {
483
+ const priceMatch = item.content.match(/(?:Price|Cost):\s*\$?([\d,.]+)/i) || item.content.match(/\$\s*([\d,.]+)/);
484
+ if (priceMatch) finalPrice = priceMatch[1].replace(/,/g, '');
485
+ }
486
+
487
+ const imageValue = this.getDynamicVal(meta, 'image', config, trainedSchema);
369
488
 
370
- if (nameMatch) {
371
489
  return {
372
490
  id: item.id,
373
- name: nameMatch[1],
374
- price: priceMatch ? parseFloat(priceMatch[1]) : undefined,
491
+ name: finalName,
492
+ price: finalPrice,
493
+ image: typeof imageValue === 'string' ? imageValue : undefined,
494
+ brand: brand ? String(brand) : undefined,
375
495
  description: item.content,
376
496
  inStock: this.determineStockStatus(item),
377
497
  };
@@ -401,7 +521,15 @@ export class UITransformer {
401
521
  tags.forEach(t => categories.add(String(t)));
402
522
  }
403
523
 
404
- // Parse from content
524
+ // Parse category strings from content lines like "Shoes & Footwear: • Product"
525
+ const contentCategories = Array.from(new Set(
526
+ Array.from(item.content.matchAll(/^\s*([^:\n]+):\s*(?:•|\*|-)*/gm))
527
+ .map(match => match[1].trim())
528
+ .filter(Boolean)
529
+ ));
530
+ contentCategories.forEach((category) => categories.add(category));
531
+
532
+ // Parse from structured category hints in content
405
533
  const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
406
534
  if (categoryMatch) {
407
535
  categories.add(categoryMatch[1].trim());
@@ -428,7 +556,7 @@ export class UITransformer {
428
556
  const meta = item.metadata || {};
429
557
  const itemCategory = (meta.category || meta.type || 'Other') as string;
430
558
 
431
- if (result.hasOwnProperty(itemCategory)) {
559
+ if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
432
560
  result[itemCategory]++;
433
561
  } else {
434
562
  result['Other'] = (result['Other'] || 0) + 1;
@@ -552,4 +680,137 @@ export class UITransformer {
552
680
 
553
681
  return fieldCount.size > 2;
554
682
  }
683
+
684
+ // ─── LLM-Driven Visualization Decision ────────────────────────────────────
685
+
686
+ /**
687
+ * analyzeAndDecide — sends user question + RAG data to the LLM with a
688
+ * structured system prompt and parses the JSON response into a
689
+ * UITransformationResponse.
690
+ *
691
+ * This is the recommended entry point for production use. The heuristic
692
+ * `transform()` method is used as a fallback if the LLM call fails.
693
+ *
694
+ * System prompt instructs the LLM to:
695
+ * - Analyze the question and retrieved data
696
+ * - Choose the best visualization: bar_chart | line_chart | pie_chart | table | text
697
+ * - Return a strict JSON object — no prose, no markdown fences
698
+ *
699
+ * @param query - the original user question
700
+ * @param sources - vector DB matches returned by RAG retrieval
701
+ * @param llm - any ILLMProvider instance (OpenAI, Anthropic, Ollama, Gemini, REST…)
702
+ * @returns - a validated UITransformationResponse (type + title + description + data)
703
+ */
704
+ static async analyzeAndDecide(
705
+ query: string,
706
+ sources: VectorMatch[],
707
+ llm: ILLMProvider,
708
+ ): Promise<UITransformationResponse> {
709
+ try {
710
+ const context = this.buildContextSummary(sources);
711
+ const systemPrompt = this.buildVisualizationSystemPrompt();
712
+
713
+ const userPrompt = [
714
+ `USER QUESTION: ${query}`,
715
+ '',
716
+ 'RETRIEVED DATA (JSON):',
717
+ context,
718
+ ].join('\n');
719
+
720
+ const rawResponse = await llm.chat(
721
+ [{ role: 'user', content: userPrompt }],
722
+ '',
723
+ { systemPrompt, temperature: 0 },
724
+ );
725
+
726
+ const parsed = this.parseTransformationResponse(rawResponse);
727
+ if (parsed) {
728
+ console.debug('[UITransformer] LLM chose visualization type:', parsed.type);
729
+ return parsed;
730
+ }
731
+
732
+ console.warn('[UITransformer] LLM returned unparseable response; falling back to heuristic.');
733
+ } catch (err) {
734
+ console.warn('[UITransformer] analyzeAndDecide LLM call failed; falling back to heuristic.', err);
735
+ }
736
+
737
+ // Fallback: keyword-heuristic transform
738
+ return this.transform(query, sources);
739
+ }
740
+
741
+ /**
742
+ * Build the system prompt that instructs the LLM to return a visualization JSON.
743
+ */
744
+ private static buildVisualizationSystemPrompt(): string {
745
+ return `You are a data visualization expert embedded in a RAG chat system.
746
+ You will receive a user question and structured data retrieved from a vector database.
747
+ Your ONLY job is to analyze this information and return a single JSON object that tells
748
+ the frontend how to visualize it.
749
+
750
+ Return ONLY a valid JSON object — no markdown code fences, no explanation, no prose.
751
+
752
+ The JSON must have this exact shape:
753
+ {
754
+ "type": "bar_chart" | "line_chart" | "pie_chart" | "radar_chart" | "table" | "text",
755
+ "title": "A concise, descriptive title for the visualization",
756
+ "description": "One sentence describing what the visualization shows",
757
+ "data": <structured data — see rules below>
555
758
  }
759
+
760
+ DATA SHAPE per type:
761
+ - bar_chart: array of { "category": string, "value": number }
762
+ - line_chart: array of { "timestamp": string, "value": number, "label": string }
763
+ - pie_chart: array of { "label": string, "value": number }
764
+ - radar_chart: array of { "attribute": string, "[series1]": number, "[series2]": number, ... }
765
+ Example for radar_chart:
766
+ [
767
+ { "attribute": "Longevity", "Dolce Shine": 4, "CK One": 3 },
768
+ { "attribute": "Sillage", "Dolce Shine": 3, "CK One": 4 },
769
+ { "attribute": "Freshness", "Dolce Shine": 5, "CK One": 4 }
770
+ ]
771
+ - table: { "columns": string[], "rows": (string|number)[][] }
772
+ - text: { "content": "<prose answer>" }
773
+
774
+ DECISION RULES (follow strictly):
775
+ 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.
776
+ 2. line_chart → trends or changes over time (dates, months, years, sequential events)
777
+ 3. pie_chart → proportional breakdown or percentage distribution
778
+ 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.
779
+ 5. table → multi-field structured records where each item has ≥ 3 attributes
780
+ 6. text → conversational or free-form answers where no chart adds value
781
+
782
+ IMPORTANT:
783
+ - Aggregate numeric values from the raw data — do not pass raw object arrays as data.
784
+ - Ensure all "value" fields are numbers, not strings.
785
+ - For bar/line/pie, keep at most 12 data points for readability.
786
+ - Never include nested objects or arrays inside bar_chart / line_chart / pie_chart data items.`;
787
+ }
788
+
789
+ /**
790
+ * Serialize retrieved vector matches into a compact JSON context string.
791
+ * Limits the total character count to avoid exceeding LLM context windows.
792
+ */
793
+ private static buildContextSummary(sources: VectorMatch[], maxChars = 6000): string {
794
+ const items = sources.map((s, i) => ({
795
+ index: i + 1,
796
+ content: s.content?.substring(0, 400) ?? '',
797
+ metadata: s.metadata ?? {},
798
+ score: s.score ?? 0,
799
+ }));
800
+
801
+ const full = JSON.stringify(items, null, 2);
802
+ if (full.length <= maxChars) return full;
803
+
804
+ // Truncate: include as many items as fit
805
+ const partial: typeof items = [];
806
+ let chars = 2; // for []
807
+ for (const item of items) {
808
+ const chunk = JSON.stringify(item);
809
+ if (chars + chunk.length + 2 > maxChars) break;
810
+ partial.push(item);
811
+ chars += chunk.length + 2;
812
+ }
813
+ return JSON.stringify(partial, null, 2) + '\n// ... truncated';
814
+ }
815
+ }
816
+