@retrivora-ai/rag-engine 1.8.2 → 1.8.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/dist/handlers/index.js +142 -16
- package/dist/handlers/index.mjs +5848 -17
- package/dist/index.css +3162 -0
- package/dist/index.d.mts +9 -15
- package/dist/index.d.ts +9 -15
- package/dist/index.js +1355 -919
- package/dist/index.mjs +1437 -896
- package/dist/server.d.mts +6 -0
- package/dist/server.d.ts +6 -0
- package/dist/server.js +171 -17
- package/dist/server.mjs +5923 -68
- package/package.json +8 -6
- package/src/app/globals.css +35 -11
- package/src/components/ChatWidget.tsx +0 -1
- package/src/components/MarkdownComponents.tsx +3 -0
- package/src/components/MessageBubble.tsx +3 -2
- package/src/components/ObservabilityPanel.tsx +1 -1
- package/src/components/UIDispatcher.tsx +0 -3
- package/src/config/ConfigBuilder.ts +38 -1
- package/src/core/LangChainAgent.ts +1 -4
- package/src/core/Pipeline.ts +31 -18
- package/src/core/QueryProcessor.ts +65 -0
- package/src/rag/Reranker.ts +99 -6
- package/src/tailwind.css +2 -0
- package/src/utils/ProductExtractor.ts +3 -3
- package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
- package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
- package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
- package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +0 -8
- package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
- package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
- package/dist/RedisProvider-SR65SCKV.mjs +0 -8
- package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
- package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
- package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
- package/dist/chunk-5AJ4XHLW.mjs +0 -201
- package/dist/chunk-5YGUXK7Z.mjs +0 -80
- package/dist/chunk-CFVEZTBJ.mjs +0 -102
- package/dist/chunk-ICKRMZQK.mjs +0 -76
- package/dist/chunk-IMP6FUCY.mjs +0 -30
- package/dist/chunk-LR3VMDVK.mjs +0 -157
- package/dist/chunk-LZVVLSDN.mjs +0 -4077
- package/dist/chunk-M6JSPGAR.mjs +0 -117
- package/dist/chunk-OZFBG4BA.mjs +0 -291
- package/dist/chunk-PSFPZXHX.mjs +0 -245
- package/dist/chunk-U55XRW3U.mjs +0 -96
- package/dist/chunk-VUQJVIJT.mjs +0 -148
- package/dist/chunk-X4TOT24V.mjs +0 -89
- package/dist/chunk-YLTMFW4M.mjs +0 -49
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.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": "MIT",
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
"require": "./dist/index.js",
|
|
37
37
|
"import": "./dist/index.mjs"
|
|
38
38
|
},
|
|
39
|
+
"./style.css": "./dist/index.css",
|
|
39
40
|
"./handlers": {
|
|
40
41
|
"types": "./dist/handlers/index.d.ts",
|
|
41
42
|
"require": "./dist/handlers/index.js",
|
|
@@ -73,25 +74,24 @@
|
|
|
73
74
|
"scripts": {
|
|
74
75
|
"dev": "next dev",
|
|
75
76
|
"build": "next build",
|
|
76
|
-
"build:pkg": "tsup src/index.ts src/handlers/index.ts src/server.ts --format cjs,esm --dts --clean --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",
|
|
77
|
+
"build:pkg": "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 && npx @tailwindcss/cli -i src/tailwind.css -o dist/index.css",
|
|
77
78
|
"start": "next start",
|
|
78
79
|
"lint": "eslint",
|
|
79
80
|
"prepublishOnly": "npm run build:pkg"
|
|
80
81
|
},
|
|
81
82
|
"peerDependencies": {
|
|
82
|
-
"next": ">=14.0.0",
|
|
83
83
|
"react": ">=18.0.0",
|
|
84
84
|
"react-dom": ">=18.0.0"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@anthropic-ai/sdk": "^0.
|
|
87
|
+
"@anthropic-ai/sdk": "^0.95.1",
|
|
88
88
|
"@google/genai": "^0.8.0",
|
|
89
89
|
"@google/generative-ai": "^0.24.1",
|
|
90
90
|
"@pinecone-database/pinecone": "^7.2.0",
|
|
91
91
|
"@types/papaparse": "^5.5.2",
|
|
92
92
|
"axios": "^1.15.0",
|
|
93
93
|
"lucide-react": "^1.8.0",
|
|
94
|
-
"next": "
|
|
94
|
+
"next": "^16.2.6",
|
|
95
95
|
"next-themes": "^0.4.6",
|
|
96
96
|
"openai": "^6.34.0",
|
|
97
97
|
"papaparse": "^5.5.3",
|
|
@@ -106,14 +106,16 @@
|
|
|
106
106
|
"@langchain/core": "^1.1.42",
|
|
107
107
|
"@langchain/openai": "^1.4.5",
|
|
108
108
|
"langchain": "^1.3.5",
|
|
109
|
-
"llamaindex": "^0.
|
|
109
|
+
"llamaindex": "^0.11.9",
|
|
110
110
|
"mammoth": "^1.8.0",
|
|
111
111
|
"mongodb": "^7.1.1",
|
|
112
112
|
"pdf-parse": "^1.1.1",
|
|
113
113
|
"pg": "^8.20.0"
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
|
+
"@tailwindcss/cli": "^4",
|
|
116
117
|
"@tailwindcss/postcss": "^4",
|
|
118
|
+
"@types/estree": "^1.0.9",
|
|
117
119
|
"@types/node": "^20",
|
|
118
120
|
"@types/pdf-parse": "^1.1.5",
|
|
119
121
|
"@types/pg": "^8.20.0",
|
package/src/app/globals.css
CHANGED
|
@@ -7,15 +7,20 @@
|
|
|
7
7
|
/* ─── Design Tokens ─────────────────────────────────────────── */
|
|
8
8
|
:root {
|
|
9
9
|
--font-inter: 'Inter', sans-serif;
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
/* Light Mode (Default) */
|
|
12
|
-
--bg-base: #f8fafc;
|
|
12
|
+
--bg-base: #f8fafc;
|
|
13
|
+
/* slate-50 */
|
|
13
14
|
--bg-surface: #ffffff;
|
|
14
|
-
--bg-elevated: #f1f5f9;
|
|
15
|
+
--bg-elevated: #f1f5f9;
|
|
16
|
+
/* slate-100 */
|
|
15
17
|
--border: rgba(0, 0, 0, 0.08);
|
|
16
|
-
--text-primary: #0f172a;
|
|
17
|
-
|
|
18
|
-
--text-
|
|
18
|
+
--text-primary: #0f172a;
|
|
19
|
+
/* slate-900 */
|
|
20
|
+
--text-secondary: #475569;
|
|
21
|
+
/* slate-600 */
|
|
22
|
+
--text-muted: #94a3b8;
|
|
23
|
+
/* slate-400 */
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
.dark {
|
|
@@ -54,13 +59,16 @@ body {
|
|
|
54
59
|
width: 4px;
|
|
55
60
|
height: 4px;
|
|
56
61
|
}
|
|
62
|
+
|
|
57
63
|
::-webkit-scrollbar-track {
|
|
58
64
|
background: transparent;
|
|
59
65
|
}
|
|
66
|
+
|
|
60
67
|
::-webkit-scrollbar-thumb {
|
|
61
68
|
background: rgba(255, 255, 255, 0.12);
|
|
62
69
|
border-radius: 999px;
|
|
63
70
|
}
|
|
71
|
+
|
|
64
72
|
::-webkit-scrollbar-thumb:hover {
|
|
65
73
|
background: rgba(255, 255, 255, 0.2);
|
|
66
74
|
}
|
|
@@ -93,13 +101,21 @@ body {
|
|
|
93
101
|
margin: 0.5em 0;
|
|
94
102
|
}
|
|
95
103
|
|
|
96
|
-
.prose ul,
|
|
104
|
+
.prose ul,
|
|
105
|
+
.prose ol {
|
|
97
106
|
padding-left: 1.5em;
|
|
98
107
|
margin: 0.5em 0;
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
.prose li {
|
|
102
111
|
margin: 0.25em 0;
|
|
112
|
+
list-style-type: none;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
.prose li::before {
|
|
116
|
+
content: "\27A4";
|
|
117
|
+
/* Unicode for a right-pointing arrow */
|
|
118
|
+
padding-right: 8px;
|
|
103
119
|
}
|
|
104
120
|
|
|
105
121
|
.prose table {
|
|
@@ -109,7 +125,8 @@ body {
|
|
|
109
125
|
font-size: 0.8rem;
|
|
110
126
|
}
|
|
111
127
|
|
|
112
|
-
.prose th,
|
|
128
|
+
.prose th,
|
|
129
|
+
.prose td {
|
|
113
130
|
border: 1px solid var(--border);
|
|
114
131
|
padding: 0.5rem;
|
|
115
132
|
text-align: left;
|
|
@@ -175,6 +192,7 @@ body {
|
|
|
175
192
|
opacity: 0;
|
|
176
193
|
transform: translateY(8px);
|
|
177
194
|
}
|
|
195
|
+
|
|
178
196
|
to {
|
|
179
197
|
opacity: 1;
|
|
180
198
|
transform: translateY(0);
|
|
@@ -186,6 +204,7 @@ body {
|
|
|
186
204
|
opacity: 0;
|
|
187
205
|
transform: translateY(16px) scale(0.98);
|
|
188
206
|
}
|
|
207
|
+
|
|
189
208
|
to {
|
|
190
209
|
opacity: 1;
|
|
191
210
|
transform: translateY(0) scale(1);
|
|
@@ -193,8 +212,13 @@ body {
|
|
|
193
212
|
}
|
|
194
213
|
|
|
195
214
|
@keyframes shimmer {
|
|
196
|
-
0% {
|
|
197
|
-
|
|
215
|
+
0% {
|
|
216
|
+
background-position: -200% 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
100% {
|
|
220
|
+
background-position: 200% 0;
|
|
221
|
+
}
|
|
198
222
|
}
|
|
199
223
|
|
|
200
224
|
.animate-fade-in-up {
|
|
@@ -218,4 +242,4 @@ body {
|
|
|
218
242
|
-webkit-background-clip: text;
|
|
219
243
|
-webkit-text-fill-color: transparent;
|
|
220
244
|
background-clip: text;
|
|
221
|
-
}
|
|
245
|
+
}
|
|
@@ -20,6 +20,9 @@ export function createMarkdownComponents({
|
|
|
20
20
|
viewportSize,
|
|
21
21
|
}: MarkdownComponentsProps): Components {
|
|
22
22
|
return {
|
|
23
|
+
p: ({ ...props }: React.HTMLAttributes<HTMLParagraphElement>) => (
|
|
24
|
+
<p className="whitespace-pre-line" {...props} />
|
|
25
|
+
),
|
|
23
26
|
table: ({ ...props }: React.HTMLAttributes<HTMLTableElement>) => {
|
|
24
27
|
// Fix: intercept table rendering during streaming instead of using sentinel
|
|
25
28
|
if (isStreaming) {
|
|
@@ -78,9 +78,10 @@ export function MessageBubble({
|
|
|
78
78
|
message.content,
|
|
79
79
|
structuredContent.text,
|
|
80
80
|
payloadCandidates,
|
|
81
|
-
productsFromSources
|
|
81
|
+
productsFromSources,
|
|
82
|
+
isStreaming
|
|
82
83
|
);
|
|
83
|
-
}, [message.content, isUser, structuredContent, productsFromSources]);
|
|
84
|
+
}, [message.content, isUser, structuredContent, productsFromSources, isStreaming]);
|
|
84
85
|
|
|
85
86
|
const allProducts = React.useMemo<Product[]>(() => {
|
|
86
87
|
return deduplicateProducts(productsFromSources, productsFromContent, message.content);
|
|
@@ -323,7 +323,7 @@ export function ObservabilityPanel({ trace, primaryColor = '#6366f1' }: Observab
|
|
|
323
323
|
const [activeTab, setActiveTab] = React.useState<TabId>('retrieval');
|
|
324
324
|
|
|
325
325
|
return (
|
|
326
|
-
<div className="mt-2 w-full rounded-xl border border-slate-200 dark:border-white/8 bg-white dark:bg-slate-900/60 shadow-sm overflow-hidden text-[12px]">
|
|
326
|
+
<div className="mt-2 w-[500px] max-w-full rounded-xl border border-slate-200 dark:border-white/8 bg-white dark:bg-slate-900/60 shadow-sm overflow-hidden text-[12px]">
|
|
327
327
|
{/* Header */}
|
|
328
328
|
<div
|
|
329
329
|
className="flex items-center gap-2 px-3 py-2 border-b border-slate-100 dark:border-white/8"
|
|
@@ -264,9 +264,6 @@ export function UIDispatcher({ rawContent, primaryColor, accentColor, isStreamin
|
|
|
264
264
|
const hasInsights = Array.isArray(config.insights) && config.insights.length > 0;
|
|
265
265
|
const hasDescription = typeof config.description === 'string' && config.description.trim().length > 0;
|
|
266
266
|
|
|
267
|
-
console.log("🚀 ~ UIDispatcher ~ description:", config.description);
|
|
268
|
-
console.log("🚀 ~ UIDispatcher ~ hasDescription:", hasDescription);
|
|
269
|
-
|
|
270
267
|
switch (config.view) {
|
|
271
268
|
case 'chart':
|
|
272
269
|
return (
|
|
@@ -32,6 +32,8 @@ import {
|
|
|
32
32
|
VectorDBProvider,
|
|
33
33
|
LLMProvider,
|
|
34
34
|
EmbeddingProvider,
|
|
35
|
+
GraphDBConfig,
|
|
36
|
+
GraphDBProvider,
|
|
35
37
|
} from './RagConfig';
|
|
36
38
|
|
|
37
39
|
export class ConfigBuilder {
|
|
@@ -41,6 +43,7 @@ export class ConfigBuilder {
|
|
|
41
43
|
private _embedding?: EmbeddingConfig;
|
|
42
44
|
private _ui?: UIConfig;
|
|
43
45
|
private _rag?: RAGConfig;
|
|
46
|
+
private _graphDb?: GraphDBConfig;
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
49
|
* Set the project/application ID for namespacing
|
|
@@ -118,6 +121,24 @@ export class ConfigBuilder {
|
|
|
118
121
|
return this;
|
|
119
122
|
}
|
|
120
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Configure the graph database provider
|
|
126
|
+
*/
|
|
127
|
+
graphDb(
|
|
128
|
+
provider: string | 'auto',
|
|
129
|
+
options?: Record<string, unknown>
|
|
130
|
+
): this {
|
|
131
|
+
if (provider === 'auto') {
|
|
132
|
+
this._graphDb = this._autoDetectGraphDb();
|
|
133
|
+
} else {
|
|
134
|
+
this._graphDb = {
|
|
135
|
+
provider: provider as GraphDBProvider,
|
|
136
|
+
options: options ?? {},
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
121
142
|
/**
|
|
122
143
|
* Set RAG-specific pipeline parameters
|
|
123
144
|
*/
|
|
@@ -152,13 +173,19 @@ export class ConfigBuilder {
|
|
|
152
173
|
);
|
|
153
174
|
}
|
|
154
175
|
|
|
176
|
+
const ragConfig = this._rag ?? { chunkSize: 1000, chunkOverlap: 200, topK: 5 };
|
|
177
|
+
if (process.env.RAG_USE_GRAPH_RETRIEVAL === 'true') {
|
|
178
|
+
ragConfig.useGraphRetrieval = true;
|
|
179
|
+
}
|
|
180
|
+
|
|
155
181
|
return {
|
|
156
182
|
projectId: this._projectId!,
|
|
157
183
|
vectorDb: this._vectorDb!,
|
|
158
184
|
llm: this._llm!,
|
|
159
185
|
embedding: this._embedding!,
|
|
160
186
|
ui: this._rag ? this._ui : undefined,
|
|
161
|
-
rag:
|
|
187
|
+
rag: ragConfig,
|
|
188
|
+
graphDb: this._graphDb,
|
|
162
189
|
...(this._ui ? { ui: this._ui } : {}),
|
|
163
190
|
};
|
|
164
191
|
}
|
|
@@ -212,6 +239,16 @@ export class ConfigBuilder {
|
|
|
212
239
|
}
|
|
213
240
|
return { provider: 'openai', model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY };
|
|
214
241
|
}
|
|
242
|
+
|
|
243
|
+
private _autoDetectGraphDb(): GraphDBConfig | undefined {
|
|
244
|
+
if (process.env.GRAPH_DB_PROVIDER) {
|
|
245
|
+
return {
|
|
246
|
+
provider: process.env.GRAPH_DB_PROVIDER as GraphDBProvider,
|
|
247
|
+
options: {},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
return undefined;
|
|
251
|
+
}
|
|
215
252
|
}
|
|
216
253
|
|
|
217
254
|
/**
|
|
@@ -25,7 +25,7 @@ export class LangChainAgent {
|
|
|
25
25
|
*/
|
|
26
26
|
async initialize(chatModel: unknown) {
|
|
27
27
|
try {
|
|
28
|
-
const [{ DynamicTool },
|
|
28
|
+
const [{ DynamicTool }, , { createAgent }] = await Promise.all([
|
|
29
29
|
import('@langchain/core/tools'),
|
|
30
30
|
import('@langchain/core/messages'),
|
|
31
31
|
import('langchain'),
|
|
@@ -80,9 +80,6 @@ export class LangChainAgent {
|
|
|
80
80
|
tools: [searchTool],
|
|
81
81
|
systemPrompt: finalSystemPrompt,
|
|
82
82
|
});
|
|
83
|
-
|
|
84
|
-
// Suppress unused import warning — HumanMessage is used in run()
|
|
85
|
-
void HumanMessage;
|
|
86
83
|
} catch (error) {
|
|
87
84
|
const isMissing =
|
|
88
85
|
error instanceof Error && error.message.includes('Cannot find module');
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -76,14 +76,14 @@ function estimateTokens(text: string): number {
|
|
|
76
76
|
* Used for estimating cost when providers don't return billing data.
|
|
77
77
|
*/
|
|
78
78
|
const MODEL_COST_PER_1K: Record<string, { input: number; output: number }> = {
|
|
79
|
-
'gpt-4o':
|
|
80
|
-
'gpt-4o-mini':
|
|
81
|
-
'gpt-4-turbo':
|
|
82
|
-
'gpt-3.5-turbo':
|
|
83
|
-
'claude-3-5-sonnet':
|
|
84
|
-
'claude-3-haiku':
|
|
85
|
-
'gemini-1.5-flash':
|
|
86
|
-
'gemini-1.5-pro':
|
|
79
|
+
'gpt-4o': { input: 0.0025, output: 0.010 },
|
|
80
|
+
'gpt-4o-mini': { input: 0.00015, output: 0.0006 },
|
|
81
|
+
'gpt-4-turbo': { input: 0.01, output: 0.03 },
|
|
82
|
+
'gpt-3.5-turbo': { input: 0.0005, output: 0.0015 },
|
|
83
|
+
'claude-3-5-sonnet': { input: 0.003, output: 0.015 },
|
|
84
|
+
'claude-3-haiku': { input: 0.00025, output: 0.00125 },
|
|
85
|
+
'gemini-1.5-flash': { input: 0.000075, output: 0.0003 },
|
|
86
|
+
'gemini-1.5-pro': { input: 0.00125, output: 0.005 },
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
function estimateCostUsd(
|
|
@@ -434,6 +434,8 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
434
434
|
const rerankStart = performance.now();
|
|
435
435
|
let sources = rawSources.filter((m) => m.score >= scoreThreshold);
|
|
436
436
|
if (this.config.rag?.useReranking) {
|
|
437
|
+
// We do not pass llmProvider here to opt into the fast, deterministic keyword-based re-ranking in production.
|
|
438
|
+
// Pass this.llmProvider as the 4th argument if you want high-accuracy LLM-based re-ranking.
|
|
437
439
|
sources = await this.reranker.rerank(sources, question, topK);
|
|
438
440
|
} else {
|
|
439
441
|
sources = sources.slice(0, topK);
|
|
@@ -446,6 +448,7 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
446
448
|
: 'No relevant context found.';
|
|
447
449
|
|
|
448
450
|
if (graphData && graphData.nodes.length > 0) {
|
|
451
|
+
console.log(`[Graph Retrieval] Found ${graphData.nodes.length} relevant entities.`);
|
|
449
452
|
const graphContext = graphData.nodes.map(n =>
|
|
450
453
|
`Entity: ${n.label} (${n.id})${n.properties ? ' - ' + JSON.stringify(n.properties) : ''}`
|
|
451
454
|
).join('\n');
|
|
@@ -513,7 +516,7 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
513
516
|
|
|
514
517
|
// 8. Automated UI transformation
|
|
515
518
|
const trainedSchema = await trainingPromise;
|
|
516
|
-
const uiTransformation = await this.generateUiTransformation(question, sources,
|
|
519
|
+
const uiTransformation = await this.generateUiTransformation(question, sources, trainedSchema);
|
|
517
520
|
|
|
518
521
|
// 9. Build observability trace
|
|
519
522
|
const trace: ObservabilityTrace = {
|
|
@@ -557,7 +560,6 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
557
560
|
private async generateUiTransformation(
|
|
558
561
|
question: string,
|
|
559
562
|
sources: VectorMatch[],
|
|
560
|
-
_context: string,
|
|
561
563
|
trainedSchema?: SchemaMap
|
|
562
564
|
): Promise<unknown> {
|
|
563
565
|
if (!sources || sources.length === 0) {
|
|
@@ -579,19 +581,30 @@ You are a helpful product assistant. Use the provided context to answer question
|
|
|
579
581
|
const cacheKey = `${ns}::${query}`;
|
|
580
582
|
let queryVector = this.embeddingCache.get(cacheKey);
|
|
581
583
|
|
|
584
|
+
// Determine strategy
|
|
585
|
+
const strategy = await QueryProcessor.determineRetrievalStrategy(query, this.llmProvider);
|
|
586
|
+
console.debug(`[Pipeline] Determined retrieval strategy: ${strategy}`);
|
|
587
|
+
|
|
582
588
|
const [retrievedVector, graphData] = await Promise.all([
|
|
583
|
-
|
|
584
|
-
|
|
589
|
+
// Only embed if we need vector search (strategy is 'vector' or 'both')
|
|
590
|
+
(strategy === 'vector' || strategy === 'both')
|
|
591
|
+
? (queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: 'query' }))
|
|
592
|
+
: Promise.resolve([]),
|
|
593
|
+
|
|
594
|
+
// Only query graph if we need graph search (strategy is 'graph' or 'both')
|
|
595
|
+
(strategy === 'graph' || strategy === 'both') && this.graphDB && this.config.rag?.useGraphRetrieval
|
|
585
596
|
? this.graphDB.query(query)
|
|
586
597
|
: Promise.resolve(undefined)
|
|
587
598
|
]);
|
|
588
599
|
|
|
589
|
-
if (!queryVector) {
|
|
600
|
+
if ((strategy === 'vector' || strategy === 'both') && !queryVector && retrievedVector && retrievedVector.length > 0) {
|
|
590
601
|
this.embeddingCache.set(cacheKey, retrievedVector);
|
|
591
602
|
queryVector = retrievedVector;
|
|
592
603
|
}
|
|
593
604
|
|
|
594
|
-
const sources =
|
|
605
|
+
const sources = (strategy === 'vector' || strategy === 'both') && queryVector && queryVector.length > 0
|
|
606
|
+
? await this.vectorDB.query(queryVector, topK, ns, options.filter)
|
|
607
|
+
: [];
|
|
595
608
|
|
|
596
609
|
return { sources, graphData };
|
|
597
610
|
}
|
|
@@ -627,14 +640,14 @@ Optimized Search Query:`;
|
|
|
627
640
|
const ns = namespace ?? this.config.projectId;
|
|
628
641
|
|
|
629
642
|
try {
|
|
630
|
-
const { sources } = await this.retrieve(query, { namespace: ns, topK:
|
|
643
|
+
const { sources } = await this.retrieve(query, { namespace: ns, topK: 5 });
|
|
631
644
|
if (sources.length === 0) return [];
|
|
632
645
|
|
|
633
646
|
const context = sources.map((s) => s.content).join('\n\n---\n\n');
|
|
634
|
-
const prompt = `Based on the following snippets from a document, what are
|
|
647
|
+
const prompt = `Based on the following snippets from a document, what are at least 5 short, relevant questions a user might ask?
|
|
635
648
|
Focus on questions that can be answered by the context.
|
|
636
649
|
Keep each question under 10 words and make them very specific to the content.
|
|
637
|
-
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3"].
|
|
650
|
+
Return ONLY a JSON array of strings like ["Question 1", "Question 2", "Question 3", "Question 4", "Question 5"].
|
|
638
651
|
|
|
639
652
|
Context:
|
|
640
653
|
${context}
|
|
@@ -653,7 +666,7 @@ Suggestions:`;
|
|
|
653
666
|
if (match) {
|
|
654
667
|
const suggestions = JSON.parse(match[0]);
|
|
655
668
|
if (Array.isArray(suggestions)) {
|
|
656
|
-
return suggestions.map(s => String(s)).slice(0,
|
|
669
|
+
return suggestions.map(s => String(s)).slice(0, 5);
|
|
657
670
|
}
|
|
658
671
|
}
|
|
659
672
|
} catch (error) {
|
|
@@ -198,4 +198,69 @@ export class QueryProcessor {
|
|
|
198
198
|
|
|
199
199
|
return filter;
|
|
200
200
|
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Determines the retrieval strategy based on the query content.
|
|
204
|
+
*/
|
|
205
|
+
static async determineRetrievalStrategy(
|
|
206
|
+
query: string,
|
|
207
|
+
llm?: import('../llm/ILLMProvider').ILLMProvider,
|
|
208
|
+
customGraphKeywords?: string[],
|
|
209
|
+
customVectorKeywords?: string[]
|
|
210
|
+
): Promise<'vector' | 'graph' | 'both'> {
|
|
211
|
+
|
|
212
|
+
if (llm) {
|
|
213
|
+
try {
|
|
214
|
+
const prompt = `You are a routing expert in a RAG system.
|
|
215
|
+
Given the user query, determine which retrieval mechanism is needed:
|
|
216
|
+
- 'vector': For semantic search, finding specific documents or content based on similarity.
|
|
217
|
+
- 'graph': For finding relationships between entities, multi-hop questions, or network structures.
|
|
218
|
+
- 'both': If the query requires both specific content and relationship/structural analysis.
|
|
219
|
+
|
|
220
|
+
Query: "${query}"
|
|
221
|
+
|
|
222
|
+
Return ONLY 'vector', 'graph', or 'both'. No explanation.`;
|
|
223
|
+
|
|
224
|
+
const response = await llm.chat(
|
|
225
|
+
[{ role: 'user', content: prompt }],
|
|
226
|
+
'',
|
|
227
|
+
{ temperature: 0, maxTokens: 10 }
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
const cleanResponse = response.trim().toLowerCase();
|
|
231
|
+
if (cleanResponse.includes('vector')) return 'vector';
|
|
232
|
+
if (cleanResponse.includes('graph')) return 'graph';
|
|
233
|
+
if (cleanResponse.includes('both')) return 'both';
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.warn('[QueryProcessor] LLM strategy classification failed, falling back to keywords:', error);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Fallback to keywords
|
|
240
|
+
const normalized = query.toLowerCase();
|
|
241
|
+
|
|
242
|
+
// Graph keywords
|
|
243
|
+
const graphKeywords = customGraphKeywords || [
|
|
244
|
+
'relationship', 'connect', 'link', 'network', 'friend', 'colleague',
|
|
245
|
+
'manager', 'hierarchy', 'multi-hop', 'shortest path', 'between',
|
|
246
|
+
'related to', 'associated with', 'belongs to', 'part of'
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const hasGraphKeyword = graphKeywords.some(kw => normalized.includes(kw));
|
|
250
|
+
|
|
251
|
+
// Vector keywords (usually generic search or content specific)
|
|
252
|
+
const vectorKeywords = customVectorKeywords || [
|
|
253
|
+
'find', 'search', 'tell me about', 'what is', 'how to', 'documents about'
|
|
254
|
+
];
|
|
255
|
+
|
|
256
|
+
const hasVectorKeyword = vectorKeywords.some(kw => normalized.includes(kw));
|
|
257
|
+
|
|
258
|
+
if (hasGraphKeyword && !hasVectorKeyword) {
|
|
259
|
+
return 'graph';
|
|
260
|
+
} else if (hasGraphKeyword && hasVectorKeyword) {
|
|
261
|
+
return 'both';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return 'vector';
|
|
265
|
+
}
|
|
201
266
|
}
|
package/src/rag/Reranker.ts
CHANGED
|
@@ -1,18 +1,111 @@
|
|
|
1
1
|
import { VectorMatch } from '../types';
|
|
2
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
2
3
|
|
|
3
4
|
export class Reranker {
|
|
4
5
|
/**
|
|
5
|
-
* Re-ranks matches based on a secondary relevance score.
|
|
6
|
-
* In a production environment, this would call a Cross-Encoder model.
|
|
7
|
-
* Here we implement a placeholder that filters by score and limits count.
|
|
6
|
+
* Re-ranks matches based on a secondary relevance score using an LLM.
|
|
8
7
|
*/
|
|
9
8
|
async rerank(
|
|
10
9
|
matches: VectorMatch[],
|
|
11
10
|
query: string,
|
|
12
|
-
limit: number = 5
|
|
11
|
+
limit: number = 5,
|
|
12
|
+
llm?: ILLMProvider
|
|
13
13
|
): Promise<VectorMatch[]> {
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
if (!llm || matches.length <= 1) {
|
|
15
|
+
// Fallback to deterministic keyword-based re-ranking for production speed
|
|
16
|
+
const keywords = query.toLowerCase()
|
|
17
|
+
.replace(/[^\w\s]/g, '')
|
|
18
|
+
.split(/\s+/)
|
|
19
|
+
.filter(w => w.length > 2); // Filter out short words/stop words
|
|
20
|
+
|
|
21
|
+
if (keywords.length === 0) {
|
|
22
|
+
return matches
|
|
23
|
+
.sort((a, b) => b.score - a.score)
|
|
24
|
+
.slice(0, limit);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const scoredMatches = matches.map(match => {
|
|
28
|
+
const contentLower = match.content.toLowerCase();
|
|
29
|
+
let keywordScore = 0;
|
|
30
|
+
|
|
31
|
+
keywords.forEach(keyword => {
|
|
32
|
+
if (contentLower.includes(keyword)) {
|
|
33
|
+
keywordScore += 1;
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Normalize keyword score (fraction of keywords found)
|
|
38
|
+
const normalizedKeywordScore = keywordScore / keywords.length;
|
|
39
|
+
|
|
40
|
+
// Combine scores: Vector score (usually 0-1) + Keyword score weighted
|
|
41
|
+
// We give a significant weight to keyword matches to surface specific results
|
|
42
|
+
const combinedScore = match.score + (normalizedKeywordScore * 0.5);
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...match,
|
|
46
|
+
score: combinedScore
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
return scoredMatches
|
|
51
|
+
.sort((a, b) => b.score - a.score)
|
|
52
|
+
.slice(0, limit);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Take top 10 matches to avoid large context and high latency
|
|
57
|
+
const topN = matches.slice(0, 10);
|
|
58
|
+
|
|
59
|
+
const prompt = `You are a relevance ranking expert.
|
|
60
|
+
Given the following user query and a list of retrieved document chunks, rank the chunks by relevance to the query.
|
|
61
|
+
Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant.
|
|
62
|
+
Use the indices provided in brackets like [0], [1], etc.
|
|
63
|
+
Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.
|
|
64
|
+
|
|
65
|
+
Query: "${query}"
|
|
66
|
+
|
|
67
|
+
Documents:
|
|
68
|
+
${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}`;
|
|
69
|
+
|
|
70
|
+
const response = await llm.chat(
|
|
71
|
+
[{ role: 'user', content: prompt }],
|
|
72
|
+
'',
|
|
73
|
+
{ temperature: 0, maxTokens: 50 }
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const cleanedResponse = response.trim().replace(/[\[\]\s]/g, '');
|
|
77
|
+
const rankedIndices = cleanedResponse.split(',').map(Number).filter(n => !isNaN(n));
|
|
78
|
+
|
|
79
|
+
if (rankedIndices.length > 0) {
|
|
80
|
+
const rankedMatches: VectorMatch[] = [];
|
|
81
|
+
const usedIndices = new Set<number>();
|
|
82
|
+
|
|
83
|
+
// Add matches in the order specified by the LLM
|
|
84
|
+
for (const index of rankedIndices) {
|
|
85
|
+
if (index >= 0 && index < topN.length && !usedIndices.has(index)) {
|
|
86
|
+
rankedMatches.push(topN[index]);
|
|
87
|
+
usedIndices.add(index);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Add any remaining matches that the LLM might have missed
|
|
92
|
+
for (let i = 0; i < topN.length; i++) {
|
|
93
|
+
if (!usedIndices.has(i)) {
|
|
94
|
+
rankedMatches.push(topN[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Add back the rest of the matches that weren't in the top N
|
|
99
|
+
const rest = matches.slice(10);
|
|
100
|
+
rankedMatches.push(...rest);
|
|
101
|
+
|
|
102
|
+
return rankedMatches.slice(0, limit);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
console.warn('[Reranker] LLM re-ranking failed, falling back to score-based sorting:', error);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Fallback if LLM fails or returns invalid response
|
|
16
109
|
return matches
|
|
17
110
|
.sort((a, b) => b.score - a.score)
|
|
18
111
|
.slice(0, limit);
|
package/src/tailwind.css
ADDED
|
@@ -211,7 +211,6 @@ export function extractProductsFromSources(sources: VectorMatch[] | undefined, i
|
|
|
211
211
|
const brand = resolveMetadataValue(m, 'brand') as string;
|
|
212
212
|
const price = resolveMetadataValue(m, 'price') as string | number;
|
|
213
213
|
const description = resolveMetadataValue(m, 'description') as string;
|
|
214
|
-
console.log("🚀 ~ extractProductsFromSources ~ description:", s)
|
|
215
214
|
|
|
216
215
|
return {
|
|
217
216
|
id: s.id,
|
|
@@ -229,7 +228,8 @@ export function extractProductsFromContent(
|
|
|
229
228
|
content: string,
|
|
230
229
|
payloadText: string,
|
|
231
230
|
payloadCandidates: string[],
|
|
232
|
-
productsFromSources: Product[]
|
|
231
|
+
productsFromSources: Product[],
|
|
232
|
+
isStreaming: boolean = false
|
|
233
233
|
): { productsFromContent: Product[]; cleanContent: string } {
|
|
234
234
|
const jsonRegex = /```(?:json|ui)?\s*([\s\S]*?)\s*```/g;
|
|
235
235
|
const products: Product[] = [];
|
|
@@ -284,7 +284,7 @@ export function extractProductsFromContent(
|
|
|
284
284
|
const bulletRegex = /^[ \t]*(?:[•*-]|\d+\.)\s+(?:\*\*)?([^\n\-\$–—\(]+?)(?:\*\*)?(?:\s*\(?Price\s*[:\-–—]?\s*)?(?:\s*[:\-–—]\s*|\s+)\$?([\d,.]+)(?:\s*USD)?/gim;
|
|
285
285
|
const matches = Array.from(cleanContent.matchAll(bulletRegex));
|
|
286
286
|
|
|
287
|
-
if (matches.length >= 2) {
|
|
287
|
+
if (!isStreaming && matches.length >= 2) {
|
|
288
288
|
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
289
289
|
|
|
290
290
|
for (const match of matches) {
|