@retrivora-ai/rag-engine 1.8.4 → 1.8.5
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/LICENSE.txt +21 -0
- package/dist/{ILLMProvider-BOJFz3Na.d.mts → ILLMProvider-CbUtvWAW.d.mts} +2 -0
- package/dist/{ILLMProvider-BOJFz3Na.d.ts → ILLMProvider-CbUtvWAW.d.ts} +2 -0
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +100 -52
- package/dist/handlers/index.mjs +100 -52
- package/dist/{index-BwpcaziY.d.ts → index-DgzcnqZs.d.ts} +1 -1
- package/dist/{index-D3V9Et2M.d.mts → index-c_y_5qdw.d.mts} +1 -1
- package/dist/index.css +27 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +18 -4
- package/dist/index.mjs +18 -4
- package/dist/server.d.mts +3 -3
- package/dist/server.d.ts +3 -3
- package/dist/server.js +108 -55
- package/dist/server.mjs +108 -55
- package/package.json +1 -1
- package/src/app/globals.css +97 -0
- package/src/components/ChatWidget.tsx +23 -10
- package/src/config/RagConfig.ts +2 -0
- package/src/providers/vectordb/ChromaDBProvider.ts +4 -0
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +18 -5
- package/src/providers/vectordb/PostgreSQLProvider.ts +14 -3
- package/src/providers/vectordb/QdrantProvider.ts +4 -1
- package/src/utils/UITransformer.ts +99 -51
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.5",
|
|
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",
|
package/src/app/globals.css
CHANGED
|
@@ -2,7 +2,104 @@
|
|
|
2
2
|
|
|
3
3
|
@custom-variant dark (&:where(.dark, .dark *));
|
|
4
4
|
|
|
5
|
+
@theme {
|
|
6
|
+
--animate-shapeshift: shapeshift 5s ease-in-out infinite forwards;
|
|
7
|
+
--animate-gradientMove: gradientMove 3.5s ease-in-out infinite alternate;
|
|
8
|
+
|
|
9
|
+
@keyframes shapeshift {
|
|
10
|
+
0% { clip-path: var(--circle); rotate: 0turn; }
|
|
11
|
+
25% { clip-path: var(--flower); }
|
|
12
|
+
50% { clip-path: var(--cylinder); }
|
|
13
|
+
75% { clip-path: var(--hexagon); }
|
|
14
|
+
100% { clip-path: var(--circle); rotate: 1turn; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@keyframes gradientMove {
|
|
18
|
+
0% { translate: 0 0; }
|
|
19
|
+
100% { translate: -75% -75%; }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
5
22
|
|
|
23
|
+
@layer base {
|
|
24
|
+
:root {
|
|
25
|
+
--star: shape(
|
|
26
|
+
evenodd from 50% 24.787%,
|
|
27
|
+
curve by 7.143% 18.016% with 0% 0% / 2.9725% 13.814%,
|
|
28
|
+
curve by 17.882% 7.197% with 4.171% 4.2025% / 17.882% 7.197%,
|
|
29
|
+
curve by -17.882% 8.6765% with 0% 0% / -13.711% 4.474%,
|
|
30
|
+
curve by -7.143% 16.5365% with -4.1705% 4.202% / -7.143% 16.5365%,
|
|
31
|
+
curve by -8.6115% -16.5365% with 0% 0% / -4.441% -12.3345%,
|
|
32
|
+
curve by -16.4135% -8.6765% with -4.171% -4.2025% / -16.4135% -8.6765%,
|
|
33
|
+
curve by 16.4135% -7.197% with 0% 0% / 12.2425% -2.9945%,
|
|
34
|
+
curve by 8.6115% -18.016% with 4.1705% -4.202% / 8.6115% -18.016%,
|
|
35
|
+
close
|
|
36
|
+
);
|
|
37
|
+
--flower: shape(
|
|
38
|
+
evenodd from 17.9665% 82.0335%,
|
|
39
|
+
curve by -12.349% -32.0335% with -13.239% -5.129% / -18.021% -15.402%,
|
|
40
|
+
curve by -0.0275% -22.203% with -3.1825% -9.331% / -3.074% -16.6605%,
|
|
41
|
+
curve by 12.3765% -9.8305% with 2.3835% -4.3365% / 6.565% -7.579%,
|
|
42
|
+
curve by 32.0335% -12.349% with 5.129% -13.239% / 15.402% -18.021%,
|
|
43
|
+
curve by 20.4535% -0.8665% with 8.3805% -2.858% / 15.1465% -3.062%,
|
|
44
|
+
curve by 11.58% 13.2155% with 5.225% 2.161% / 9.0355% 6.6475%,
|
|
45
|
+
curve by 12.349% 32.0335% with 13.239% 5.129% / 18.021% 15.402%,
|
|
46
|
+
curve by 0.5715% 21.1275% with 2.9805% 8.7395% / 3.0745% 15.723%,
|
|
47
|
+
curve by -12.9205% 10.906% with -2.26% 4.88% / -6.638% 8.472%,
|
|
48
|
+
curve by -32.0335% 12.349% with -5.129% 13.239% / -15.402% 18.021%,
|
|
49
|
+
curve by -21.1215% 0.5745% with -8.736% 2.9795% / -15.718% 3.0745%,
|
|
50
|
+
curve by -10.912% -12.9235% with -4.883% -2.2595% / -8.477% -6.6385%,
|
|
51
|
+
close
|
|
52
|
+
);
|
|
53
|
+
--hexagon: shape(
|
|
54
|
+
evenodd from 6.47% 67.001%,
|
|
55
|
+
curve by 0% -34.002% with -1.1735% -7.7% / -1.1735% -26.302%,
|
|
56
|
+
curve by 7.0415% -12.1965% with 0.7075% -4.641% / 3.3765% -9.2635%,
|
|
57
|
+
curve by 29.447% -17.001% with 6.0815% -4.8665% / 22.192% -14.1675%,
|
|
58
|
+
curve by 14.083% 0% with 4.3725% -1.708% / 9.7105% -1.708%,
|
|
59
|
+
curve by 29.447% 17.001% with 7.255% 2.8335% / 23.3655% 12.1345%,
|
|
60
|
+
curve by 7.0415% 12.1965% with 3.665% 2.933% / 6.334% 7.5555%,
|
|
61
|
+
curve by 0% 34.002% with 1.1735% 7.7% / 1.1735% 26.302%,
|
|
62
|
+
curve by -7.0415% 12.1965% with -0.7075% 4.641% / -3.3765% 9.2635%,
|
|
63
|
+
curve by -29.447% 17.001% with -6.0815% 4.8665% / -22.192% 14.1675%,
|
|
64
|
+
curve by -14.083% 0% with -4.3725% 1.708% / -9.7105% 1.708%,
|
|
65
|
+
curve by -29.447% -17.001% with -7.255% -2.8335% / -23.3655% -12.1345%,
|
|
66
|
+
curve by -7.0415% -12.1965% with -3.665% -2.933% / -6.334% -7.5555%,
|
|
67
|
+
close
|
|
68
|
+
);
|
|
69
|
+
--cylinder: shape(
|
|
70
|
+
evenodd from 10.5845% 59.7305%,
|
|
71
|
+
curve by 0% -19.461% with -0.113% -1.7525% / -0.11% -18.14%,
|
|
72
|
+
curve by 10.098% -26.213% with 0.837% -10.0375% / 3.821% -19.2625%,
|
|
73
|
+
curve by 29.3175% -13.0215% with 7.2175% -7.992% / 17.682% -13.0215%,
|
|
74
|
+
curve by 19.5845% 5.185% with 7.1265% 0% / 13.8135% 1.887%,
|
|
75
|
+
curve by 9.8595% 7.9775% with 3.7065% 2.1185% / 7.035% 4.8195%,
|
|
76
|
+
curve by 9.9715% 26.072% with 6.2015% 6.933% / 9.4345% 16.082%,
|
|
77
|
+
curve by 0% 19.461% with 0.074% 1.384% / 0.0745% 17.7715%,
|
|
78
|
+
curve by -13.0065% 29.1155% with -0.511% 11.5345% / -5.021% 21.933%,
|
|
79
|
+
curve by -26.409% 10.119% with -6.991% 6.288% / -16.254% 10.119%,
|
|
80
|
+
curve by -20.945% -5.9995% with -7.6935% 0% / -14.8755% -2.199%,
|
|
81
|
+
curve by -8.713% -7.404% with -3.255% -2.0385% / -6.1905% -4.537%,
|
|
82
|
+
curve by -9.7575% -25.831% with -6.074% -6.9035% / -9.1205% -15.963%,
|
|
83
|
+
close
|
|
84
|
+
);
|
|
85
|
+
--circle: shape(
|
|
86
|
+
evenodd from 13.482% 79.505%,
|
|
87
|
+
curve by -7.1945% -12.47% with -1.4985% -1.8575% / -6.328% -10.225%,
|
|
88
|
+
curve by 0.0985% -33.8965% with -4.1645% -10.7945% / -4.1685% -23.0235%,
|
|
89
|
+
curve by 6.9955% -12.101% with 1.72% -4.3825% / 4.0845% -8.458%,
|
|
90
|
+
curve by 30.125% -17.119% with 7.339% -9.1825% / 18.4775% -15.5135%,
|
|
91
|
+
curve by 13.4165% 0.095% with 4.432% -0.6105% / 8.9505% -0.5855%,
|
|
92
|
+
curve by 29.364% 16.9% with 11.6215% 1.77% / 22.102% 7.9015%,
|
|
93
|
+
curve by 7.176% 12.4145% with 3.002% 3.7195% / 5.453% 7.968%,
|
|
94
|
+
curve by -0.0475% 33.8925% with 4.168% 10.756% / 4.2305% 22.942%,
|
|
95
|
+
curve by -7.1135% 12.2825% with -1.74% 4.4535% / -4.1455% 8.592%,
|
|
96
|
+
curve by -29.404% 16.9075% with -7.202% 8.954% / -18.019% 15.137%,
|
|
97
|
+
curve by -14.19% -0.018% with -4.6635% 0.7255% / -9.4575% 0.7205%,
|
|
98
|
+
curve by -29.226% -16.8875% with -11.573% -1.8065% / -21.9955% -7.9235%,
|
|
99
|
+
close
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
6
103
|
|
|
7
104
|
/* ─── Design Tokens ─────────────────────────────────────────── */
|
|
8
105
|
:root {
|
|
@@ -119,25 +119,38 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart }: ChatWidge
|
|
|
119
119
|
{/* ── Floating Action Button ── */}
|
|
120
120
|
<button
|
|
121
121
|
onClick={isOpen ? () => setIsOpen(false) : handleOpen}
|
|
122
|
-
className={`fixed z-[9999] w-14 h-14 shadow-2xl flex items-center justify-center transition-all duration-300 cursor-pointer hover:scale-110 active:scale-95 ${
|
|
123
|
-
ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'
|
|
124
|
-
} ${positionClass}`}
|
|
125
|
-
style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
|
|
122
|
+
className={`group fixed z-[9999] w-14 h-14 drop-shadow-2xl flex items-center justify-center transition-all duration-300 cursor-pointer hover:scale-110 active:scale-95 ${positionClass}`}
|
|
126
123
|
aria-label="Open chat"
|
|
127
124
|
>
|
|
125
|
+
{/* Animated Background */}
|
|
126
|
+
<div
|
|
127
|
+
className={`absolute inset-0 transition-all duration-300 overflow-hidden ${
|
|
128
|
+
ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'
|
|
129
|
+
} group-hover:rounded-none group-hover:animate-shapeshift`}
|
|
130
|
+
>
|
|
131
|
+
<div
|
|
132
|
+
className="absolute top-0 left-0 w-[400%] h-[400%] transition-transform duration-300 group-hover:animate-gradientMove"
|
|
133
|
+
style={{
|
|
134
|
+
background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor}, ${ui.primaryColor}, ${ui.accentColor})`
|
|
135
|
+
}}
|
|
136
|
+
/>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
128
139
|
{/* Ripple ring */}
|
|
129
140
|
<span
|
|
130
|
-
className="absolute inset-0 rounded-full animate-ping opacity-20"
|
|
141
|
+
className="absolute inset-0 rounded-full animate-ping opacity-20 pointer-events-none"
|
|
131
142
|
style={{ background: ui.primaryColor }}
|
|
132
143
|
/>
|
|
133
144
|
|
|
145
|
+
{/* Icon Layer */}
|
|
134
146
|
<span
|
|
135
|
-
className={`transition-transform duration-300 ${isOpen ? 'rotate-90 scale-90' : 'rotate-0 scale-100'}`}
|
|
147
|
+
className={`relative z-10 transition-transform duration-300 ${isOpen ? 'rotate-90 scale-90' : 'rotate-0 scale-100'}`}
|
|
136
148
|
>
|
|
137
|
-
{isOpen
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
149
|
+
{isOpen ? (
|
|
150
|
+
<X className="w-6 h-6 text-white" />
|
|
151
|
+
) : (
|
|
152
|
+
<MessageSquare className="w-6 h-6 text-white" />
|
|
153
|
+
)}
|
|
141
154
|
</span>
|
|
142
155
|
|
|
143
156
|
{/* Unread badge */}
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -44,6 +44,8 @@ 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';
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
// ---------------------------------------------------------------------------
|
|
@@ -41,9 +41,13 @@ 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
|
+
|
|
44
47
|
console.log(`[ChromaDBProvider] ⏳ Collection "${this.indexName}" not found. Creating...`);
|
|
45
48
|
const { data } = await this.http.post('/api/v1/collections', {
|
|
46
49
|
name: this.indexName,
|
|
50
|
+
metadata: { "hnsw:space": space }
|
|
47
51
|
});
|
|
48
52
|
this.collectionId = data.id;
|
|
49
53
|
console.log(`[ChromaDBProvider] ✅ Created collection "${this.indexName}" (id: ${this.collectionId})`);
|
|
@@ -66,10 +66,13 @@ 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
|
+
|
|
69
72
|
await client.query(`
|
|
70
73
|
CREATE INDEX IF NOT EXISTS ${this.uploadTable}_embedding_idx
|
|
71
74
|
ON ${this.uploadTable}
|
|
72
|
-
USING hnsw (embedding
|
|
75
|
+
USING hnsw (embedding ${indexOps})
|
|
73
76
|
`);
|
|
74
77
|
|
|
75
78
|
const res = await client.query(`
|
|
@@ -147,10 +150,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
147
150
|
await client.query(createTableSql);
|
|
148
151
|
|
|
149
152
|
// 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
|
+
|
|
150
156
|
await client.query(`
|
|
151
157
|
CREATE INDEX IF NOT EXISTS "${tableName}_embedding_idx"
|
|
152
158
|
ON "${tableName}"
|
|
153
|
-
USING hnsw (embedding
|
|
159
|
+
USING hnsw (embedding ${indexOps})
|
|
154
160
|
`);
|
|
155
161
|
|
|
156
162
|
// Ensure this new table is added to our search scope
|
|
@@ -274,6 +280,13 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
274
280
|
let sqlQuery = '';
|
|
275
281
|
let params: unknown[] = [];
|
|
276
282
|
|
|
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
|
+
|
|
277
290
|
if (queryText) {
|
|
278
291
|
const hasEntityHints = entityHints.length > 0;
|
|
279
292
|
const exactNameScoreExpr = hasEntityHints
|
|
@@ -290,9 +303,9 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
290
303
|
// Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
|
|
291
304
|
sqlQuery = `
|
|
292
305
|
SELECT *,
|
|
293
|
-
(
|
|
306
|
+
(${scoreExpr}) AS vector_score,
|
|
294
307
|
COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
|
|
295
|
-
((
|
|
308
|
+
((${scoreExpr}) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
|
|
296
309
|
FROM "${table}" t
|
|
297
310
|
ORDER BY hybrid_score DESC
|
|
298
311
|
LIMIT 50
|
|
@@ -302,7 +315,7 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
|
|
|
302
315
|
// Fallback to Vector-only Search
|
|
303
316
|
sqlQuery = `
|
|
304
317
|
SELECT *,
|
|
305
|
-
(
|
|
318
|
+
(${scoreExpr}) AS hybrid_score
|
|
306
319
|
FROM "${table}" t
|
|
307
320
|
ORDER BY hybrid_score DESC
|
|
308
321
|
LIMIT 50
|
|
@@ -97,10 +97,13 @@ 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
|
+
|
|
100
103
|
await client.query(`
|
|
101
104
|
CREATE INDEX IF NOT EXISTS ${this.tableName}_embedding_idx
|
|
102
105
|
ON ${this.tableName}
|
|
103
|
-
USING hnsw (embedding
|
|
106
|
+
USING hnsw (embedding ${indexOps})
|
|
104
107
|
`);
|
|
105
108
|
} finally {
|
|
106
109
|
client.release();
|
|
@@ -182,11 +185,19 @@ export class PostgreSQLProvider extends BaseVectorProvider {
|
|
|
182
185
|
const efSearch = (this.config.options.efSearch as number) || Math.max(topK * 10, 40);
|
|
183
186
|
await client.query(`SET LOCAL hnsw.ef_search = ${efSearch}`);
|
|
184
187
|
|
|
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
|
+
|
|
185
196
|
const result = await client.query(
|
|
186
|
-
`SELECT id, content, metadata,
|
|
197
|
+
`SELECT id, content, metadata, ${scoreExpr} AS score
|
|
187
198
|
FROM ${this.tableName}
|
|
188
199
|
${whereClause}
|
|
189
|
-
ORDER BY embedding
|
|
200
|
+
ORDER BY embedding ${queryOp} $1::vector
|
|
190
201
|
LIMIT $2`,
|
|
191
202
|
params
|
|
192
203
|
);
|
|
@@ -120,11 +120,14 @@ 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
|
+
|
|
123
126
|
console.log(`[QdrantProvider] ⏳ Creating collection "${this.indexName}" (dimensions: ${dimensionsForCreate})...`);
|
|
124
127
|
await this.http.put(`/collections/${this.indexName}`, {
|
|
125
128
|
vectors: {
|
|
126
129
|
size: dimensionsForCreate,
|
|
127
|
-
distance:
|
|
130
|
+
distance: distance,
|
|
128
131
|
},
|
|
129
132
|
});
|
|
130
133
|
console.log(`[QdrantProvider] ✅ Created collection "${this.indexName}"`);
|
|
@@ -29,7 +29,8 @@ export class UITransformer {
|
|
|
29
29
|
? retrievedData.filter(item => this.determineStockStatus(item))
|
|
30
30
|
: retrievedData;
|
|
31
31
|
|
|
32
|
-
const
|
|
32
|
+
const groupByField = this.determineGroupByField(userQuery, filteredData);
|
|
33
|
+
const categories = this.detectCategories(filteredData, groupByField);
|
|
33
34
|
const hasProducts = filteredData.some(item => this.isProductData(item));
|
|
34
35
|
const isTimeSeries = filteredData.some(item => this.isTimeSeriesData(item));
|
|
35
36
|
const isTrendQuery = this.isTrendQuery(userQuery);
|
|
@@ -39,14 +40,16 @@ export class UITransformer {
|
|
|
39
40
|
return this.transformToLineChart(filteredData);
|
|
40
41
|
}
|
|
41
42
|
|
|
43
|
+
const explicitChartRequest = this.isExplicitChartQuery(userQuery);
|
|
44
|
+
|
|
42
45
|
// 2. High Priority: Products (Always prefer carousel for products unless it's a specific breakdown request)
|
|
43
|
-
if (hasProducts && !this.shouldShowCategoryChart(userQuery
|
|
46
|
+
if (hasProducts && !explicitChartRequest && !this.shouldShowCategoryChart(userQuery)) {
|
|
44
47
|
return this.transformToProductCarousel(filteredData, config, trainedSchema);
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// 3. Category breakdowns
|
|
48
|
-
if (categories.length > 1 && this.shouldShowCategoryChart(userQuery
|
|
49
|
-
return this.transformToPieChart(filteredData);
|
|
51
|
+
if (explicitChartRequest || (categories.length > 1 && this.shouldShowCategoryChart(userQuery))) {
|
|
52
|
+
return this.transformToPieChart(filteredData, groupByField);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
// 4. Secondary: Products as carousel if not already handled
|
|
@@ -87,13 +90,16 @@ export class UITransformer {
|
|
|
87
90
|
* Transform data to pie chart format
|
|
88
91
|
*/
|
|
89
92
|
private static transformToPieChart(
|
|
90
|
-
data: VectorMatch[]
|
|
93
|
+
data: VectorMatch[],
|
|
94
|
+
groupByField: string
|
|
91
95
|
): UITransformationResponse {
|
|
92
|
-
|
|
93
|
-
|
|
96
|
+
let categories = this.detectCategories(data, groupByField);
|
|
97
|
+
if (categories.length === 0) categories = ['All Data'];
|
|
98
|
+
|
|
99
|
+
const categoryData = this.aggregateByCategory(data, categories, groupByField);
|
|
94
100
|
|
|
95
101
|
const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => {
|
|
96
|
-
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
|
|
102
|
+
const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data, groupByField);
|
|
97
103
|
return {
|
|
98
104
|
label,
|
|
99
105
|
value: count as number,
|
|
@@ -104,7 +110,7 @@ export class UITransformer {
|
|
|
104
110
|
|
|
105
111
|
return {
|
|
106
112
|
type: 'pie_chart',
|
|
107
|
-
title:
|
|
113
|
+
title: `Distribution by ${groupByField.charAt(0).toUpperCase() + groupByField.slice(1)}`,
|
|
108
114
|
description: `Showing breakdown across ${categories.length} categories`,
|
|
109
115
|
data: pieData,
|
|
110
116
|
};
|
|
@@ -371,11 +377,16 @@ export class UITransformer {
|
|
|
371
377
|
return hasTimeKeyword || hasValidDateValue;
|
|
372
378
|
}
|
|
373
379
|
|
|
374
|
-
private static
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
380
|
+
private static isExplicitChartQuery(query: string): boolean {
|
|
381
|
+
const normalized = query.toLowerCase();
|
|
382
|
+
return normalized.includes('pie chart') ||
|
|
383
|
+
normalized.includes('piechart') ||
|
|
384
|
+
normalized.includes('bar chart') ||
|
|
385
|
+
normalized.includes('barchart') ||
|
|
386
|
+
normalized.includes('radar chart');
|
|
387
|
+
}
|
|
378
388
|
|
|
389
|
+
private static shouldShowCategoryChart(query: string): boolean {
|
|
379
390
|
const normalized = query.toLowerCase();
|
|
380
391
|
const chartKeywords = [
|
|
381
392
|
'distribution',
|
|
@@ -389,6 +400,10 @@ export class UITransformer {
|
|
|
389
400
|
'split',
|
|
390
401
|
'category breakdown',
|
|
391
402
|
'category distribution',
|
|
403
|
+
'pie chart',
|
|
404
|
+
'piechart',
|
|
405
|
+
'bar chart',
|
|
406
|
+
'barchart'
|
|
392
407
|
];
|
|
393
408
|
|
|
394
409
|
return chartKeywords.some(keyword => normalized.includes(keyword));
|
|
@@ -501,38 +516,57 @@ export class UITransformer {
|
|
|
501
516
|
}
|
|
502
517
|
|
|
503
518
|
/**
|
|
504
|
-
* Helper:
|
|
519
|
+
* Helper: Dynamically determine what field to group by based on user query
|
|
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
|
|
505
550
|
*/
|
|
506
|
-
private static detectCategories(data: VectorMatch[]): string[] {
|
|
551
|
+
private static detectCategories(data: VectorMatch[], groupByField: string): string[] {
|
|
507
552
|
const categories = new Set<string>();
|
|
508
553
|
|
|
509
554
|
data.forEach(item => {
|
|
510
555
|
const meta = item.metadata || {};
|
|
511
556
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
categories.add(String(
|
|
515
|
-
|
|
516
|
-
if (
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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());
|
|
557
|
+
if (meta[groupByField] !== undefined) {
|
|
558
|
+
const val = meta[groupByField];
|
|
559
|
+
if (Array.isArray(val)) val.forEach(v => categories.add(String(v)));
|
|
560
|
+
else categories.add(String(val));
|
|
561
|
+
} else if (groupByField === 'category') {
|
|
562
|
+
// Fallbacks for legacy 'category' extraction
|
|
563
|
+
if (meta.type) categories.add(String(meta.type));
|
|
564
|
+
if (meta.tag) {
|
|
565
|
+
const tags = Array.isArray(meta.tag) ? meta.tag : [meta.tag];
|
|
566
|
+
tags.forEach(t => categories.add(String(t)));
|
|
567
|
+
}
|
|
568
|
+
const categoryMatch = item.content.match(/(?:Category|Type|Class):\s*([^\n]+)/i);
|
|
569
|
+
if (categoryMatch) categories.add(categoryMatch[1].trim());
|
|
536
570
|
}
|
|
537
571
|
});
|
|
538
572
|
|
|
@@ -540,11 +574,12 @@ export class UITransformer {
|
|
|
540
574
|
}
|
|
541
575
|
|
|
542
576
|
/**
|
|
543
|
-
* Helper: Aggregate data
|
|
577
|
+
* Helper: Aggregate data dynamically
|
|
544
578
|
*/
|
|
545
579
|
private static aggregateByCategory(
|
|
546
580
|
data: VectorMatch[],
|
|
547
|
-
categories: string[]
|
|
581
|
+
categories: string[],
|
|
582
|
+
groupByField: string
|
|
548
583
|
): Record<string, number> {
|
|
549
584
|
const result: Record<string, number> = {};
|
|
550
585
|
|
|
@@ -554,7 +589,13 @@ export class UITransformer {
|
|
|
554
589
|
|
|
555
590
|
data.forEach(item => {
|
|
556
591
|
const meta = item.metadata || {};
|
|
557
|
-
|
|
592
|
+
let itemCategory = 'Other';
|
|
593
|
+
|
|
594
|
+
if (meta[groupByField] !== undefined) {
|
|
595
|
+
itemCategory = String(meta[groupByField]);
|
|
596
|
+
} else if (groupByField === 'category' && meta.type) {
|
|
597
|
+
itemCategory = String(meta.type);
|
|
598
|
+
}
|
|
558
599
|
|
|
559
600
|
if (Object.prototype.hasOwnProperty.call(result, itemCategory)) {
|
|
560
601
|
result[itemCategory]++;
|
|
@@ -615,15 +656,21 @@ export class UITransformer {
|
|
|
615
656
|
}
|
|
616
657
|
|
|
617
658
|
/**
|
|
618
|
-
* Helper: Calculate stock counts
|
|
659
|
+
* Helper: Calculate stock counts dynamically
|
|
619
660
|
*/
|
|
620
|
-
private static calculateStockCounts(category: string, data: VectorMatch[]): { inStockCount: number; outOfStockCount: number } {
|
|
661
|
+
private static calculateStockCounts(category: string, data: VectorMatch[], groupByField: string): { inStockCount: number; outOfStockCount: number } {
|
|
621
662
|
let inStock = 0;
|
|
622
663
|
let outOfStock = 0;
|
|
623
664
|
|
|
624
665
|
data.forEach(d => {
|
|
625
666
|
const meta = d.metadata || {};
|
|
626
|
-
|
|
667
|
+
let itemCategory = 'Other';
|
|
668
|
+
if (meta[groupByField] !== undefined) {
|
|
669
|
+
itemCategory = String(meta[groupByField]);
|
|
670
|
+
} else if (groupByField === 'category' && meta.type) {
|
|
671
|
+
itemCategory = String(meta.type);
|
|
672
|
+
}
|
|
673
|
+
|
|
627
674
|
if (itemCategory === category) {
|
|
628
675
|
if (this.determineStockStatus(d)) {
|
|
629
676
|
inStock++;
|
|
@@ -772,12 +819,13 @@ DATA SHAPE per type:
|
|
|
772
819
|
- text: { "content": "<prose answer>" }
|
|
773
820
|
|
|
774
821
|
DECISION RULES (follow strictly):
|
|
775
|
-
1.
|
|
776
|
-
2.
|
|
777
|
-
3.
|
|
778
|
-
4.
|
|
779
|
-
5.
|
|
780
|
-
6.
|
|
822
|
+
1. IF the user explicitly asks for a "pie chart", "bar chart", or "table", you MUST output that exact type. Do not deviate from their request.
|
|
823
|
+
2. bar_chart → comparing quantities across categories (e.g. sales by region, price by product). Use this when there is only ONE value per category.
|
|
824
|
+
3. line_chart → trends or changes over time (dates, months, years, sequential events)
|
|
825
|
+
4. pie_chart → proportional breakdown or percentage distribution
|
|
826
|
+
5. radar_chart → comparing 2 or more products across MULTIPLE attributes.
|
|
827
|
+
6. table → multi-field structured records where each item has ≥ 3 attributes
|
|
828
|
+
7. text → conversational or free-form answers where no chart adds value
|
|
781
829
|
|
|
782
830
|
IMPORTANT:
|
|
783
831
|
- Aggregate numeric values from the raw data — do not pass raw object arrays as data.
|