@retrivora-ai/rag-engine 1.8.8 → 1.9.0
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/{ILLMProvider-CbUtvWAW.d.mts → ILLMProvider-BOJFz3Na.d.mts} +0 -2
- package/dist/{ILLMProvider-CbUtvWAW.d.ts → ILLMProvider-BOJFz3Na.d.ts} +0 -2
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +52 -100
- package/dist/handlers/index.mjs +52 -100
- package/dist/{index-DgzcnqZs.d.ts → index-BwpcaziY.d.ts} +1 -1
- package/dist/{index-c_y_5qdw.d.mts → index-D3V9Et2M.d.mts} +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -25
- package/dist/index.mjs +2 -24
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/server.js +55 -108
- package/dist/server.mjs +55 -108
- package/package.json +2 -2
- package/src/config/RagConfig.ts +0 -2
- package/src/index.ts +1 -1
- package/src/providers/vectordb/ChromaDBProvider.ts +0 -4
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +5 -18
- package/src/providers/vectordb/PostgreSQLProvider.ts +3 -14
- package/src/providers/vectordb/QdrantProvider.ts +1 -4
- package/src/utils/UITransformer.ts +51 -99
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "Retrivora AI is a plug-and-play AI engine for RAG chat experiences — generic vector DB + LLM provider, embeddable or standalone.",
|
|
5
5
|
"author": "Abhinav Alkuchi",
|
|
6
6
|
"license": "MIT",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"scripts": {
|
|
75
75
|
"dev": "next dev",
|
|
76
76
|
"build": "next build",
|
|
77
|
-
"build:pkg": "
|
|
77
|
+
"build:pkg": "tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --no-splitting --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex,./index.css && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
|
|
78
78
|
"start": "next start",
|
|
79
79
|
"lint": "eslint",
|
|
80
80
|
"prepublishOnly": "npm run build:pkg"
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -44,8 +44,6 @@ export interface VectorDBConfig {
|
|
|
44
44
|
* - searchFields?: string[] | string // optional override for which columns receive exact-match boosts
|
|
45
45
|
*/
|
|
46
46
|
options: Record<string, unknown>;
|
|
47
|
-
/** Optional distance metric (defaults to cosine if not specified and supported by provider) */
|
|
48
|
-
distanceMetric?: 'cosine' | 'euclidean' | 'dotproduct';
|
|
49
47
|
}
|
|
50
48
|
|
|
51
49
|
// ---------------------------------------------------------------------------
|
package/src/index.ts
CHANGED
|
@@ -41,13 +41,9 @@ export class ChromaDBProvider extends BaseVectorProvider {
|
|
|
41
41
|
} catch (err) {
|
|
42
42
|
if (axios.isAxiosError(err) && err.response?.status === 404) {
|
|
43
43
|
// Create the collection if it doesn't exist
|
|
44
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
45
|
-
const space = metric === 'euclidean' ? 'l2' : metric === 'dotproduct' ? 'ip' : 'cosine';
|
|
46
|
-
|
|
47
44
|
console.log(`[ChromaDBProvider] ⏳ Collection "${this.indexName}" not found. Creating...`);
|
|
48
45
|
const { data } = await this.http.post('/api/v1/collections', {
|
|
49
46
|
name: this.indexName,
|
|
50
|
-
metadata: { "hnsw:space": space }
|
|
51
47
|
});
|
|
52
48
|
this.collectionId = data.id;
|
|
53
49
|
console.log(`[ChromaDBProvider] ✅ Created collection "${this.indexName}" (id: ${this.collectionId})`);
|
|
@@ -66,13 +66,10 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
66
66
|
embedding VECTOR(${this.dimensions})
|
|
67
67
|
)
|
|
68
68
|
`);
|
|
69
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
70
|
-
const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
|
|
71
|
-
|
|
72
69
|
await client.query(`
|
|
73
70
|
CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
|
|
74
71
|
ON ${this.uploadTable}
|
|
75
|
-
USING hnsw (embedding
|
|
72
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
76
73
|
`);
|
|
77
74
|
|
|
78
75
|
const res = await client.query(`
|
|
@@ -150,13 +147,10 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
150
147
|
await client.query(createTableSql);
|
|
151
148
|
|
|
152
149
|
// Create Index
|
|
153
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
154
|
-
const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
|
|
155
|
-
|
|
156
150
|
await client.query(`
|
|
157
151
|
CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
|
|
158
152
|
ON "${tableName}"
|
|
159
|
-
USING hnsw (embedding
|
|
153
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
160
154
|
`);
|
|
161
155
|
|
|
162
156
|
// Ensure this new table is added to our search scope
|
|
@@ -280,13 +274,6 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
280
274
|
let sqlQuery = '';
|
|
281
275
|
let params: unknown[] = [];
|
|
282
276
|
|
|
283
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
284
|
-
const scoreExpr = metric === 'euclidean'
|
|
285
|
-
? `1 / (1 + (embedding <-> $1::vector))`
|
|
286
|
-
: metric === 'dotproduct'
|
|
287
|
-
? `(embedding <#> $1::vector) * -1`
|
|
288
|
-
: `1 - (embedding <=> $1::vector)`;
|
|
289
|
-
|
|
290
277
|
if (queryText) {
|
|
291
278
|
const hasEntityHints = entityHints.length > 0;
|
|
292
279
|
const exactNameScoreExpr = hasEntityHints
|
|
@@ -303,9 +290,9 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
303
290
|
// Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
|
|
304
291
|
sqlQuery = `
|
|
305
292
|
SELECT *,
|
|
306
|
-
($
|
|
293
|
+
(1 - (embedding <=> $1::vector)) AS vector_score,
|
|
307
294
|
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
308
|
-
(($
|
|
295
|
+
((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
309
296
|
FROM "${table}" t
|
|
310
297
|
ORDER BY hybrid_score DESC
|
|
311
298
|
LIMIT 50
|
|
@@ -315,7 +302,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
315
302
|
// Fallback to Vector-only Search
|
|
316
303
|
sqlQuery = `
|
|
317
304
|
SELECT *,
|
|
318
|
-
($
|
|
305
|
+
(1 - (embedding <=> $1::vector)) AS hybrid_score
|
|
319
306
|
FROM "${table}" t
|
|
320
307
|
ORDER BY hybrid_score DESC
|
|
321
308
|
LIMIT 50
|
|
@@ -97,13 +97,10 @@ export class PostgreSQLProvider extends BaseVectorProvider {
|
|
|
97
97
|
embedding VECTOR(${this.dimensions})
|
|
98
98
|
)
|
|
99
99
|
`);
|
|
100
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
101
|
-
const indexOps = metric === 'euclidean' ? 'vector_l2_ops' : metric === 'dotproduct' ? 'vector_ip_ops' : 'vector_cosine_ops';
|
|
102
|
-
|
|
103
100
|
await client.query(`
|
|
104
101
|
CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
|
|
105
102
|
ON ${this.tableName}
|
|
106
|
-
USING hnsw (embedding
|
|
103
|
+
USING hnsw (embedding vector_cosine_ops)
|
|
107
104
|
`);
|
|
108
105
|
} finally {
|
|
109
106
|
client.release();
|
|
@@ -185,19 +182,11 @@ export class PostgreSQLProvider extends BaseVectorProvider {
|
|
|
185
182
|
const efSearch = (this.config.options.efSearch as number) || Math.max(topK * 10, 40);
|
|
186
183
|
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
187
184
|
|
|
188
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
189
|
-
const queryOp = metric === 'euclidean' ? '<->' : metric === 'dotproduct' ? '<#>' : '<=>';
|
|
190
|
-
const scoreExpr = metric === 'euclidean'
|
|
191
|
-
? `1 / (1 + (embedding <-> $1::vector))`
|
|
192
|
-
: metric === 'dotproduct'
|
|
193
|
-
? `(embedding <#> $1::vector) * -1`
|
|
194
|
-
: `1 - (embedding <=> $1::vector)`;
|
|
195
|
-
|
|
196
185
|
const result = await client.query(
|
|
197
|
-
`SELECT id, content, metadata, $
|
|
186
|
+
`SELECT id, content, metadata, 1 - (embedding <=> $1::vector) AS score
|
|
198
187
|
FROM ${this.tableName}
|
|
199
188
|
${whereClause}
|
|
200
|
-
ORDER BY embedding
|
|
189
|
+
ORDER BY embedding <=> $1::vector
|
|
201
190
|
LIMIT $2`,
|
|
202
191
|
params
|
|
203
192
|
);
|
|
@@ -120,14 +120,11 @@ export class QdrantProvider extends BaseVectorProvider {
|
|
|
120
120
|
const opts = this.config.options as Record<string, unknown>;
|
|
121
121
|
const dimensionsForCreate = (opts.dimensions as number) || 1536;
|
|
122
122
|
|
|
123
|
-
const metric = this.config.distanceMetric || 'cosine';
|
|
124
|
-
const distance = metric === 'euclidean' ? 'Euclid' : metric === 'dotproduct' ? 'Dot' : 'Cosine';
|
|
125
|
-
|
|
126
123
|
console.log(`[QdrantProvider] ⏳ Creating collection "${this.indexName}" (dimensions: ${dimensionsForCreate})...`);
|
|
127
124
|
await this.http.put(`/collections/${this.indexName}`, {
|
|
128
125
|
vectors: {
|
|
129
126
|
size: dimensionsForCreate,
|
|
130
|
-
distance:
|
|
127
|
+
distance: 'Cosine',
|
|
131
128
|
},
|
|
132
129
|
});
|
|
133
130
|
console.log(`[QdrantProvider] ✅ Created collection "${this.indexName}"`);
|
|
@@ -29,8 +29,7 @@ export class UITransformer {
|
|
|
29
29
|
? retrievedData.filter(item => this.determineStockStatus(item))
|
|
30
30
|
: retrievedData;
|
|
31
31
|
|
|
32
|
-
const
|
|
33
|
-
const categories = this.detectCategories(filteredData, groupByField);
|
|
32
|
+
const categories = this.detectCategories(filteredData);
|
|
34
33
|
const hasProducts = filteredData.some(item => this.isProductData(item));
|
|
35
34
|
const isTimeSeries = filteredData.some(item => this.isTimeSeriesData(item));
|
|
36
35
|
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
@@ -40,16 +39,14 @@ export class UITransformer {
|
|
|
40
39
|
return this.transformToLineChart(filteredData);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
const explicitChartRequest = this.isExplicitChartQuery(userQuery);
|
|
44
|
-
|
|
45
42
|
// 2. High Priority: Products (Always prefer carousel for products unless it's a specific breakdown request)
|
|
46
|
-
if (hasProducts && !
|
|
43
|
+
if (hasProducts && !this.shouldShowCategoryChart(userQuery, categories)) {
|
|
47
44
|
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
// 3. Category breakdowns
|
|
51
|
-
if (
|
|
52
|
-
return this.transformToPieChart(filteredData
|
|
48
|
+
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery, categories)) {
|
|
49
|
+
return this.transformToPieChart(filteredData);
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
// 4. Secondary: Products as carousel if not already handled
|
|
@@ -90,16 +87,13 @@ export class UITransformer {
|
|
|
90
87
|
* Transform data to pie chart format
|
|
91
88
|
*/
|
|
92
89
|
private static transformToPieChart(
|
|
93
|
-
data: VectorMatch[]
|
|
94
|
-
groupByField: string
|
|
90
|
+
data: VectorMatch[]
|
|
95
91
|
): UITransformationResponse {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const categoryData = this.aggregateByCategory(data, categories, groupByField);
|
|
92
|
+
const categories = this.detectCategories(data);
|
|
93
|
+
const categoryData = this.aggregateByCategory(data, categories);
|
|
100
94
|
|
|
101
95
|
const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => {
|
|
102
|
-
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data
|
|
96
|
+
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
|
|
103
97
|
return {
|
|
104
98
|
label,
|
|
105
99
|
value: count as number,
|
|
@@ -110,7 +104,7 @@ export class UITransformer {
|
|
|
110
104
|
|
|
111
105
|
return {
|
|
112
106
|
type: 'pie_chart',
|
|
113
|
-
title:
|
|
107
|
+
title: 'Distribution by Category',
|
|
114
108
|
description: `Showing breakdown across ${categories.length} categories`,
|
|
115
109
|
data: pieData,
|
|
116
110
|
};
|
|
@@ -377,16 +371,11 @@ export class UITransformer {
|
|
|
377
371
|
return hasTimeKeyword || hasValidDateValue;
|
|
378
372
|
}
|
|
379
373
|
|
|
380
|
-
private static
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
normalized.includes('bar chart') ||
|
|
385
|
-
normalized.includes('barchart') ||
|
|
386
|
-
normalized.includes('radar chart');
|
|
387
|
-
}
|
|
374
|
+
private static shouldShowCategoryChart(query: string, categories: string[]): boolean {
|
|
375
|
+
if (categories.length < 2) {
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
388
378
|
|
|
389
|
-
private static shouldShowCategoryChart(query: string): boolean {
|
|
390
379
|
const normalized = query.toLowerCase();
|
|
391
380
|
const chartKeywords = [
|
|
392
381
|
'distribution',
|
|
@@ -400,10 +389,6 @@ export class UITransformer {
|
|
|
400
389
|
'split',
|
|
401
390
|
'category breakdown',
|
|
402
391
|
'category distribution',
|
|
403
|
-
'pie chart',
|
|
404
|
-
'piechart',
|
|
405
|
-
'bar chart',
|
|
406
|
-
'barchart'
|
|
407
392
|
];
|
|
408
393
|
|
|
409
394
|
return chartKeywords.some(keyword => normalized.includes(keyword));
|
|
@@ -516,57 +501,38 @@ export class UITransformer {
|
|
|
516
501
|
}
|
|
517
502
|
|
|
518
503
|
/**
|
|
519
|
-
* Helper:
|
|
520
|
-
*/
|
|
521
|
-
private static determineGroupByField(query: string, data: VectorMatch[]): string {
|
|
522
|
-
const normalized = query.toLowerCase();
|
|
523
|
-
const match = normalized.match(/(?:by|across|per|based on)\s+([a-zA-Z]+)/i);
|
|
524
|
-
|
|
525
|
-
let targetField = 'category';
|
|
526
|
-
if (match && match[1]) {
|
|
527
|
-
let field = match[1].toLowerCase();
|
|
528
|
-
// Un-pluralize common words to match schema keys
|
|
529
|
-
if (field.endsWith('ies')) field = field.replace(/ies$/, 'y');
|
|
530
|
-
else if (field.endsWith('s') && field !== 'status') field = field.slice(0, -1);
|
|
531
|
-
|
|
532
|
-
const stopWords = ['the', 'all', 'different', 'various', 'some', 'these', 'those'];
|
|
533
|
-
if (!stopWords.includes(field)) {
|
|
534
|
-
targetField = field;
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
// Verify field exists, or fallback
|
|
539
|
-
const hasTargetField = data.some(d => d.metadata && d.metadata[targetField] !== undefined);
|
|
540
|
-
if (!hasTargetField && targetField === 'category') {
|
|
541
|
-
if (data.some(d => d.metadata && d.metadata['type'] !== undefined)) return 'type';
|
|
542
|
-
if (data.some(d => d.metadata && d.metadata['brand'] !== undefined)) return 'brand';
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
return targetField;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
/**
|
|
549
|
-
* Helper: Detect grouping values dynamically
|
|
504
|
+
* Helper: Detect categories in data
|
|
550
505
|
*/
|
|
551
|
-
private static detectCategories(data: VectorMatch[]
|
|
506
|
+
private static detectCategories(data: VectorMatch[]): string[] {
|
|
552
507
|
const categories = new Set<string>();
|
|
553
508
|
|
|
554
509
|
data.forEach(item => {
|
|
555
510
|
const meta = item.metadata || {};
|
|
556
511
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
512
|
+
// Check for category field
|
|
513
|
+
if (meta.category) {
|
|
514
|
+
categories.add(String(meta.category));
|
|
515
|
+
}
|
|
516
|
+
if (meta.type) {
|
|
517
|
+
categories.add(String(meta.type));
|
|
518
|
+
}
|
|
519
|
+
if (meta.tag) {
|
|
520
|
+
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
521
|
+
tags.forEach(t => categories.add(String(t)));
|
|
522
|
+
}
|
|
523
|
+
|
|
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
|
|
533
|
+
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
534
|
+
if (categoryMatch) {
|
|
535
|
+
categories.add(categoryMatch[1].trim());
|
|
570
536
|
}
|
|
571
537
|
});
|
|
572
538
|
|
|
@@ -574,12 +540,11 @@ export class UITransformer {
|
|
|
574
540
|
}
|
|
575
541
|
|
|
576
542
|
/**
|
|
577
|
-
* Helper: Aggregate data
|
|
543
|
+
* Helper: Aggregate data by category
|
|
578
544
|
*/
|
|
579
545
|
private static aggregateByCategory(
|
|
580
546
|
data: VectorMatch[],
|
|
581
|
-
categories: string[]
|
|
582
|
-
groupByField: string
|
|
547
|
+
categories: string[]
|
|
583
548
|
): Record<string, number> {
|
|
584
549
|
const result: Record<string, number> = {};
|
|
585
550
|
|
|
@@ -589,13 +554,7 @@ export class UITransformer {
|
|
|
589
554
|
|
|
590
555
|
data.forEach(item => {
|
|
591
556
|
const meta = item.metadata || {};
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
if (meta[groupByField] !== undefined) {
|
|
595
|
-
itemCategory = String(meta[groupByField]);
|
|
596
|
-
} else if (groupByField === 'category' && meta.type) {
|
|
597
|
-
itemCategory = String(meta.type);
|
|
598
|
-
}
|
|
557
|
+
const itemCategory = (meta.category || meta.type || 'Other') as string;
|
|
599
558
|
|
|
600
559
|
if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
|
|
601
560
|
result[itemCategory]++;
|
|
@@ -656,21 +615,15 @@ export class UITransformer {
|
|
|
656
615
|
}
|
|
657
616
|
|
|
658
617
|
/**
|
|
659
|
-
* Helper: Calculate stock counts
|
|
618
|
+
* Helper: Calculate stock counts by category
|
|
660
619
|
*/
|
|
661
|
-
private static calculateStockCounts(category: string, data: VectorMatch[]
|
|
620
|
+
private static calculateStockCounts(category: string, data: VectorMatch[]): { inStockCount: number; outOfStockCount: number } {
|
|
662
621
|
let inStock = 0;
|
|
663
622
|
let outOfStock = 0;
|
|
664
623
|
|
|
665
624
|
data.forEach(d => {
|
|
666
625
|
const meta = d.metadata || {};
|
|
667
|
-
|
|
668
|
-
if (meta[groupByField] !== undefined) {
|
|
669
|
-
itemCategory = String(meta[groupByField]);
|
|
670
|
-
} else if (groupByField === 'category' && meta.type) {
|
|
671
|
-
itemCategory = String(meta.type);
|
|
672
|
-
}
|
|
673
|
-
|
|
626
|
+
const itemCategory = (meta.category || meta.type || 'Other') as string;
|
|
674
627
|
if (itemCategory === category) {
|
|
675
628
|
if (this.determineStockStatus(d)) {
|
|
676
629
|
inStock++;
|
|
@@ -819,13 +772,12 @@ DATA SHAPE per type:
|
|
|
819
772
|
- text: { "content": "<prose answer>" }
|
|
820
773
|
|
|
821
774
|
DECISION RULES (follow strictly):
|
|
822
|
-
1.
|
|
823
|
-
2.
|
|
824
|
-
3.
|
|
825
|
-
4.
|
|
826
|
-
5.
|
|
827
|
-
6.
|
|
828
|
-
7. text → conversational or free-form answers where no chart adds value
|
|
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
|
|
829
781
|
|
|
830
782
|
IMPORTANT:
|
|
831
783
|
- Aggregate numeric values from the raw data — do not pass raw object arrays as data.
|