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