@retrivora-ai/rag-engine 2.0.2 → 2.0.3

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.
Files changed (49) hide show
  1. package/FREE_TIER_ARCHITECTURE.md +232 -0
  2. package/README.md +9 -0
  3. package/dist/{ILLMProvider-DMxLyTdq.d.mts → ILLMProvider-0rRBYbVW.d.mts} +133 -1
  4. package/dist/{ILLMProvider-DMxLyTdq.d.ts → ILLMProvider-0rRBYbVW.d.ts} +133 -1
  5. package/dist/handlers/index.d.mts +2 -2
  6. package/dist/handlers/index.d.ts +2 -2
  7. package/dist/handlers/index.js +679 -15
  8. package/dist/handlers/index.mjs +679 -15
  9. package/dist/index-B1wGUlSL.d.mts +532 -0
  10. package/dist/{index-tzwc7UhY.d.ts → index-BIHHp_f6.d.ts} +1 -1
  11. package/dist/{index-DHsFLJXQ.d.mts → index-BvODr57d.d.mts} +1 -1
  12. package/dist/index-DcklhThn.d.ts +532 -0
  13. package/dist/index.css +51 -3
  14. package/dist/index.d.mts +6 -105
  15. package/dist/index.d.ts +6 -105
  16. package/dist/index.js +1918 -8
  17. package/dist/index.mjs +1913 -8
  18. package/dist/server.d.mts +51 -7
  19. package/dist/server.d.ts +51 -7
  20. package/dist/server.js +721 -15
  21. package/dist/server.mjs +704 -15
  22. package/package.json +9 -7
  23. package/src/app/page.tsx +54 -18
  24. package/src/components/ChatWindow.tsx +2 -2
  25. package/src/components/DocumentUpload.tsx +5 -4
  26. package/src/components/MessageBubble.tsx +2 -4
  27. package/src/core/Pipeline.ts +23 -5
  28. package/src/handlers/index.ts +48 -1
  29. package/src/index.ts +22 -4
  30. package/src/rendering/IntentClassifier.ts +167 -0
  31. package/src/rendering/RendererRegistry.ts +164 -0
  32. package/src/rendering/RuleEngine.ts +67 -0
  33. package/src/rendering/VisualizationDecisionEngine.ts +164 -0
  34. package/src/rendering/__tests__/VisualizationDecisionEngine.test.ts +237 -0
  35. package/src/rendering/index.ts +12 -0
  36. package/src/rendering/rules/Rule1SpecificInfoRule.ts +28 -0
  37. package/src/rendering/rules/Rule2ComparisonRule.ts +24 -0
  38. package/src/rendering/rules/Rule3ProductDiscoveryRule.ts +28 -0
  39. package/src/rendering/rules/Rule4AnalyticalRule.ts +38 -0
  40. package/src/rendering/rules/Rule5MixedResponseRule.ts +29 -0
  41. package/src/rendering/rules/Rule6SmallResultSetRule.ts +24 -0
  42. package/src/rendering/rules/Rule7LargeTableRule.ts +35 -0
  43. package/src/rendering/types.ts +96 -0
  44. package/src/server.ts +3 -2
  45. package/src/types/index.ts +48 -0
  46. package/src/utils/DocumentParser.ts +64 -28
  47. package/src/utils/UITransformer.ts +15 -4
  48. package/dist/index-CfkqZd2Y.d.ts +0 -197
  49. package/dist/index-xygonxpW.d.mts +0 -197
