@retrivora-ai/rag-engine 1.7.8 → 1.8.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.
@@ -10,6 +10,7 @@ import { SourceCard } from './SourceCard';
10
10
  import { ProductCarousel } from './ProductCarousel';
11
11
  import { DynamicChart } from './DynamicChart';
12
12
  import { VisualizationRenderer } from './VisualizationRenderer';
13
+ import { UITransformationResponse } from '../utils/UITransformer';
13
14
 
14
15
  // ─── JSON sanitization ────────────────────────────────────────────────────────
15
16
  // Order matters: each step is a targeted fix, no step undoes a previous one.
@@ -520,18 +521,25 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
520
521
  config.chartType = (config.chartType as 'pie' | 'bar' | 'line') || 'bar';
521
522
  }
522
523
 
523
- // 2. Auto-Detection: Fill in missing view
524
- if (!config.view) {
525
- if (config.type === 'products' || Array.isArray(config.items) || hasProductLikeData) {
524
+ // 2. Auto-Detection: Fill in missing or dynamic view
525
+ if (!config.view || ['pie', 'bar', 'line', 'pie_chart', 'bar_chart', 'line_chart'].includes(String(config.view))) {
526
+ const viewStr = String(config.view || '').toLowerCase();
527
+
528
+ if (viewStr.includes('carousel') || config.type === 'products' || Array.isArray(config.items) || hasProductLikeData) {
526
529
  config.view = 'carousel';
527
530
  } else if (
531
+ viewStr.includes('chart') ||
532
+ viewStr.includes('pie') || viewStr.includes('bar') || viewStr.includes('line') ||
528
533
  hasAggregationLikeData ||
529
534
  (typeof config.type === 'string' && ['pie', 'bar', 'line'].includes(config.type)) ||
530
535
  (typeof config.chartType === 'string' && ['pie', 'bar', 'line'].includes(config.chartType))
531
536
  ) {
532
537
  config.view = 'chart';
533
538
  config.chartType = (config.chartType as 'pie' | 'bar' | 'line') ||
539
+ (viewStr.replace('_chart', '') as 'pie' | 'bar' | 'line') ||
534
540
  (config.type as 'pie' | 'bar' | 'line') || 'bar';
541
+ } else if (viewStr.includes('table')) {
542
+ config.view = 'table';
535
543
  } else {
536
544
  config.view = 'table';
537
545
  }
@@ -588,12 +596,12 @@ function UIDispatcher({ rawContent, primaryColor, accentColor, isStreaming, onAd
588
596
  />
589
597
  {hasInsights && (
590
598
  <div className="mt-4 flex flex-wrap gap-2 px-2">
591
- {config.insights?.map((insight) => (
599
+ {config.insights?.map((insight, i) => (
592
600
  <span
593
- key={insight}
601
+ key={`insight-${i}`}
594
602
  className={`rounded-full border border-emerald-200 bg-emerald-50 ${isCompact ? 'px-2.5 py-1 text-[10px]' : 'px-3 py-1 text-[11px]'} font-medium text-emerald-700 dark:border-emerald-500/20 dark:bg-emerald-500/10 dark:text-emerald-300`}
595
603
  >
596
- {insight}
604
+ {String(insight)}
597
605
  </span>
598
606
  ))}
599
607
  </div>
@@ -990,9 +998,9 @@ export function MessageBubble({
990
998
  )}
991
999
 
992
1000
  {/* Automated UI Fallback (if LLM failed to output structured block but UITransformer found data) */}
993
- {!isUser && !structuredContent.payload && message.uiTransformation && (
1001
+ {!isUser && !structuredContent.payload && !!message.uiTransformation && (
994
1002
  <div className="mt-4 border-t border-slate-100 dark:border-white/5 pt-4">
995
- <VisualizationRenderer data={message.uiTransformation} />
1003
+ <VisualizationRenderer data={message.uiTransformation as UITransformationResponse} />
996
1004
  </div>
997
1005
  )}
998
1006
 
@@ -123,19 +123,19 @@ Classify the query into ONE of the following intents:
123
123
 
124
124
  ---
125
125
 
126
- ## 2. VIEW MAPPING (STRICT)
126
+ ## 2. VIEW SELECTION (DYNAMIC)
127
127
 
128
- | Intent | View |
129
- |----------|-----------|
130
- | explore | carousel |
131
- | analyze | chart |
132
- | compare | table |
133
- | detail | table |
128
+ Choose the best primary "view" ("carousel", "chart", or "table") based on the data:
129
+ - TRENDS/GROWTH → "chart" (type: "line")
130
+ - DISTRIBUTIONS/RATIOS → "chart" (type: "pie")
131
+ - COMPARISONS → "chart" (type: "bar")
132
+ - PRODUCT LISTS → "carousel"
133
+ - RAW DATA/SPECS → "table"
134
134
 
135
- ⚠️ Override Rules:
136
- - If query contains "distribution", "percentage", "ratio" ALWAYS chart
137
- - If query contains "show products", "list items" ALWAYS carousel
138
- - NEVER mix views
135
+ ⚠️ Dynamic Overrides:
136
+ - If query is "show products", ALWAYS use carousel.
137
+ - If data is statistical, ALWAYS use a chart.
138
+ - NEVER explain why you chose a view.
139
139
 
140
140
  ---
141
141
 
@@ -193,7 +193,8 @@ Classify the query into ONE of the following intents:
193
193
  {
194
194
  "label": "string",
195
195
  "value": number,
196
- "inStock": boolean (optional)
196
+ "inStockCount": number (optional),
197
+ "outOfStockCount": number (optional)
197
198
  }
198
199
 
199
200
  ### 🔹 TABLE FORMAT
@@ -204,14 +205,14 @@ Classify the query into ONE of the following intents:
204
205
 
205
206
  ## 5. DATA RULES
206
207
 
207
- - NEVER hallucinate fields or data
208
- - ONLY use retrieved vector DB data
209
- - NO placeholders ("...", "N/A")
210
- - If image/link missing REMOVE FIELD
211
- - Aggregate BEFORE rendering chart
212
- - IF NO DATA IS FOUND in the retrieved context:
213
- DO NOT output a \`\`\`ui\`\`\` block
214
- Instead, state clearly that no relevant products or data were found.
208
+ - NEVER hallucinate fields or data.
209
+ - ONLY use data provided in the retrieved context.
210
+ - STRICT DATA ISOLATION: The labels and numbers in the EXAMPLES section below are placeholders. NEVER use them.
211
+ - IF NO DATA IS FOUND: You MUST NOT generate a chart. You MUST NOT say "This example assumes" or provide hypothetical numbers. Simply state "No relevant data found in the database." and STOP.
212
+ - NO META-COMMENTARY: NEVER talk about the task itself (e.g., "I've used JSON", "In a real-world app", "Dedicated library").
213
+ - NEVER explain YOUR formatting choice.
214
+ - Aggregate BEFORE rendering chart.
215
+ - DYNAMIC INTELLIGENCE: Autonomously extract relevant categories/values from the context and calculate totals for the "data" array based on the user query.
215
216
 
216
217
  ---
217
218
 
@@ -228,11 +229,12 @@ Classify the query into ONE of the following intents:
228
229
 
229
230
  ## 7. OUTPUT RULES
230
231
 
231
- - ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block
232
- - JSON must be VALID
233
- - No explanation inside block
234
- - NEVER use ASCII charts, Markdown tables, or text-based drawings for data.
235
- - If data is tabular or statistical, ALWAYS use the \`\`\`ui\`\`\` block.
232
+ - ALWAYS return EXACTLY ONE \`\`\`ui\`\`\` block.
233
+ - JSON must be VALID.
234
+ - FIELD MAPPING (MANDATORY): Always use "label" for the name/category and "value" for the numeric count.
235
+ - NO xKey or yKeys: Do not include these fields; the UI uses "label" and "value" by default.
236
+ - SILENT DATA: If a chart is provided, DO NOT say "Here is the JSON" or "Below is the data". Simply output the block.
237
+ - TEXT OUTSIDE THE BLOCK: One brief sentence maximum.
236
238
 
237
239
  ---
238
240
 
@@ -243,8 +245,8 @@ User: "show me beauty products"
243
245
  \`\`\`ui
244
246
  {
245
247
  "view": "carousel",
246
- "title": "Beauty Products",
247
- "description": "Browse available beauty items",
248
+ "title": "TITLE",
249
+ "description": "DESCRIPTION",
248
250
  "metadata": {
249
251
  "intent": "explore",
250
252
  "confidence": 0.95
@@ -252,43 +254,41 @@ User: "show me beauty products"
252
254
  "carousel": {
253
255
  "items": [
254
256
  {
255
- "id": "1",
256
- "name": "Mascara",
257
- "brand": "Essence",
258
- "price": 9.99,
259
- "image": "https://example.com/mascara.jpg",
260
- "link": "https://example.com/p1",
257
+ "id": "ID",
258
+ "name": "PRODUCT_NAME",
259
+ "brand": "BRAND",
260
+ "price": 0,
261
+ "image": "URL",
262
+ "link": "URL",
261
263
  "inStock": true
262
264
  }
263
265
  ]
264
266
  },
265
- "insights": ["Most products are affordable"]
267
+ "insights": ["INSIGHT"]
266
268
  }
267
269
  \`\`\`
268
270
 
269
271
  ---
270
272
 
271
- User: "distribution of products across categories"
273
+
272
274
 
273
275
  \`\`\`ui
274
276
  {
275
277
  "view": "chart",
276
- "title": "Product Distribution",
277
- "description": "Category-wise product distribution",
278
+ "title": "TITLE",
279
+ "description": "DESCRIPTION",
278
280
  "metadata": {
279
281
  "intent": "analyze",
280
282
  "confidence": 0.98
281
283
  },
282
284
  "chart": {
283
285
  "type": "pie",
284
- "xKey": "label",
285
- "yKeys": ["value"],
286
286
  "data": [
287
- { "label": "Beauty", "value": 15 },
288
- { "label": "Electronics", "value": 10 }
287
+ { "label": "LABEL_A", "value": 0, "inStockCount": 0, "outOfStockCount": 0 },
288
+ { "label": "LABEL_B", "value": 0, "inStockCount": 0, "outOfStockCount": 0 }
289
289
  ]
290
290
  },
291
- "insights": ["Beauty dominates inventory"]
291
+ "insights": ["INSIGHT"]
292
292
  }
293
293
  \`\`\`
294
294
  `;
@@ -487,16 +487,18 @@ User: "distribution of products across categories"
487
487
  let sources: VectorMatch[] = [];
488
488
  let graphData: GraphSearchResult | undefined;
489
489
 
490
+ let uiTransformation: any;
490
491
  for await (const chunk of stream) {
491
492
  if (typeof chunk === 'string') {
492
493
  reply += chunk;
493
- } else if ('sources' in chunk) {
494
- sources = chunk.sources;
495
- graphData = chunk.graphData;
494
+ } else if (typeof chunk === 'object' && chunk !== null) {
495
+ if ('sources' in chunk) sources = chunk.sources;
496
+ if ('graphData' in chunk) graphData = chunk.graphData;
497
+ if ('ui_transformation' in chunk) uiTransformation = chunk.ui_transformation;
496
498
  }
497
499
  }
498
500
 
499
- return { reply, sources, graphData };
501
+ return { reply, sources, graphData, ui_transformation: uiTransformation };
500
502
  }
501
503
 
502
504
  /**
@@ -27,7 +27,7 @@ import { useStoredMessages } from './useStoredMessages';
27
27
  interface SseTextFrame { type: 'text'; text: string }
28
28
  interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[] }
29
29
  interface SseErrorFrame { type: 'error'; error: string }
30
- interface SseUiFrame { type: 'ui_transformation'; data: any }
30
+ interface SseUiFrame { type: 'ui_transformation'; data: unknown }
31
31
  type SseFrame = SseTextFrame | SseMetaFrame | SseErrorFrame | SseUiFrame;
32
32
 
33
33
  /**
@@ -125,7 +125,7 @@ export function useRagChat(
125
125
  const decoder = new TextDecoder();
126
126
  let assistantContent = '';
127
127
  let sources: VectorMatch[] = [];
128
- let uiTransformation: any = null;
128
+ let uiTransformation: unknown = null;
129
129
  // Buffer for partial SSE frames that arrive split across reads
130
130
  let buffer = '';
131
131
 
@@ -174,7 +174,7 @@ export function useRagChat(
174
174
  ...msg,
175
175
  content: assistantContent,
176
176
  sources: sources.length > 0 ? sources : msg.sources,
177
- uiTransformation: uiTransformation || (msg as any).uiTransformation
177
+ uiTransformation: uiTransformation || (msg as RagMessage).uiTransformation
178
178
  }
179
179
  : msg
180
180
  )
package/src/types/chat.ts CHANGED
@@ -10,7 +10,7 @@ export interface ChatMessage {
10
10
  export interface RagMessage extends ChatMessage {
11
11
  id: string;
12
12
  sources?: VectorMatch[];
13
- uiTransformation?: any;
13
+ uiTransformation?: unknown;
14
14
  createdAt: string;
15
15
  }
16
16
 
@@ -22,6 +22,7 @@ export interface ChatResponse {
22
22
  reply: string;
23
23
  sources: VectorMatch[];
24
24
  graphData?: GraphSearchResult;
25
+ ui_transformation?: unknown;
25
26
  }
26
27
 
27
28
  export interface GraphNode {
@@ -27,7 +27,8 @@ export interface UITransformationResponse {
27
27
  export interface PieChartData {
28
28
  label: string;
29
29
  value: number;
30
- inStock?: boolean;
30
+ inStockCount?: number;
31
+ outOfStockCount?: number;
31
32
  [key: string]: unknown;
32
33
  }
33
34
 
@@ -37,7 +38,8 @@ export interface PieChartData {
37
38
  export interface BarChartData {
38
39
  category: string;
39
40
  value: number;
40
- inStock?: boolean;
41
+ inStockCount?: number;
42
+ outOfStockCount?: number;
41
43
  [key: string]: unknown;
42
44
  }
43
45
 
@@ -202,11 +204,15 @@ export class UITransformer {
202
204
  const categories = this.detectCategories(data);
203
205
  const categoryData = this.aggregateByCategory(data, categories);
204
206
 
205
- const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => ({
206
- label,
207
- value: count as number,
208
- inStock: this.checkStockStatus(label, data),
209
- }));
207
+ const pieData: PieChartData[] = Object.entries(categoryData).map(([label, count]) => {
208
+ const { inStockCount, outOfStockCount } = this.calculateStockCounts(label, data);
209
+ return {
210
+ label,
211
+ value: count as number,
212
+ inStockCount,
213
+ outOfStockCount,
214
+ };
215
+ });
210
216
 
211
217
  return {
212
218
  type: 'pie_chart',
@@ -225,11 +231,15 @@ export class UITransformer {
225
231
  const categories = this.detectCategories(data);
226
232
  const categoryData = this.aggregateByCategory(data, categories);
227
233
 
228
- const barData: BarChartData[] = Object.entries(categoryData).map(([category, value]) => ({
229
- category,
230
- value: value as number,
231
- inStock: this.checkStockStatus(category, data),
232
- }));
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
+ });
233
243
 
234
244
  return {
235
245
  type: 'bar_chart',
@@ -477,15 +487,25 @@ export class UITransformer {
477
487
  }
478
488
 
479
489
  /**
480
- * Helper: Check stock status
490
+ * Helper: Calculate stock counts by category
481
491
  */
482
- private static checkStockStatus(category: string, data: VectorMatch[]): boolean {
483
- const item = data.find(d => {
492
+ private static calculateStockCounts(category: string, data: VectorMatch[]): { inStockCount: number; outOfStockCount: number } {
493
+ let inStock = 0;
494
+ let outOfStock = 0;
495
+
496
+ data.forEach(d => {
484
497
  const meta = d.metadata || {};
485
- return meta.category === category || meta.type === category;
498
+ const itemCategory = (meta.category || meta.type || 'Other') as string;
499
+ if (itemCategory === category) {
500
+ if (this.determineStockStatus(d)) {
501
+ inStock++;
502
+ } else {
503
+ outOfStock++;
504
+ }
505
+ }
486
506
  });
487
507
 
488
- return this.determineStockStatus(item || ({} as VectorMatch));
508
+ return { inStockCount: inStock, outOfStockCount: outOfStock };
489
509
  }
490
510
 
491
511
  /**