@retrivora-ai/rag-engine 1.8.2 → 1.8.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 (47) hide show
  1. package/dist/handlers/index.js +142 -16
  2. package/dist/handlers/index.mjs +5848 -17
  3. package/dist/index.d.mts +9 -15
  4. package/dist/index.d.ts +9 -15
  5. package/dist/index.js +1355 -919
  6. package/dist/index.mjs +1437 -896
  7. package/dist/server.d.mts +6 -0
  8. package/dist/server.d.ts +6 -0
  9. package/dist/server.js +171 -17
  10. package/dist/server.mjs +5923 -68
  11. package/package.json +6 -6
  12. package/src/app/globals.css +35 -11
  13. package/src/components/ChatWidget.tsx +0 -1
  14. package/src/components/MarkdownComponents.tsx +3 -0
  15. package/src/components/MessageBubble.tsx +3 -2
  16. package/src/components/ObservabilityPanel.tsx +1 -1
  17. package/src/components/UIDispatcher.tsx +0 -3
  18. package/src/config/ConfigBuilder.ts +38 -1
  19. package/src/core/LangChainAgent.ts +1 -4
  20. package/src/core/Pipeline.ts +31 -18
  21. package/src/core/QueryProcessor.ts +65 -0
  22. package/src/rag/Reranker.ts +99 -6
  23. package/src/utils/ProductExtractor.ts +3 -3
  24. package/dist/ChromaDBProvider-MIDOR4FW.mjs +0 -8
  25. package/dist/MilvusProvider-U7SKC27V.mjs +0 -8
  26. package/dist/MongoDBProvider-YNKC7EJ6.mjs +0 -8
  27. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +0 -8
  28. package/dist/PineconeProvider-QZNRKTN2.mjs +0 -8
  29. package/dist/QdrantProvider-RLJTNGPY.mjs +0 -8
  30. package/dist/RedisProvider-SR65SCKV.mjs +0 -8
  31. package/dist/SimpleGraphProvider-SLOXO4M7.mjs +0 -62
  32. package/dist/UniversalVectorProvider-IN67OS56.mjs +0 -9
  33. package/dist/WeaviateProvider-5FWDFITI.mjs +0 -8
  34. package/dist/chunk-5AJ4XHLW.mjs +0 -201
  35. package/dist/chunk-5YGUXK7Z.mjs +0 -80
  36. package/dist/chunk-CFVEZTBJ.mjs +0 -102
  37. package/dist/chunk-ICKRMZQK.mjs +0 -76
  38. package/dist/chunk-IMP6FUCY.mjs +0 -30
  39. package/dist/chunk-LR3VMDVK.mjs +0 -157
  40. package/dist/chunk-LZVVLSDN.mjs +0 -4077
  41. package/dist/chunk-M6JSPGAR.mjs +0 -117
  42. package/dist/chunk-OZFBG4BA.mjs +0 -291
  43. package/dist/chunk-PSFPZXHX.mjs +0 -245
  44. package/dist/chunk-U55XRW3U.mjs +0 -96
  45. package/dist/chunk-VUQJVIJT.mjs +0 -148
  46. package/dist/chunk-X4TOT24V.mjs +0 -89
  47. 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.2",
3
+ "version": "1.8.3",
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",
@@ -73,25 +73,24 @@
73
73
  "scripts": {
74
74
  "dev": "next dev",
75
75
  "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",
76
+ "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",
77
77
  "start": "next start",
78
78
  "lint": "eslint",
79
79
  "prepublishOnly": "npm run build:pkg"
80
80
  },
81
81
  "peerDependencies": {
82
- "next": ">=14.0.0",
83
82
  "react": ">=18.0.0",
84
83
  "react-dom": ">=18.0.0"
85
84
  },
