@retrivora-ai/rag-engine 2.0.2 → 2.0.4

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 (51) 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 +681 -17
  8. package/dist/handlers/index.mjs +681 -17
  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 +723 -17
  21. package/dist/server.mjs +706 -17
  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/DatabaseStorage.ts +5 -1
  28. package/src/core/LicenseVerifier.ts +1 -1
  29. package/src/core/Pipeline.ts +23 -5
  30. package/src/handlers/index.ts +48 -1
  31. package/src/index.ts +22 -4
  32. package/src/rendering/IntentClassifier.ts +167 -0
  33. package/src/rendering/RendererRegistry.ts +164 -0
  34. package/src/rendering/RuleEngine.ts +67 -0
  35. package/src/rendering/VisualizationDecisionEngine.ts +164 -0
  36. package/src/rendering/__tests__/VisualizationDecisionEngine.test.ts +237 -0
  37. package/src/rendering/index.ts +12 -0
  38. package/src/rendering/rules/Rule1SpecificInfoRule.ts +28 -0
  39. package/src/rendering/rules/Rule2ComparisonRule.ts +24 -0
  40. package/src/rendering/rules/Rule3ProductDiscoveryRule.ts +28 -0
  41. package/src/rendering/rules/Rule4AnalyticalRule.ts +38 -0
  42. package/src/rendering/rules/Rule5MixedResponseRule.ts +29 -0
  43. package/src/rendering/rules/Rule6SmallResultSetRule.ts +24 -0
  44. package/src/rendering/rules/Rule7LargeTableRule.ts +35 -0
  45. package/src/rendering/types.ts +96 -0
  46. package/src/server.ts +3 -2
  47. package/src/types/index.ts +48 -0
  48. package/src/utils/DocumentParser.ts +64 -28
  49. package/src/utils/UITransformer.ts +15 -4
  50. package/dist/index-CfkqZd2Y.d.ts +0 -197
  51. package/dist/index-xygonxpW.d.mts +0 -197
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@retrivora-ai/rag-engine",
3
- "version": "2.0.2",
3
+ "version": "2.0.4",
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": "UNLICENSED",
@@ -69,19 +69,20 @@
69
69
  "dist",
70
70
  "src",
71
71
  ".env.example",
72
- "README.md"
72
+ "README.md",
73
+ "FREE_TIER_ARCHITECTURE.md"
73
74
  ],
74
75
  "scripts": {
75
- "dev": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --watch --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 --inject-style",
76
+ "dev": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --watch --tsconfig tsconfig.build.json --external react,react-dom,next,mongodb,pg,openai,@anthropic-ai/sdk,@pinecone-database/pinecone,axios,lucide-react,mammoth,pdf-parse,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style",
76
77
  "build": "npm run build:pkg",
77
- "build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && 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 --inject-style && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css && rm src/index.css",
78
+ "build:pkg": "npx @tailwindcss/cli -i src/tailwind.css -o src/index.css && 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,xlsx,react-markdown,remark-gfm,next-themes,langchain,@langchain/core,@langchain/openai,llamaindex --inject-style && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css && rm src/index.css",
78
79
  "lint": "eslint",
79
80
  "clean": "rm -rf dist"
80
81
  },
81
82
  "peerDependencies": {
83
+ "next": ">=15.0.0",
82
84
  "react": ">=18.0.0",
83
- "react-dom": ">=18.0.0",
84
- "next": ">=15.0.0"
85
+ "react-dom": ">=18.0.0"
85
86
  },