@@ -0,0 +1,24 @@
1
+ import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule2ComparisonRule implements IRenderRule {
4
+ readonly name = 'Rule2Comparison';
5
+ readonly priority = 90;
6
+
7
+ evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
8
+ const q = (context.userQuery || '').toLowerCase().trim();
9
+
10
+ const isComparison =
11
+ intent === 'comparison' ||
12
+ /\b(compare|comparison|vs\.?|versus|difference between|differ|contrast|against)\b/i.test(q);
13
+
14
+ if (isComparison) {
15
+ return {
16
+ renderType: 'table',
17
+ confidence: 0.90,
18
+ reason: 'Rule 2: Comparison query contrasting multiple entities requires structured table rendering.',
19
+ };
20
+ }
21
+
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,28 @@
1
+ import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule3ProductDiscoveryRule implements IRenderRule {
4
+ readonly name = 'Rule3ProductDiscovery';
5
+ readonly priority = 85;
6
+
7
+ evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
8
+ const q = (context.userQuery || '').toLowerCase().trim();
9
+
10
+ const isDiscovery =
11
+ intent === 'product_search' ||
12
+ intent === 'recommendation' ||
13
+ /\b(best|recommend|top|buy|shopping|under \$|cheap|shoes|laptops|chairs|headphones|sneakers|apparel|products|show available)\b/i.test(q);
14
+
15
+ const isNotChart = !/\b(chart|sales|revenue|monthly|growth|trend|over time)\b/i.test(q);
16
+ const isAnalyticalRanking = /\b(top performing|top selling|best performing|performing|sales|revenue|ranking)\b/i.test(q);
17
+
18
+ if (isDiscovery && isNotChart && !isAnalyticalRanking) {
19
+ return {
20
+ renderType: 'carousel',
21
+ confidence: 0.90,
22
+ reason: 'Rule 3: Product discovery/recommendation query requires product carousel rendering.',
23
+ };
24
+ }
25
+
26
+ return null;
27
+ }
28
+ }
@@ -0,0 +1,38 @@
1
+ import { ChartType, DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule4AnalyticalRule implements IRenderRule {
4
+ readonly name = 'Rule4Analytical';
5
+ readonly priority = 80;
6
+
7
+ evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
8
+ const q = (context.userQuery || '').toLowerCase().trim();
9
+
10
+ const isAnalytical =
11
+ intent === 'analytics' ||
12
+ intent === 'trend_analysis' ||
13
+ intent === 'ranking' ||
14
+ /\b(monthly sales|revenue growth|top performing|population trends|sales by|growth|distribution|share|percentage|breakdown|correlation|scatter|trend)\b/i.test(q);
15
+
16
+ if (!isAnalytical) return null;
17
+
18
+ // Determine specific chart type based on query intent signals
19
+ let chartType: ChartType = 'bar'; // Default category comparison
20
+
21
+ if (intent === 'trend_analysis' || /\b(monthly|yearly|weekly|growth|over time|trend|timeline|historical)\b/i.test(q)) {
22
+ chartType = 'line';
23
+ } else if (/\b(share|percentage|percent|breakup|breakdown|composition|pie|donut|proportion)\b/i.test(q)) {
24
+ chartType = 'pie';
25
+ } else if (/\b(correlation|scatter|relationship|relation|impact of|depends on)\b/i.test(q)) {
26
+ chartType = 'scatter';
27
+ } else {
28
+ chartType = 'bar';
29
+ }
30
+
31
+ return {
32
+ renderType: 'chart',
33
+ chartType,
34
+ confidence: 0.85,
35
+ reason: `Rule 4: Analytical query with numerical insights selects ${chartType} chart.`,
36
+ };
37
+ }
38
+ }
@@ -0,0 +1,29 @@
1
+ import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule5MixedResponseRule implements IRenderRule {
4
+ readonly name = 'Rule5MixedResponse';
5
+ readonly priority = 95;
6
+
7
+ evaluate(context: DecisionContext, _intent: IntentCategory): RenderDecision | null {
8
+ const q = (context.userQuery || '').toLowerCase().trim();
9
+
10
+ // Specific mixed intent match: "top selling products this month" / multi-section requests
11
+ const isTopSellingThisMonth = /\btop\s*(selling|performing)?\s*products?\s*(this|last)?\s*(month|year|week)\b/i.test(q);
12
+ const hasMultipleSectionsRequest = /\b(summary|overview)\b/i.test(q) && /\b(carousel|products?|items?)\b/i.test(q) && /\b(chart|sales|trend|revenue)\b/i.test(q);
13
+
14
+ if (isTopSellingThisMonth || hasMultipleSectionsRequest) {
15
+ return {
16
+ renderType: 'mixed',
17
+ confidence: 0.95,
18
+ reason: 'Rule 5: Mixed response query requires multi-section composition (text, carousel, chart).',
19
+ sections: [
20
+ { type: 'text', title: 'Summary' },
21
+ { type: 'carousel', title: 'Top Products' },
22
+ { type: 'chart', chartType: 'bar', title: 'Sales Performance' },
23
+ ],
24
+ };
25
+ }
26
+
27
+ return null;
28
+ }
29
+ }
@@ -0,0 +1,24 @@
1
+ import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule6SmallResultSetRule implements IRenderRule {
4
+ readonly name = 'Rule6SmallResultSet';
5
+ readonly priority = 110;
6
+
7
+ evaluate(context: DecisionContext, _intent: IntentCategory): RenderDecision | null {
8
+ const docs = context.retrievedDocuments || [];
9
+
10
+ // Only apply if documents are present and fewer than 3
11
+ if (docs.length > 0 && docs.length < 3) {
12
+ const q = (context.userQuery || '').toLowerCase().trim();
13
+ const isExplicitComparison = /\b(compare|versus|vs\.?)\b/i.test(q);
14
+
15
+ return {
16
+ renderType: isExplicitComparison ? 'table' : 'text',
17
+ confidence: 0.98,
18
+ reason: `Rule 6: Small result set (${docs.length} rows < 3). Charts avoided to prevent sparse or uninformative visualizations.`,
19
+ };
20
+ }
21
+
22
+ return null;
23
+ }
24
+ }
@@ -0,0 +1,35 @@
1
+ import { DecisionContext, IntentCategory, IRenderRule, RenderDecision } from '../types';
2
+
3
+ export class Rule7LargeTableRule implements IRenderRule {
4
+ readonly name = 'Rule7LargeTable';
5
+ readonly priority = 75;
6
+
7
+ evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null {
8
+ const docs = context.retrievedDocuments || [];
9
+
10
+ if (docs.length > 5) {
11
+ const q = (context.userQuery || '').toLowerCase().trim();
12
+ const hasNumericAgg = intent === 'analytics' || intent === 'ranking' || /\b(sales|revenue|growth|count|total|average)\b/i.test(q);
13
+
14
+ if (hasNumericAgg) {
15
+ return {
16
+ renderType: 'mixed',
17
+ confidence: 0.88,
18
+ reason: `Rule 7: Large result set (${docs.length} rows > 5) with numeric aggregation rendered as Table + Chart.`,
19
+ sections: [
20
+ { type: 'table', title: 'Detailed Data Table' },
21
+ { type: 'chart', chartType: 'bar', title: 'Numerical Breakdown' },
22
+ ],
23
+ };
24
+ }
25
+
26
+ return {
27
+ renderType: 'table',
28
+ confidence: 0.85,
29
+ reason: `Rule 7: Large result set (${docs.length} rows > 5) rendered as structured table for readability.`,
30
+ };
31
+ }
32
+
33
+ return null;
34
+ }
35
+ }
@@ -0,0 +1,96 @@
1
+ import { VectorMatch, UITransformationResponse } from '../types';
2
+
3
+ /**
4
+ * Primary rendering outcome types supported by the decision engine.
5
+ */
6
+ export type RenderType =
7
+ | 'text'
8
+ | 'table'
9
+ | 'carousel'
10
+ | 'chart'
11
+ | 'mixed'
12
+ | 'card'
13
+ | 'none';
14
+
15
+ /**
16
+ * Chart sub-types available when renderType === 'chart'.
17
+ */
18
+ export type ChartType =
19
+ | 'bar'
20
+ | 'line'
21
+ | 'pie'
22
+ | 'scatter';
23
+
24
+ /**
25
+ * High-level intent classification categories.
26
+ */
27
+ export type IntentCategory =
28
+ | 'information_lookup'
29
+ | 'product_search'
30
+ | 'recommendation'
31
+ | 'comparison'
32
+ | 'analytics'
33
+ | 'trend_analysis'
34
+ | 'ranking'
35
+ | 'summarization';
36
+
37
+ /**
38
+ * Individual section specifier for multi-section (mixed) responses.
39
+ */
40
+ export interface RenderSectionDecision {
41
+ type: RenderType;
42
+ chartType?: ChartType;
43
+ title?: string;
44
+ }
45
+
46
+ /**
47
+ * Final decision produced by the Visualization Decision Engine.
48
+ */
49
+ export interface RenderDecision {
50
+ /** Primary rendering format selected */
51
+ renderType: RenderType;
52
+ /** Specific chart sub-type if renderType === 'chart' */
53
+ chartType?: ChartType;
54
+ /** Confidence score between 0.0 and 1.0 */
55
+ confidence: number;
56
+ /** Human-readable explanation of why this decision was reached */
57
+ reason: string;
58
+ /** Sub-sections if renderType === 'mixed' */
59
+ sections?: RenderSectionDecision[];
60
+ }
61
+
62
+ /**
63
+ * Context provided to the intent classifier & decision engine rules.
64
+ */
65
+ export interface DecisionContext {
66
+ /** Original user prompt/query string */
67
+ userQuery: string;
68
+ /** Vector DB retrieved documents / context records */
69
+ retrievedDocuments?: VectorMatch[];
70
+ /** Optional LLM text response if already available */
71
+ llmResponse?: string;
72
+ /** Additional custom metadata or config settings */
73
+ metadata?: Record<string, unknown>;
74
+ }
75
+
76
+ /**
77
+ * Interface implemented by rule strategies in the Rule Engine.
78
+ */
79
+ export interface IRenderRule {
80
+ /** Unique name of the decision rule */
81
+ name: string;
82
+ /** Precedence score (higher priority rules run first) */
83
+ priority: number;
84
+ /** Evaluate the rule against current query context and classified intent */
85
+ evaluate(context: DecisionContext, intent: IntentCategory): RenderDecision | null;
86
+ }
87
+
88
+ /**
89
+ * Interface implemented by pluggable Renderer Strategies in RendererRegistry.
90
+ */
91
+ export interface IRendererStrategy {
92
+ /** Render type identifier handled by this strategy */
93
+ type: RenderType | string;
94
+ /** Transform input data into a standardized UITransformationResponse payload */
95
+ render(data: unknown, options?: Record<string, unknown>): UITransformationResponse;
96
+ }
package/src/server.ts CHANGED
@@ -20,7 +20,7 @@ export type {
20
20
  LLMProvider,
21
21
  EmbeddingProvider,
22
22
  } from './config/RagConfig';
