@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.
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
- package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
- package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
- package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
- package/dist/chunk-ICKRMZQK.mjs +76 -0
- package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
- package/dist/chunk-OZFBG4BA.mjs +291 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +1269 -656
- package/dist/handlers/index.mjs +4 -1
- package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
- package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
- package/dist/index.d.mts +24 -4
- package/dist/index.d.ts +24 -4
- package/dist/index.js +1354 -826
- package/dist/index.mjs +1284 -795
- package/dist/server.d.mts +47 -27
- package/dist/server.d.ts +47 -27
- package/dist/server.js +1417 -829
- package/dist/server.mjs +164 -176
- package/package.json +6 -2
- package/src/app/api/upload/route.ts +4 -0
- package/src/app/constants.tsx +2 -2
- package/src/app/page.tsx +12 -322
- package/src/components/AmbientBackground.tsx +29 -0
- package/src/components/ArchitectureCard.tsx +17 -0
- package/src/components/ArchitectureCardsSection.tsx +15 -0
- package/src/components/ChatWindow.tsx +32 -0
- package/src/components/CodeViewer.tsx +51 -0
- package/src/components/ConfigProvider.tsx +1 -0
- package/src/components/DocViewer.tsx +37 -0
- package/src/components/DocumentUpload.tsx +44 -1
- package/src/components/Documentation.tsx +58 -0
- package/src/components/DynamicChart.tsx +27 -2
- package/src/components/Hero.tsx +59 -0
- package/src/components/HourglassLoader.tsx +87 -0
- package/src/components/Lifecycle.tsx +37 -0
- package/src/components/MarkdownComponents.tsx +140 -0
- package/src/components/MessageBubble.tsx +124 -904
- package/src/components/Navbar.tsx +55 -0
- package/src/components/ObservabilityPanel.tsx +374 -0
- package/src/components/ProductCard.tsx +5 -3
- package/src/components/UIDispatcher.tsx +344 -0
- package/src/components/VisualizationRenderer.tsx +372 -250
- package/src/config/RagConfig.ts +5 -0
- package/src/config/serverConfig.ts +3 -1
- package/src/core/Pipeline.ts +240 -271
- package/src/core/ProviderRegistry.ts +2 -2
- package/src/core/VectorPlugin.ts +9 -0
- package/src/handlers/index.ts +91 -15
- package/src/hooks/useRagChat.ts +21 -11
- package/src/index.ts +9 -1
- package/src/llm/LLMFactory.ts +54 -2
- package/src/llm/providers/AnthropicProvider.ts +12 -8
- package/src/llm/providers/GeminiProvider.ts +188 -143
- package/src/llm/providers/OllamaProvider.ts +7 -3
- package/src/llm/providers/OpenAIProvider.ts +12 -8
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
- package/src/types/chat.ts +8 -0
- package/src/types/index.ts +132 -0
- package/src/types/props.ts +9 -1
- package/src/utils/ProductExtractor.ts +347 -0
- package/src/utils/SchemaMapper.ts +129 -0
- package/src/utils/UITransformer.ts +470 -209
- package/src/utils/synonyms.ts +78 -0
- package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
- package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
- package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
- 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
|
|
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
|
-
|
|
102
|
-
const
|
|
27
|
+
const isStockRequest = this.isStockQuery(userQuery);
|
|
28
|
+
const filteredData = isStockRequest
|
|
29
|
+
? retrievedData.filter(item => this.determineStockStatus(item))
|
|
30
|
+
: retrievedData;
|
|
103
31
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
47
|
+
// 3. Category breakdowns
|
|
48
|
+
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
49
|
+
return this.transformToPieChart(filteredData);
|
|
168
50
|
}
|
|
169
51
|
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
175
|
-
|
|
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
|
|
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(
|
|
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 = [
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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 = ['
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
|
432
|
+
* Helper: Extract property from metadata using mapping, AI training, case-insensitivity, and synonyms.
|
|
349
433
|
*/
|
|
350
|
-
private static
|
|
351
|
-
|
|
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
|
-
|
|
354
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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:
|
|
374
|
-
price:
|
|
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 (
|
|
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
|
+
|