86
87
  "peerDependenciesMeta": {
87
88
  "next": {
@@ -102,7 +103,8 @@
102
103
  "react-is": "^18.3.1",
103
104
  "react-markdown": "^10.1.0",
104
105
  "recharts": "^3.8.1",
105
- "remark-gfm": "^4.0.1"
106
+ "remark-gfm": "^4.0.1",
107
+ "xlsx": "^0.18.5"
106
108
  },
107
109
  "optionalDependencies": {
108
110
  "@langchain/core": "^1.1.42",
package/src/app/page.tsx CHANGED
@@ -1,28 +1,64 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
4
- import { AmbientBackground } from '@/components/AmbientBackground';
5
- import { Navbar } from '@/components/Navbar';
6
- import { Hero } from '@/components/Hero';
7
- import { Lifecycle } from '@/components/Lifecycle';
8
- import { ArchitectureCardsSection } from '@/components/ArchitectureCardsSection';
9
- import { RagPatternsSection } from '@/components/RagPatternsSection';
10
- import { Documentation } from '@/components/Documentation';
3
+ import React, { useState } from 'react';
11
4
  import { ChatWidget } from '@/components/ChatWidget';
5
+ import { ChatWindow } from '@/components/ChatWindow';
12
6
 
13
7
  export default function HomePage() {
8
+ const [showChatWindow, setShowChatWindow] = useState(true);
9
+ const [showChatWidget, setShowChatWidget] = useState(true);
10
+
14
11
  return (
15
- <main className="min-h-screen bg-slate-50 dark:bg-[#080811] relative transition-colors duration-300">
16
- <AmbientBackground />
17
- <Navbar />
18
- <div className="relative z-10 container mx-auto px-6 py-12">
19
- <Hero />
20
- <Lifecycle />
21
- <RagPatternsSection />
22
- <ArchitectureCardsSection />
23
- <Documentation />
12
+ <main className="min-h-screen bg-slate-50 dark:bg-[#080811] text-slate-900 dark:text-slate-100 p-6 md:p-12 transition-colors duration-300">
13
+ <div className="max-w-4xl mx-auto space-y-8">
14
+ {/* Header / Intro */}
15
+ <div className="border-b border-slate-200 dark:border-slate-800 pb-6">
16
+ <h1 className="text-3xl font-bold tracking-tight">Retrivora SDK Preview</h1>
17
+ <p className="text-sm text-slate-500 dark:text-slate-400 mt-2">
18
+ Test and preview the SDK interactive components: Embedded <code className="text-xs bg-slate-200 dark:bg-slate-800 px-1.5 py-0.5 rounded">ChatWindow</code> and Floating <code className="text-xs bg-slate-200 dark:bg-slate-800 px-1.5 py-0.5 rounded">ChatWidget</code>.
19
+ </p>
20
+ </div>
21
+
22
+ {/* Controls */}
23
+ <div className="flex flex-wrap items-center gap-4 bg-white dark:bg-slate-900 p-4 rounded-xl border border-slate-200 dark:border-slate-800 shadow-sm">
24
+ <label className="flex items-center gap-2 cursor-pointer text-sm font-medium">
25
+ <input
26
+ type="checkbox"
27
+ checked={showChatWindow}
28
+ onChange={(e) => setShowChatWindow(e.target.checked)}
29
+ className="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 text-sm"
30
+ />
31
+ Show Embedded ChatWindow
32
+ </label>
33
+
34
+ <label className="flex items-center gap-2 cursor-pointer text-sm font-medium">
35
+ <input
36
+ type="checkbox"
37
+ checked={showChatWidget}
38
+ onChange={(e) => setShowChatWidget(e.target.checked)}
39
+ className="w-4 h-4 rounded text-blue-600 focus:ring-blue-500 text-sm"
40
+ />
41
+ Enable Floating ChatWidget
42
+ </label>
43
+ </div>
44
+
45
+ {/* Embedded ChatWindow Section */}
46
+ {showChatWindow ? (
47
+ <div className="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-2xl p-4 shadow-lg h-[600px]">
48
+ <ChatWindow
49
+ className="h-full"
50
+ showClose={false}
51
+ />
52
+ </div>
53
+ ) : (
54
+ <div className="border border-dashed border-slate-300 dark:border-slate-700 rounded-2xl p-8 text-center text-slate-400">
55
+ Embedded ChatWindow is disabled. Check the toggle above to enable it.
56
+ </div>
57
+ )}
24
58
  </div>
25
- <ChatWidget position="bottom-right" />
59
+
60
+ {/* Floating ChatWidget */}
61
+ {showChatWidget && <ChatWidget position="bottom-right" />}
26
62
  </main>
27
63
  );
28
64
  }
@@ -250,7 +250,7 @@ export function ChatWindow({
250
250
  return (
251
251
  <div
252
252
  ref={windowRef}
253
- className={`relative flex flex-col border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
253
+ className={`relative flex flex-col h-full flex-1 border border-slate-200 dark:border-white/10 shadow-2xl transition-all duration-300 ${currentRadius} ${
254
254
  isGlass
255
255
  ? 'bg-white/90 dark:bg-[#0f0f1a]/90 backdrop-blur-xl'
256
256
  : 'bg-white dark:bg-[#0f0f1a]'
@@ -346,7 +346,7 @@ export function ChatWindow({
346
346
  <div className="flex-1 overflow-y-auto px-4 py-4 space-y-5 scrollbar-thin scrollbar-thumb-slate-200 dark:scrollbar-thumb-white/10 scrollbar-track-transparent min-h-0 bg-slate-50 dark:bg-transparent">
347
347
  {!mounted || isEmpty ? (
348
348
  /* Welcome state */
349
- <div className="h-full flex flex-col items-center justify-center text-center gap-4 px-6 py-12">
349
+ <div className="h-full flex flex-col items-center justify-center text-center gap-4 px-6 py-4">
350
350
  <div
351
351
  className={`w-16 h-16 flex items-center justify-center shadow-lg ${ui.borderRadius === 'full' ? 'rounded-full' : 'rounded-2xl'}`}
352
352
  style={{ background: `linear-gradient(135deg, ${ui.primaryColor}, ${ui.accentColor})` }}
@@ -13,7 +13,7 @@ interface FileState {
13
13
  }
14
14
 
15
15
  export function DocumentUpload({ namespace, onUploadComplete, className = '' }: DocumentUploadProps) {
16
- const { ui, embedding } = useConfig();
16
+ const { ui, embedding, projectId } = useConfig();
17
17
  const [fileStates, setFileStates] = useState<FileState[]>([]);
18
18
  const [isDragging, setIsDragging] = useState(false);
19
19
  const fileInputRef = useRef<HTMLInputElement>(null);
@@ -81,7 +81,8 @@ export function DocumentUpload({ namespace, onUploadComplete, className = '' }:
81
81
 
82
82
  const formData = new FormData();
83
83
  idleFiles.forEach(s => formData.append('files', s.file));
84
- if (namespace) formData.append('namespace', namespace);
84
+ const targetNs = namespace || projectId;
85
+ if (targetNs) formData.append('namespace', targetNs);
85
86
  formData.append('dimension', dimension.toString());
86
87
 
87
88
  try {
@@ -125,7 +126,7 @@ export function DocumentUpload({ namespace, onUploadComplete, className = '' }:
125
126
  </div>
126
127
  <h3 className="text-slate-900 dark:text-white font-semibold mb-1">Upload Documents</h3>
127
128
  <p className="text-slate-500 dark:text-white/40 text-sm mb-6 max-w-xs">
128
- Drag and drop PDF, DOCX, TXT, or JSON files to train your AI on your own data.
129
+ Drag and drop PDF, DOCX, Excel (.xlsx, .xls), CSV, TXT, or JSON files to train your AI on your own data.
129
130
  </p>
130
131
 
131
132
  <button
@@ -146,7 +147,7 @@ export function DocumentUpload({ namespace, onUploadComplete, className = '' }:
146
147
  multiple
147
148
  onChange={onFileSelect}
148
149
  className="hidden"
149
- accept=".pdf,.docx,.txt,.md,.json,.csv"
150
+ accept=".pdf,.docx,.doc,.xlsx,.xls,.csv,.txt,.md,.json"
150
151
  />
151
152
 
152
153
  <div className="mt-6 flex flex-col items-center gap-2 text-sm">
@@ -402,10 +402,8 @@ export function MessageBubble({
402
402
 
403
403
  const shouldShow =
404
404
  !isNegativeOrFallbackText &&
405
- (ui.type === 'table' ||
406
- !['text', 'table'].includes(ui.type) ||
407
- (ui.type === 'text' && !hasRichUI && allProducts.length === 0 && textContent &&
408
- !processedMarkdown.toLowerCase().includes(textContent.trim().toLowerCase())));
405
+ ui.type !== 'text' &&
406
+ (ui.type === 'table' || !['text', 'table'].includes(ui.type));
409
407
 
410
408
  if (!shouldShow) return null;
411
409
  return (
@@ -76,7 +76,11 @@ export class DatabaseStorage {
76
76
  tier !== 'enterprise' &&
77
77
  tier !== 'growth' &&
78
78
  tier !== 'pro' &&
79
- tier !== 'developer_pro'
79
+ tier !== 'developer_pro' &&
80
+ tier !== 'free_trial' &&
81
+ tier !== 'free_tier' &&
82
+ tier !== 'free' &&
83
+ tier !== 'hobby'
80
84
  ) {
81
85
  throw new Error(
82
86
  `[Retrivora SDK] Chat History and Feedback features are not available on the "${tier}" tier. ` +
@@ -149,7 +149,7 @@ MwIDAQAB
149
149
  `Please upgrade to a Developer Pro or Enterprise plan.`
150
150
  );
151
151
  }
152
- } else if (tier === 'pro' || tier === 'developer_pro' || tier === 'premium' || tier === 'growth') {
152
+ } else if (tier === 'pro' || tier === 'developer_pro' || tier === 'premium' || tier === 'growth' || tier === 'free_trial' || tier === 'free_tier' || tier === 'free') {
153
153
  // Developer Pro supports: PostgreSQL, Pinecone, Qdrant, MongoDB, Milvus, ChromaDB
154
154
  const allowedPro = [
155
155
  'postgresql', 'pgvector', 'supabase',
@@ -24,6 +24,24 @@ import {
24
24
  import { UITransformer } from '../utils/UITransformer';
25
25
  import { SchemaMapper, SchemaMap } from '../utils/SchemaMapper';
26
26
 
27
+ // ─── Namespace Formatter ───────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Clean & format namespace string to satisfy vector DB identifier rules
31
+ * and enforce the 'retrivora-' namespace prefix convention for Free Tier isolation.
32
+ */
33
+ export function formatNamespace(raw?: string): string {
34
+ if (!raw || !raw.trim()) return 'retrivora-default';
35
+ const trimmed = raw.trim();
36
+ if (trimmed.startsWith('retrivora-')) return trimmed;
37
+ const sanitized = trimmed
38
+ .toLowerCase()
39
+ .replace(/[^a-z0-9_-]/g, '_')
40
+ .replace(/_+/g, '_')
41
+ .replace(/^_+|_+$/g, '');
42
+ return `retrivora-${sanitized}`;
43
+ }
44
+
27
45
  // ─── LRU Embedding Cache ───────────────────────────────────────────────────────
28
46
 
29
47
  /**
@@ -310,7 +328,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
310
328
  namespace?: string
311
329
  ): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
312
330
  await this.initialize();
313
- const ns = namespace ?? this.config.projectId;
331
+ const ns = formatNamespace(namespace ?? this.config.projectId);
314
332
  const results = [];
315
333
 
316
334
  for (const doc of documents) {
@@ -500,7 +518,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
500
518
  */
501
519
  async *askStreamInternal(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
502
520
  await this.initialize();
503
- const ns = namespace ?? this.config.projectId;
521
+ const ns = formatNamespace(namespace ?? this.config.projectId);
504
522
  const topK = this.config.rag?.topK ?? 5;
505
523
  // Default score threshold of 0.3 filters random matches while keeping
506
524
  // genuinely relevant chunks. Users can override via rag.scoreThreshold.
@@ -696,7 +714,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
696
714
  const isSimulatedThinking = !isNativeThinking && (
697
715
  this.config.llm.options?.thinking === true ||
698
716
  this.config.llm.options?.thinking === 'enabled' ||
699
- (isReasoningModel && this.config.llm.options?.thinking !== false)
717
+ this.config.llm.options?.thinking !== false
700
718
  );
701
719
 
702
720
  let finalRestrictionSuffix = restrictionSuffix;
@@ -1082,7 +1100,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
1082
1100
  }
1083
1101
 
1084
1102
  async retrieve(query: string, options: { namespace?: string; topK?: number; filter?: Record<string, unknown> }): Promise<RetrievalResult> {
1085
- const ns = options.namespace ?? this.config.projectId;
1103
+ const ns = formatNamespace(options.namespace ?? this.config.projectId);
1086
1104
  const topK = options.topK ?? 5;
1087
1105
 
1088
1106
  const cacheKey = `${ns}::${query}`;
@@ -1175,7 +1193,7 @@ Optimized Search Query:`;
1175
1193
  if (!query || query.trim().length < 3) return [];
1176
1194
 
1177
1195
  await this.initialize();
1178
- const ns = namespace ?? this.config.projectId;
1196
+ const ns = formatNamespace(namespace ?? this.config.projectId);
1179
1197
 
1180
1198
  try {
1181
1199
  const { sources } = await this.retrieve(query, { namespace: ns, topK: 5 });
@@ -576,6 +576,12 @@ export function createUploadHandler(
576
576
  const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
577
577
 
578
578
  for (const file of files) {
579
+ const isExcel =
580
+ file.name.toLowerCase().endsWith('.xlsx') ||
581
+ file.name.toLowerCase().endsWith('.xls') ||
582
+ file.type.includes('spreadsheet') ||
583
+ file.type.includes('excel');
584
+
579
585
  if (file.name.toLowerCase().endsWith('.csv') || file.type === 'text/csv') {
580
586
  const text = await file.text();
581
587
  const Papa = await import('papaparse');
@@ -613,7 +619,7 @@ export function createUploadHandler(
613
619
  metadata: {
614
620
  fileName: file.name,
615
621
  fileSize: file.size,
616
- fileType: file.type,
622
+ fileType: file.type || 'text/csv',
617
623
  uploadedAt: new Date().toISOString(),
618
624
  ...(dimension ? { dimension } : {}),
619
625
  csvHeaders: csvCols,
@@ -622,6 +628,47 @@ export function createUploadHandler(
622
628
  });
623
629
  }
624
630
  }
631
+ } else if (isExcel) {
632
+ const xlsx = await import('xlsx');
633
+ const buffer = Buffer.isBuffer(file) ? file : Buffer.from(await file.arrayBuffer());
634
+ const workbook = xlsx.read(buffer, { type: 'buffer' });
635
+
636
+ for (const sheetName of workbook.SheetNames) {
637
+ const sheet = workbook.Sheets[sheetName];
638
+ const jsonRows = xlsx.utils.sheet_to_json<Record<string, any>>(sheet, { defval: '' });
639
+
640
+ if (jsonRows && jsonRows.length > 0) {
641
+ let i = 0;
642
+ const headers = Object.keys(jsonRows[0] || {});
643
+
644
+ for (const rowData of jsonRows) {
645
+ i++;
646
+ const contentParts: string[] = [];
647
+ for (const [key, val] of Object.entries(rowData)) {
648
+ if (key && val !== undefined && val !== null && String(val).trim()) {
649
+ contentParts.push(`${key}: ${val}`);
650
+ }
651
+ }
652
+
653
+ if (contentParts.length > 0) {
654
+ documents.push({
655
+ docId: `${file.name}-${sheetName}-row-${i}`,
656
+ content: contentParts.join(', '),
657
+ metadata: {
658
+ fileName: file.name,
659
+ sheetName,
660
+ fileSize: file.size,
661
+ fileType: file.type || 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
662
+ uploadedAt: new Date().toISOString(),
663
+ ...(dimension ? { dimension } : {}),
664
+ headers,
665
+ ...rowData
666
+ }
667
+ });
668
+ }
669
+ }
670
+ }
671
+ }
625
672
  } else {
626
673
  const content = await DocumentParser.parse(file, file.name, file.type);
627
674
  documents.push({
package/src/index.ts CHANGED
@@ -25,6 +25,24 @@ export { useRagChat } from './hooks/useRagChat';
25
25
  // ── Utils ─────────────────────────────────────────────────────
26
26
  export { addSynonyms } from './utils/synonyms';
27
27
 
28
+ // ── Visualization Decision Engine & Rendering ──────────────────
29
+ export {
30
+ VisualizationDecisionEngine,
31
+ decideVisualization,
32
+ RendererRegistry,
33
+ IntentClassifier,
34
+ RuleEngine,
35
+ } from './rendering';
36
+ export type {
37
+ RenderDecision,
38
+ RenderType,
39
+ ChartType,
40
+ IntentCategory,
41
+ DecisionContext,
42
+ IRenderRule,
43
+ IRendererStrategy,
44
+ } from './rendering';
45
+
28
46
  // ── Types (Interfaces/Types only, no Node.js deps) ─────────────
29
47
  export type {
30
48
  RagConfig,
@@ -53,10 +71,10 @@ export {
53
71
  ConfigurationException,
54
72
  AuthenticationException,
55
73
  } from './exceptions';
56
- export type {
57
- VectorMatch,
58
- UpsertDocument,
59
- IngestDocument,
74
+ export type {
75
+ VectorMatch,
76
+ UpsertDocument,
77
+ IngestDocument,
60
78
  ChatResponse,
61
79
  ChatMessage,
62
80
  ChatOptions,
@@ -0,0 +1,167 @@
1
+ import { DecisionContext, IntentCategory } from './types';
2
+
3
+ export interface DataSignals {
4
+ rowCount: number;
5
+ hasNumericFields: boolean;
6
+ hasDateFields: boolean;
7
+ hasCategoricalFields: boolean;
8
+ isProductLike: boolean;
9
+ numericFieldCount: number;
10
+ categoricalFieldCount: number;
11
+ }
12
+
13
+ export class IntentClassifier {
14
+ /**
15
+ * Fast, zero-latency classifier that maps a user query and optional data context
16
+ * into a high-level IntentCategory.
17
+ */
18
+ static classify(context: DecisionContext): { intent: IntentCategory; signals: DataSignals } {
19
+ const q = (context.userQuery || '').toLowerCase().trim();
20
+ const signals = this.extractDataSignals(context);
21
+
22
+ // Rule A: Specific Information Lookup
23
+ if (this.isInformationLookup(q)) {
24
+ return { intent: 'information_lookup', signals };
25
+ }
26
+
27
+ // Rule B: Comparison Query
28
+ if (this.isComparisonQuery(q)) {
29
+ return { intent: 'comparison', signals };
30
+ }
31
+
32
+ // Rule C: Product Search / Recommendation
33
+ if (this.isProductQuery(q) || (signals.isProductLike && this.isRecommendationQuery(q))) {
34
+ return { intent: 'product_search', signals };
35
+ }
36
+
37
+ // Rule D: Trend Analysis
38
+ if (this.isTrendQuery(q) || (signals.hasDateFields && signals.hasNumericFields && /\b(growth|trend|over time|monthly|yearly|weekly)\b/.test(q))) {
39
+ return { intent: 'trend_analysis', signals };
40
+ }
41
+
42
+ // Rule E: Ranking Query
43
+ if (this.isRankingQuery(q)) {
44
+ return { intent: 'ranking', signals };
45
+ }
46
+
47
+ // Rule F: Analytics Query
48
+ if (this.isAnalyticsQuery(q) || (signals.hasNumericFields && signals.hasCategoricalFields && /\b(sales|revenue|performance|count|total|average|breakdown|share|percentage|correlation)\b/.test(q))) {
49
+ return { intent: 'analytics', signals };
50
+ }
51
+
52
+ // Rule G: Recommendation fallback
53
+ if (this.isRecommendationQuery(q)) {
54
+ return { intent: 'recommendation', signals };
55
+ }
56
+
57
+ // Fallback: Default to information lookup for generic queries
58
+ return { intent: 'information_lookup', signals };
59
+ }
60
+
61
+ /**
62
+ * Extract key data structure signals from retrieved documents.
63
+ */
64
+ static extractDataSignals(context: DecisionContext): DataSignals {
65
+ const docs = context.retrievedDocuments || [];
66
+ const rowCount = docs.length;
67
+
68
+ let hasNumericFields = false;
69
+ let hasDateFields = false;
70
+ let hasCategoricalFields = false;
71
+ let isProductLike = false;
72
+ let numericFieldCount = 0;
73
+ let categoricalFieldCount = 0;
74
+
75
+ if (docs.length > 0) {
76
+ const keys = new Set<string>();
77
+ let numericKeys = new Set<string>();
78
+ let dateKeys = new Set<string>();
79
+ let catKeys = new Set<string>();
80
+
81
+ let productKeyMatches = 0;
82
+
83
+ docs.forEach(doc => {
84
+ const meta = doc.metadata || {};
85
+ Object.entries(meta).forEach(([key, val]) => {
86
+ keys.add(key);
87
+ const kLower = key.toLowerCase();
88
+ if (['price', 'image', 'brand', 'in_stock', 'rating', 'sku', 'product'].some(p => kLower.includes(p))) {
89
+ productKeyMatches++;
90
+ }
91
+
92
+ if (typeof val === 'number') {
93
+ numericKeys.add(key);
94
+ } else if (typeof val === 'string') {
95
+ if (/^\d{4}-\d{2}-\d{2}/.test(val) || !isNaN(Date.parse(val)) && val.length > 5) {
96
+ dateKeys.add(key);
97
+ } else if (val.length < 50 && !val.includes('\n')) {
98
+ catKeys.add(key);
99
+ }
100
+ }
101
+ });
102
+ });
103
+
104
+ hasNumericFields = numericKeys.size > 0;
105
+ hasDateFields = dateKeys.size > 0;
106
+ hasCategoricalFields = catKeys.size > 0;
107
+ numericFieldCount = numericKeys.size;
108
+ categoricalFieldCount = catKeys.size;
109
+
110
+ if (productKeyMatches >= 2 || docs.some(d => d.content.toLowerCase().includes('price') || d.content.toLowerCase().includes('brand'))) {
111
+ isProductLike = true;
112
+ }
113
+ }
114
+
115
+ return {
116
+ rowCount,
117
+ hasNumericFields,
118
+ hasDateFields,
119
+ hasCategoricalFields,
120
+ isProductLike,
121
+ numericFieldCount,
122
+ categoricalFieldCount,
123
+ };
124
+ }
125
+
126
+ // ─── Intent Heuristic Checkers ─────────────────────────────────────────────
127
+
128
+ private static isInformationLookup(query: string): boolean {
129
+ // Exact single-fact pattern matches e.g. "What is X?", "Explain Y", "Price of Z", "Can Auvira be assigned..."
130
+ if (/^(what|who|explain|define|where|when|why|how|how does|can|could|does|do|is|are|has|have|will|should|would|meaning of)\s+/i.test(query)) {
131
+ // Exclude comparison or analytical phrasing inside "what is the trend..."
132
+ if (!/\b(compare|versus|vs\.?|trend|monthly|revenue|top selling|best)\b/i.test(query)) {
133
+ return true;
134
+ }
135
+ }
136
+ // "price of X" / "cost of Y"
137
+ if (/^(what is the )?(price|cost|fee|rate) of\b/i.test(query)) {
138
+ return true;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ private static isComparisonQuery(query: string): boolean {
144
+ return /\b(compare|comparison|vs\.?|versus|difference between|differ|contrast|against)\b/i.test(query);
145
+ }
146
+
147
+ private static isProductQuery(query: string): boolean {
148
+ return /\b(best|recommend|top|buy|shopping|under \$|cheap|shoes|laptops|chairs|headphones|phone|sneakers|apparel|products)\b/i.test(query)
149
+ && !/\b(compare|chart|sales|revenue|trend)\b/i.test(query);
150
+ }
151
+
152
+ private static isRecommendationQuery(query: string): boolean {
153
+ return /\b(recommend|suggest|what should i (buy|get|choose)|best options|top picks)\b/i.test(query);
154
+ }
155
+
156
+ private static isTrendQuery(query: string): boolean {
157
+ return /\b(trend|trends|growth|over time|historical|timeline|monthly|yearly|weekly|daily|revenue growth|sales growth)\b/i.test(query);
158
+ }
159
+
160
+ private static isRankingQuery(query: string): boolean {
161
+ return /\b(highest|lowest|top\s*\d+|bottom\s*\d+|rank|ranking|top performing|best performing)\b/i.test(query);
162
+ }
163
+
164
+ private static isAnalyticsQuery(query: string): boolean {
165
+ return /\b(sales|revenue|analytics|insights|distribution|percentage|share|breakdown|breakup|scatter|correlation|metrics|performance|by region|by category)\b/i.test(query);
166
+ }
167
+ }