23
- export type { VectorMatch, UpsertDocument, IngestDocument, ChatResponse, ChatMessage, ChatOptions } from './types';
23
+ export type * from './types';
24
24
  export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
25
25
  export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
26
26
  export type { HealthCheckResult, IProviderValidator, IProviderHealthChecker } from './core/ProviderInterfaces';
@@ -45,9 +45,10 @@ export { ConfigBuilder, PRESETS, createFromPreset } from './config/ConfigBuilder
45
45
  export { EmbeddingStrategyResolver, EmbeddingStrategy } from './config/EmbeddingStrategy';
46
46
  export { LLM_PROFILES, VECTOR_PROFILES } from './config/UniversalProfiles';
47
47
 
48
- // ── RAG Utilities ──────────────────────────────────────────────
48
+ // ── RAG Utilities & Visualization ──────────────────────────────────────────────
49
49
  export { DocumentChunker } from './rag/DocumentChunker';
50
50
  export { DocumentParser } from './utils/DocumentParser';
51
+ export * from './rendering';
51
52
 
52
53
  // ── Vector DB Providers ────────────────────────────────────────
53
54
  export { BaseVectorProvider } from './providers/vectordb/BaseVectorProvider';
@@ -226,3 +226,51 @@ export interface CarouselProduct {
226
226
 
227
227
  export * from './props';
228
228
  export * from './chat';
229
+
230
+ // ─── Global Retrivora Metadata Schema ───────────────────────────────────────
231
+
232
+ /**
233
+ * Standardized Retrivora Metadata Schema for document vectors and storage.
234
+ * Guarantees uniform metadata structure across vector DB backends (Pinecone, MongoDB, etc.).
235
+ */
236
+ export interface RetrivoraChunkMetadata {
237
+ /** Document unique identifier */
238
+ docId: string;
239
+ /** Original file name (e.g. report.pdf, data.json, sheets.xlsx) */
240
+ fileName: string;
241
+ /** File format type identifier */
242
+ fileType: 'txt' | 'json' | 'pdf' | 'excel' | 'word' | 'md' | 'csv' | string;
243
+ /** MIME type string */
244
+ mimeType: string;
245
+ /** 0-indexed position of chunk within the parent document */
246
+ chunkIndex: number;
247
+ /** Total count of chunks in the document */
248
+ totalChunks: number;
249
+ /** Extracted text content of the chunk */
250
+ content: string;
251
+ /** Character length of the chunk content */
252
+ characterCount: number;
253
+ /** Workspace or Project ID */
254
+ workspaceId: string;
255
+ /** Isolated vector database namespace (e.g. Pinecone namespace) */
256
+ pineconeNamespace: string;
257
+ /** Retrivora Tier level */
258
+ tier: 'free' | 'enterprise';
259
+ /** ISO 8601 timestamp when the document was processed */
260
+ ingestedAt: string;
261
+ /** Additional custom metadata Key-Value pairs */
262
+ customMetadata?: Record<string, unknown>;
263
+ }
264
+
265
+ export interface RetrivoraIngestedDocument {
266
+ docId: string;
267
+ fileName: string;
268
+ fileType: string;
269
+ mimeType: string;
270
+ fileSizeBytes?: number;
271
+ totalChunks: number;
272
+ workspaceId: string;
273
+ pineconeNamespace: string;
274
+ ingestedAt: string;
275
+ chunks: RetrivoraChunkMetadata[];
276
+ }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * DocumentParser — handles text extraction from various file formats.
3
- * Supported: .txt, .md, .json, .csv, .pdf (requires pdf-parse), .docx (requires mammoth)
3
+ * Supported: .txt, .md, .json, .csv, .pdf, .docx, .doc, .xlsx, .xls
4
4
  */
5
5
 
6
6
  export class DocumentParser {
@@ -8,14 +8,14 @@ export class DocumentParser {
8
8
  * Extract text from a File or Buffer based on its type.
9
9
  */
10
10
  static async parse(file: File | Buffer, fileName: string, mimeType: string): Promise<string> {
11
- const extension = fileName.split('.').pop()?.toLowerCase();
11
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
12
12
 
13
- // 1. Basic text formats
13
+ // 1. Basic text & Markdown formats
14
14
  if (extension === 'txt' || extension === 'md' || mimeType === 'text/plain' || mimeType === 'text/markdown') {
15
15
  return this.readAsText(file);
16
16
  }
17
17
 
18
- // 2. JSON
18
+ // 2. JSON files
19
19
  if (extension === 'json' || mimeType === 'application/json') {
20
20
  const text = await this.readAsText(file);
21
21
  try {
@@ -26,39 +26,75 @@ export class DocumentParser {
26
26
  }
27
27
  }
28
28
 
29
- // 3. CSV
29
+ // 3. CSV files
30
30
  if (extension === 'csv' || mimeType === 'text/csv') {
31
31
  return this.readAsText(file);
32
32
  }
33
33
 
34
- // 4. PDF (Requires pdf-parse)
34
+ // 4. Excel spreadsheets (.xlsx, .xls)
35
+ if (
36
+ extension === 'xlsx' ||
37
+ extension === 'xls' ||
38
+ mimeType.includes('spreadsheet') ||
39
+ mimeType.includes('excel')
40
+ ) {
41
+ try {
42
+ // Dynamic import with ts-ignore to allow optional resolution
43
+ const xlsx = await import('xlsx');
44
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
45
+ const workbook = xlsx.read(buffer, { type: 'buffer' });
46
+ const sheetsText: string[] = [];
47
+
48
+ for (const sheetName of workbook.SheetNames) {
49
+ const sheet = workbook.Sheets[sheetName];
50
+ const csv = xlsx.utils.sheet_to_csv(sheet);
51
+ if (csv && csv.trim()) {
52
+ sheetsText.push(`--- Sheet: ${sheetName} ---\n${csv.trim()}`);
53
+ }
54
+ }
55
+ return sheetsText.join('\n\n') || `[Empty Excel Workbook: ${fileName}]`;
56
+ } catch {
57
+ // Fall back to reading text lines if xlsx module is not present
58
+ try {
59
+ return await this.readAsText(file);
60
+ } catch {
61
+ return `[Excel File Content: ${fileName}]`;
62
+ }
63
+ }
64
+ }
65
+
66
+ // 5. PDF Documents (Requires pdf-parse)
35
67
  if (extension === 'pdf' || mimeType === 'application/pdf') {
36
- try {
37
- // Dynamic import to avoid errors if not installed
38
- const pdf = await import('pdf-parse');
39
- const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
40
- const data = await pdf.default(buffer);
41
- return data.text;
42
- } catch (err) {
43
- console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
44
- return `[PDF Parsing Error for ${fileName}]`;
45
- }
68
+ try {
69
+ const pdf = await import('pdf-parse');
70
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
71
+ const data = await pdf.default(buffer);
72
+ return data.text;
73
+ } catch (err) {
74
+ console.warn('[DocumentParser] PDF parsing failed. Make sure "pdf-parse" is installed.', err);
75
+ return `[PDF Parsing Error for ${fileName}]`;
76
+ }
46
77
  }
47
78
 
48
- // 5. DOCX (Requires mammoth)
49
- if (extension === 'docx' || mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') {
50
- try {
51
- const mammoth = await import('mammoth');
52
- const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
53
- const result = await mammoth.extractRawText({ buffer });
54
- return result.value;
55
- } catch (err) {
56
- console.warn('[DocumentParser] DOCX parsing failed. Make sure "mammoth" is installed.', err);
57
- return `[DOCX Parsing Error for ${fileName}]`;
58
- }
79
+ // 6. Word Documents (.docx, .doc) (Requires mammoth)
80
+ if (
81
+ extension === 'docx' ||
82
+ extension === 'doc' ||
83
+ mimeType === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
84
+ mimeType === 'application/msword'
85
+ ) {
86
+ try {
87
+ const mammoth = await import('mammoth');
88
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await (file as File).arrayBuffer());
89
+ const result = await mammoth.extractRawText({ buffer });
90
+ return result.value;
91
+ } catch (err) {
92
+ console.warn('[DocumentParser] Word parsing failed. Make sure "mammoth" is installed.', err);
93
+ return `[Word Parsing Error for ${fileName}]`;
94
+ }
59
95
  }
60
96
 
61
- // Fallback: try reading as text
97
+ // Fallback: try reading as plain UTF-8 text
62
98
  try {
63
99
  return await this.readAsText(file);
64
100
  } catch {
@@ -12,6 +12,7 @@ import {
12
12
  } from '../types';
13
13
  import { ILLMProvider } from '../llm/ILLMProvider';
14
14
  import { resolveMetadataValue } from './synonyms';
15
+ import { VisualizationDecisionEngine } from '../rendering/VisualizationDecisionEngine';
15
16
 
16
17
  // ─── Intent Types ─────────────────────────────────────────────────────────────
17
18
 
@@ -192,6 +193,9 @@ export class UITransformer {
192
193
 
193
194
  // Priority 7 – Product carousel only for explicit product intent.
194
195
  if ((hasProducts || this.isProductQuery(userQuery)) && resolvedIntent.visualizationHint === 'product_browse') {
196
+ if (!this.isProductQuery(userQuery)) {
197
+ return this.transformToText(filteredData);
198
+ }
195
199
  return this.transformToProductCarousel(filteredData, config, trainedSchema);
196
200
  }
197
201
 
@@ -214,6 +218,13 @@ export class UITransformer {
214
218
  sources: VectorMatch[],
215
219
  llm: ILLMProvider,
216
220
  ): Promise<UITransformationResponse> {
221
+ // ── Step 0: Fast Visualization Decision Engine check ────────────────────
222
+ const decision = VisualizationDecisionEngine.decideVisualization(query, sources);
223
+ if (decision.renderType === 'text') {
224
+ console.debug('[UITransformer] Decision Engine selected plain text rendering — skipping visual LLM calls.');
225
+ return this.transformToText(sources);
226
+ }
227
+
217
228
  // ── Step 1: Detect intent ───────────────────────────────────────────────
218
229
  let intent: QueryIntent;
219
230
  try {
@@ -768,7 +779,7 @@ RULES:
768
779
 
769
780
  private static transformToText(data: VectorMatch[]): UITransformationResponse {
770
781
  return this.createTextResponse(
771
- 'Search Results',
782
+ 'Retrieved Context',
772
783
  data.map(item => item.content).join('\n\n'),
773
784
  `Found ${data.length} relevant results`,
774
785
  );
@@ -974,8 +985,8 @@ RULES:
974
985
  private static isProductData(item: VectorMatch): boolean {
975
986
  const content = (item.content || '').toLowerCase();
976
987
  const productKeywords = [
977
- 'product', 'price', 'stock', 'item', 'sku', 'brand', 'model', 'msrp',
978
- 'inventory', 'buy', 'shop', 'beauty', 'care', 'cosmetic', 'facial',
988
+ 'product', 'price', 'stock', 'sku', 'brand', 'msrp',
989
+ 'inventory', 'buy', 'shop', 'beauty', 'cosmetic', 'facial',
979
990
  'cream', 'serum', 'mask', 'makeup', 'fragrance',
980
991
  ];
981
992
  const hasKeywords = productKeywords.some(kw => content.includes(kw));
@@ -984,7 +995,7 @@ RULES:
984
995
  const hasMetadataKey = Object.keys(metadata).some(k => {
985
996
  const val = metadata[k];
986
997
  return (
987
- ['price', 'product', 'sku', 'brand', 'model', 'cost', 'item'].includes(k.toLowerCase()) &&
998
+ ['price', 'product', 'sku', 'brand', 'cost'].includes(k.toLowerCase()) &&
988
999
  val !== null && val !== undefined && val !== ''
989
1000
  );
990
1001
  });