@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.
- package/FREE_TIER_ARCHITECTURE.md +232 -0
- package/README.md +9 -0
- package/dist/{ILLMProvider-DMxLyTdq.d.mts → ILLMProvider-0rRBYbVW.d.mts} +133 -1
- package/dist/{ILLMProvider-DMxLyTdq.d.ts → ILLMProvider-0rRBYbVW.d.ts} +133 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +681 -17
- package/dist/handlers/index.mjs +681 -17
- package/dist/index-B1wGUlSL.d.mts +532 -0
- package/dist/{index-tzwc7UhY.d.ts → index-BIHHp_f6.d.ts} +1 -1
- package/dist/{index-DHsFLJXQ.d.mts → index-BvODr57d.d.mts} +1 -1
- package/dist/index-DcklhThn.d.ts +532 -0
- package/dist/index.css +51 -3
- package/dist/index.d.mts +6 -105
- package/dist/index.d.ts +6 -105
- package/dist/index.js +1918 -8
- package/dist/index.mjs +1913 -8
- package/dist/server.d.mts +51 -7
- package/dist/server.d.ts +51 -7
- package/dist/server.js +723 -17
- package/dist/server.mjs +706 -17
- package/package.json +9 -7
- package/src/app/page.tsx +54 -18
- package/src/components/ChatWindow.tsx +2 -2
- package/src/components/DocumentUpload.tsx +5 -4
- package/src/components/MessageBubble.tsx +2 -4
- package/src/core/DatabaseStorage.ts +5 -1
- package/src/core/LicenseVerifier.ts +1 -1
- package/src/core/Pipeline.ts +23 -5
- package/src/handlers/index.ts +48 -1
- package/src/index.ts +22 -4
- package/src/rendering/IntentClassifier.ts +167 -0
- package/src/rendering/RendererRegistry.ts +164 -0
- package/src/rendering/RuleEngine.ts +67 -0
- package/src/rendering/VisualizationDecisionEngine.ts +164 -0
- package/src/rendering/__tests__/VisualizationDecisionEngine.test.ts +237 -0
- package/src/rendering/index.ts +12 -0
- package/src/rendering/rules/Rule1SpecificInfoRule.ts +28 -0
- package/src/rendering/rules/Rule2ComparisonRule.ts +24 -0
- package/src/rendering/rules/Rule3ProductDiscoveryRule.ts +28 -0
- package/src/rendering/rules/Rule4AnalyticalRule.ts +38 -0
- package/src/rendering/rules/Rule5MixedResponseRule.ts +29 -0
- package/src/rendering/rules/Rule6SmallResultSetRule.ts +24 -0
- package/src/rendering/rules/Rule7LargeTableRule.ts +35 -0
- package/src/rendering/types.ts +96 -0
- package/src/server.ts +3 -2
- package/src/types/index.ts +48 -0
- package/src/utils/DocumentParser.ts +64 -28
- package/src/utils/UITransformer.ts +15 -4
- package/dist/index-CfkqZd2Y.d.ts +0 -197
- 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.
|
|
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]
|
|
16
|
-
<
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
406
|
-
|
|
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',
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -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
|
-
|
|
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 });
|
package/src/handlers/index.ts
CHANGED
|
@@ -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
|
+
}
|