86
85
  "dependencies": {
87
- "@anthropic-ai/sdk": "^0.90.0",
86
+ "@anthropic-ai/sdk": "^0.95.1",
88
87
  "@google/genai": "^0.8.0",
89
88
  "@google/generative-ai": "^0.24.1",
90
89
  "@pinecone-database/pinecone": "^7.2.0",
91
90
  "@types/papaparse": "^5.5.2",
92
91
  "axios": "^1.15.0",
93
92
  "lucide-react": "^1.8.0",
94
- "next": ">=14.0.0",
93
+ "next": "^16.2.6",
95
94
  "next-themes": "^0.4.6",
96
95
  "openai": "^6.34.0",
97
96
  "papaparse": "^5.5.3",
@@ -106,7 +105,7 @@
106
105
  "@langchain/core": "^1.1.42",
107
106
  "@langchain/openai": "^1.4.5",
108
107
  "langchain": "^1.3.5",
109
- "llamaindex": "^0.12.1",
108
+ "llamaindex": "^0.11.9",
110
109
  "mammoth": "^1.8.0",
111
110
  "mongodb": "^7.1.1",
112
111
  "pdf-parse": "^1.1.1",
@@ -114,6 +113,7 @@
114
113
  },
115
114
  "devDependencies": {
116
115
  "@tailwindcss/postcss": "^4",
116
+ "@types/estree": "^1.0.9",
117
117
  "@types/node": "^20",
118
118
  "@types/pdf-parse": "^1.1.5",
119
119
  "@types/pg": "^8.20.0",
@@ -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; /* slate-50 */
12
+ --bg-base: #f8fafc;
13
+ /* slate-50 */
13
14
  --bg-surface: #ffffff;
14
- --bg-elevated: #f1f5f9; /* slate-100 */
15
+ --bg-elevated: #f1f5f9;
16
+ /* slate-100 */
15
17
  --border: rgba(0, 0, 0, 0.08);
16
- --text-primary: #0f172a; /* slate-900 */
17
- --text-secondary: #475569; /* slate-600 */
18
- --text-muted: #94a3b8; /* slate-400 */
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, .prose ol {
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, .prose td {
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% { background-position: -200% 0; }
197
- 100% { background-position: 200% 0; }
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
+ }
@@ -30,7 +30,6 @@ export function ChatWidget({ position = 'bottom-right', onAddToCart }: ChatWidge
30
30
  : 'bottom-20 right-6';
31
31
 
32
32
  const handleOpen = () => {
33
- console.log('ChatWidget: Opening...');
34
33
  setIsOpen(true);
35
34
  setHasUnread(false);
36
35
  };
@@ -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: this._rag ?? { chunkSize: 1000, chunkOverlap: 200, topK: 5 },
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 }, { HumanMessage }, { createAgent }] = await Promise.all([
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');
@@ -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': { 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 },
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, context, trainedSchema);
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
- queryVector ? Promise.resolve(queryVector) : this.embeddingProvider.embed(query, { taskType: 'query' }),
584
- this.graphDB && this.config.rag?.useGraphRetrieval
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 = await this.vectorDB.query(queryVector, topK, ns, options.filter);
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: 3 });
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 3 short, relevant questions a user might ask?
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, 3);
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
  }
@@ -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
- // Current implementation: sort by original score and apply a strict threshold
15
- // and semantic match filtering (placeholder)
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);
@@ -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) {
@@ -1,8 +0,0 @@
1
- import {
2
- ChromaDBProvider
3
- } from "./chunk-CFVEZTBJ.mjs";
4
- import "./chunk-IMP6FUCY.mjs";
5
- import "./chunk-X4TOT24V.mjs";
6
- export {
7
- ChromaDBProvider
8
- };
@@ -1,8 +0,0 @@
1
- import {
2
- MilvusProvider
3
- } from "./chunk-U55XRW3U.mjs";
4
- import "./chunk-IMP6FUCY.mjs";
5
- import "./chunk-X4TOT24V.mjs";
6
- export {
7
- MilvusProvider
8
- };