@retrivora-ai/rag-engine 1.9.2 → 1.9.6
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/README.md +27 -0
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-DNhyOYoK.d.mts} +41 -1
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-DNhyOYoK.d.ts} +41 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +563 -205
- package/dist/handlers/index.mjs +563 -205
- package/dist/index-C9v7-tWd.d.mts +183 -0
- package/dist/{index-B70ZLkfG.d.mts → index-CHL1jdYm.d.mts} +1 -1
- package/dist/index-CjQdL0cX.d.ts +183 -0
- package/dist/{index-DVu-mkAM.d.ts → index-Hgbwl9X4.d.ts} +1 -1
- package/dist/index.css +40 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +326 -158
- package/dist/index.mjs +312 -151
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +623 -205
- package/dist/server.mjs +615 -205
- package/package.json +1 -1
- package/src/app/api/chat/route.ts +2 -2
- package/src/app/api/health/route.ts +3 -2
- package/src/app/api/ingest/route.ts +3 -2
- package/src/app/api/suggestions/route.ts +3 -2
- package/src/app/api/upload/route.ts +3 -2
- package/src/app/constants.tsx +85 -30
- package/src/app/layout.tsx +18 -7
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/config/RagConfig.ts +47 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +56 -4
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +11 -2
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +51 -0
- package/src/exceptions/index.ts +59 -0
- package/src/handlers/index.ts +2 -0
- package/src/hooks/useRagChat.ts +26 -4
- package/src/index.ts +25 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +4 -1
- package/src/llm/providers/AnthropicProvider.ts +70 -20
- package/src/llm/providers/GeminiProvider.ts +14 -15
- package/src/llm/providers/OllamaProvider.ts +13 -16
- package/src/llm/providers/OpenAIProvider.ts +9 -14
- package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
- package/src/llm/utils.ts +46 -0
- package/src/providers/vectordb/MongoDBProvider.ts +19 -7
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
- package/src/rag/EntityExtractor.ts +2 -2
- package/src/rag/Reranker.ts +9 -16
- package/src/server.ts +27 -1
- package/src/types/chat.ts +7 -0
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@retrivora-ai/rag-engine",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.6",
|
|
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",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { createIngestHandler } from '@/handlers';
|
|
24
|
-
import {
|
|
24
|
+
import { plugin } from '@/lib/plugin';
|
|
25
|
+
|
|
26
|
+
export const POST = createIngestHandler(plugin);
|
|
25
27
|
|
|
26
|
-
export const POST = createIngestHandler(getRagConfig());
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSuggestionsHandler } from '@/handlers';
|
|
2
|
-
import {
|
|
2
|
+
import { plugin } from '@/lib/plugin';
|
|
3
|
+
|
|
4
|
+
export const POST = createSuggestionsHandler(plugin);
|
|
3
5
|
|
|
4
|
-
export const POST = createSuggestionsHandler(getRagConfig());
|
package/src/app/constants.tsx
CHANGED
|
@@ -89,49 +89,85 @@ export const PIPELINE_STEPS: PipelineStep[] = [
|
|
|
89
89
|
export const SNIPPETS: Snippet[] = [
|
|
90
90
|
{
|
|
91
91
|
id: 'pipeline',
|
|
92
|
-
title: '
|
|
92
|
+
title: 'Retrivora SDK',
|
|
93
93
|
language: 'typescript',
|
|
94
|
-
description: '
|
|
95
|
-
code: `import {
|
|
94
|
+
description: 'Create one server-side SDK instance from config; clients never know which providers are used.',
|
|
95
|
+
code: `import { Retrivora } from '@retrivora-ai/rag-engine/server';
|
|
96
96
|
|
|
97
|
-
const
|
|
98
|
-
|
|
97
|
+
const ai = new Retrivora({
|
|
98
|
+
projectId: 'support-app',
|
|
99
|
+
llm: {
|
|
100
|
+
provider: 'openai',
|
|
101
|
+
model: 'gpt-5',
|
|
102
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
103
|
+
},
|
|
104
|
+
embedding: {
|
|
105
|
+
provider: 'openai',
|
|
106
|
+
model: 'text-embedding-3-small',
|
|
107
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
108
|
+
},
|
|
109
|
+
vectorDatabase: {
|
|
110
|
+
provider: 'pinecone',
|
|
111
|
+
indexName: 'support-docs',
|
|
112
|
+
options: { apiKey: process.env.PINECONE_API_KEY },
|
|
113
|
+
},
|
|
114
|
+
retrieval: { strategy: 'hybrid', topK: 5 },
|
|
115
|
+
workflow: { type: 'agentic-rag' },
|
|
116
|
+
});
|
|
99
117
|
|
|
100
|
-
|
|
101
|
-
await pipeline.ingest([{
|
|
118
|
+
await ai.ingest([{
|
|
102
119
|
docId: 'doc-1',
|
|
103
120
|
content: 'Retrivora AI simplifies RAG...',
|
|
104
121
|
metadata: { source: 'docs' }
|
|
105
122
|
}]);
|
|
106
123
|
|
|
107
|
-
|
|
108
|
-
const { reply, sources } = await pipeline.ask('How does Retrivora work?');`,
|
|
124
|
+
const { reply, sources } = await ai.ask('How does Retrivora work?');`,
|
|
109
125
|
},
|
|
110
126
|
{
|
|
111
127
|
id: 'handlers',
|
|
112
|
-
title: '
|
|
128
|
+
title: 'Next.js Routes',
|
|
113
129
|
language: 'typescript',
|
|
114
|
-
description: '
|
|
115
|
-
code:
|
|
130
|
+
description: 'Keep secrets and provider code in App Router handlers, then expose simple API endpoints to React.',
|
|
131
|
+
code: `// app/api/chat/route.ts
|
|
132
|
+
import { Retrivora, createStreamHandler } from '@retrivora-ai/rag-engine/server';
|
|
116
133
|
|
|
117
|
-
const
|
|
134
|
+
const ai = new Retrivora({
|
|
135
|
+
projectId: 'support-app',
|
|
136
|
+
llm: { provider: 'openai', model: 'gpt-5', apiKey: process.env.OPENAI_API_KEY },
|
|
137
|
+
embedding: { provider: 'openai', model: 'text-embedding-3-small', apiKey: process.env.OPENAI_API_KEY },
|
|
138
|
+
vectorDatabase: {
|
|
139
|
+
provider: 'pinecone',
|
|
140
|
+
indexName: 'support-docs',
|
|
141
|
+
options: { apiKey: process.env.PINECONE_API_KEY },
|
|
142
|
+
},
|
|
143
|
+
});
|
|
118
144
|
|
|
119
|
-
|
|
120
|
-
export const POST = createChatStreamHandler(config);
|
|
145
|
+
export const POST = createStreamHandler(ai.config);
|
|
121
146
|
|
|
122
|
-
//
|
|
123
|
-
export const POST = createUploadHandler(config);`,
|
|
147
|
+
// app/api/upload/route.ts
|
|
148
|
+
// export const POST = createUploadHandler(ai.config);`,
|
|
124
149
|
},
|
|
125
150
|
{
|
|
126
151
|
id: 'ui',
|
|
127
|
-
title: 'React
|
|
152
|
+
title: 'React Client UI',
|
|
128
153
|
language: 'typescript',
|
|
129
|
-
description: '
|
|
130
|
-
code: `
|
|
154
|
+
description: 'Import browser-safe components in Client Components; API keys stay on the server.',
|
|
155
|
+
code: `'use client';
|
|
156
|
+
|
|
157
|
+
import { ConfigProvider, ChatWidget } from '@retrivora-ai/rag-engine';
|
|
131
158
|
|
|
132
159
|
export default function Layout({ children }) {
|
|
133
160
|
return (
|
|
134
|
-
<ConfigProvider
|
|
161
|
+
<ConfigProvider
|
|
162
|
+
config={{
|
|
163
|
+
projectId: 'support-app',
|
|
164
|
+
ui: {
|
|
165
|
+
title: 'AI Assistant',
|
|
166
|
+
showSources: true,
|
|
167
|
+
allowUpload: true,
|
|
168
|
+
},
|
|
169
|
+
}}
|
|
170
|
+
>
|
|
135
171
|
{children}
|
|
136
172
|
<ChatWidget position="bottom-right" />
|
|
137
173
|
</ConfigProvider>
|
|
@@ -140,16 +176,35 @@ export default function Layout({ children }) {
|
|
|
140
176
|
},
|
|
141
177
|
{
|
|
142
178
|
id: 'config',
|
|
143
|
-
title: '
|
|
179
|
+
title: 'Headless Hook',
|
|
144
180
|
language: 'typescript',
|
|
145
|
-
description: '
|
|
146
|
-
code: `
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
.
|
|
181
|
+
description: 'Build your own chat surface with the same server-backed route.',
|
|
182
|
+
code: `'use client';
|
|
183
|
+
|
|
184
|
+
import React from 'react';
|
|
185
|
+
import { useRagChat } from '@retrivora-ai/rag-engine';
|
|
186
|
+
|
|
187
|
+
export function AskBox() {
|
|
188
|
+
const [input, setInput] = React.useState('');
|
|
189
|
+
const { messages, send, isLoading } = useRagChat('support-app', {
|
|
190
|
+
apiUrl: '/api/chat',
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<form onSubmit={(event) => {
|
|
195
|
+
event.preventDefault();
|
|
196
|
+
send(input);
|
|
197
|
+
setInput('');
|
|
198
|
+
}}>
|
|
199
|
+
<input value={input} onChange={(event) => setInput(event.target.value)} />
|
|
200
|
+
<button disabled={isLoading}>Ask</button>
|
|
201
|
+
|
|
202
|
+
{messages.map((message) => (
|
|
203
|
+
<p key={message.id}>{message.content}</p>
|
|
204
|
+
))}
|
|
205
|
+
</form>
|
|
206
|
+
);
|
|
207
|
+
}`,
|
|
153
208
|
},
|
|
154
209
|
{
|
|
155
210
|
id: 'env',
|
package/src/app/layout.tsx
CHANGED
|
@@ -7,23 +7,33 @@ import { getRagConfig } from '@/config/serverConfig';
|
|
|
7
7
|
|
|
8
8
|
const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });
|
|
9
9
|
|
|
10
|
+
// Build server-side config once — same singleton source as route handlers.
|
|
11
|
+
const ragConfig = getRagConfig();
|
|
12
|
+
|
|
10
13
|
export const metadata: Metadata = {
|
|
11
|
-
title: 'Retrivora AI',
|
|
14
|
+
title: ragConfig.ui?.title ?? 'Retrivora AI',
|
|
12
15
|
description:
|
|
16
|
+
ragConfig.ui?.subtitle ??
|
|
13
17
|
'A configurable Retrieval-Augmented Generation chatbot — generic across any vector database and LLM provider.',
|
|
14
18
|
};
|
|
15
19
|
|
|
16
20
|
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
17
|
-
const clientConfig = getRagConfig();
|
|
18
|
-
|
|
19
21
|
return (
|
|
20
22
|
<html lang="en" className={inter.variable} suppressHydrationWarning>
|
|
21
|
-
<body className="bg-slate-50
|
|
22
|
-
<ThemeProvider attribute="class" defaultTheme="
|
|
23
|
+
<body className="bg-slate-50 text-slate-900 antialiased transition-colors duration-300" suppressHydrationWarning>
|
|
24
|
+
<ThemeProvider attribute="class" defaultTheme="light" disableTransitionOnChange>
|
|
23
25
|
<ConfigProvider
|
|
24
26
|
config={{
|
|
25
|
-
projectId:
|
|
26
|
-
ui:
|
|
27
|
+
projectId: ragConfig.projectId,
|
|
28
|
+
ui: ragConfig.ui,
|
|
29
|
+
// Pass embedding model/dimensions so client-side components
|
|
30
|
+
// (e.g. DocumentUpload) know the vector dimensions in use.
|
|
31
|
+
embedding: ragConfig.embedding
|
|
32
|
+
? {
|
|
33
|
+
model: ragConfig.embedding.model,
|
|
34
|
+
dimensions: ragConfig.embedding.dimensions,
|
|
35
|
+
}
|
|
36
|
+
: undefined,
|
|
27
37
|
}}
|
|
28
38
|
>
|
|
29
39
|
{children}
|
|
@@ -33,3 +43,4 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
|
|
33
43
|
</html>
|
|
34
44
|
);
|
|
35
45
|
}
|
|
46
|
+
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from '../utils/ProductExtractor';
|
|
22
22
|
import { createMarkdownComponents } from './MarkdownComponents';
|
|
23
23
|
import { UIDispatcher } from './UIDispatcher';
|
|
24
|
+
import { ThinkingBlock } from './ThinkingBlock';
|
|
24
25
|
|
|
25
26
|
function normalizePlusSeparatedLists(raw: string): string {
|
|
26
27
|
return raw
|
|
@@ -196,6 +197,15 @@ export function MessageBubble({
|
|
|
196
197
|
</div>
|
|
197
198
|
|
|
198
199
|
<div className={`flex flex-col gap-1 min-w-0 ${isCompact ? 'max-w-[94%]' : isMedium ? 'max-w-[92%]' : 'max-w-[90%]'} ${isUser ? 'items-end' : 'items-start'}`}>
|
|
200
|
+
{/* Thinking Steps */}
|
|
201
|
+
{!isUser && message.thinking && (
|
|
202
|
+
<ThinkingBlock
|
|
203
|
+
thinking={message.thinking}
|
|
204
|
+
thinkingMs={message.thinkingMs}
|
|
205
|
+
isStreaming={isStreaming && !message.content}
|
|
206
|
+
/>
|
|
207
|
+
)}
|
|
208
|
+
|
|
199
209
|
{/* Bubble */}
|
|
200
210
|
<div
|
|
201
211
|
className={`relative group ${isCompact ? 'px-3 py-2.5 text-[13px]' : 'px-4 py-3 text-sm'} rounded-2xl leading-relaxed shadow-sm dark:shadow-lg ${isUser
|
|
@@ -260,9 +270,35 @@ export function MessageBubble({
|
|
|
260
270
|
)}
|
|
261
271
|
</div>
|
|
262
272
|
|
|
263
|
-
{/* Auto-generated visualization
|
|
273
|
+
{/* Auto-generated visualization — show eagerly during streaming if sources are available */}
|
|
264
274
|
{(() => {
|
|
265
|
-
if (isUser || structuredContent.payload
|
|
275
|
+
if (isUser || structuredContent.payload) return null;
|
|
276
|
+
|
|
277
|
+
// During streaming: if we have sources but no uiTransformation yet,
|
|
278
|
+
// eagerly render a skeleton carousel for product queries so the user
|
|
279
|
+
// sees something immediately instead of waiting for the metadata frame.
|
|
280
|
+
if (isStreaming && !message.uiTransformation && sources && sources.length > 0) {
|
|
281
|
+
const hasProductSource = sources.some(s =>
|
|
282
|
+
s.metadata && Object.keys(s.metadata).some(k =>
|
|
283
|
+
['name','title','price','brand','image','product','sku','category'].includes(k.toLowerCase())
|
|
284
|
+
)
|
|
285
|
+
);
|
|
286
|
+
if (hasProductSource) {
|
|
287
|
+
return (
|
|
288
|
+
<div className="w-full mt-3">
|
|
289
|
+
<VisualizationRenderer
|
|
290
|
+
data={{ type: 'product_carousel', title: 'Recommended Products', description: 'Loading...', data: [] }}
|
|
291
|
+
primaryColor={primaryColor}
|
|
292
|
+
onAddToCart={onAddToCart}
|
|
293
|
+
className="rounded-3xl border border-slate-200/60 dark:border-white/10 bg-white/90 dark:bg-slate-900/80 p-4"
|
|
294
|
+
isStreaming={true}
|
|
295
|
+
/>
|
|
296
|
+
</div>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!message.uiTransformation) return null;
|
|
266
302
|
const ui = message.uiTransformation as UITransformationResponse;
|
|
267
303
|
const textContent = (ui.data as { content?: string })?.content ?? '';
|
|
268
304
|
|
|
@@ -294,6 +330,7 @@ export function MessageBubble({
|
|
|
294
330
|
);
|
|
295
331
|
})()}
|
|
296
332
|
|
|
333
|
+
|
|
297
334
|
{/* Product Carousel */}
|
|
298
335
|
{!isUser && !hasRichUI && !shouldSuppressProductCarousel && allProducts.length > 0 && (
|
|
299
336
|
<div className="w-full mt-1">
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useState, useEffect, useRef } from 'react';
|
|
4
|
+
import { Brain, ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
|
|
5
|
+
|
|
6
|
+
interface ThinkingBlockProps {
|
|
7
|
+
thinking: string;
|
|
8
|
+
thinkingMs?: number;
|
|
9
|
+
isStreaming?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ThinkingBlock({ thinking, thinkingMs, isStreaming = false }: ThinkingBlockProps) {
|
|
13
|
+
const [isExpanded, setIsExpanded] = useState(true);
|
|
14
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
15
|
+
|
|
16
|
+
// Collapse by default when streaming finishes
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!isStreaming) {
|
|
19
|
+
setIsExpanded(false);
|
|
20
|
+
} else {
|
|
21
|
+
setIsExpanded(true);
|
|
22
|
+
}
|
|
23
|
+
}, [isStreaming]);
|
|
24
|
+
|
|
25
|
+
// Auto-scroll thinking container while streaming
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (isStreaming && isExpanded && containerRef.current) {
|
|
28
|
+
containerRef.current.scrollTop = containerRef.current.scrollHeight;
|
|
29
|
+
}
|
|
30
|
+
}, [thinking, isStreaming, isExpanded]);
|
|
31
|
+
|
|
32
|
+
const durationSec = thinkingMs ? (thinkingMs / 1000).toFixed(1) : null;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className="w-full max-w-2xl my-1.5 border border-slate-200 dark:border-white/10 rounded-xl bg-slate-50/50 dark:bg-white/5 overflow-hidden transition-all duration-200">
|
|
36
|
+
{/* Header */}
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => setIsExpanded(!isExpanded)}
|
|
39
|
+
className="w-full px-4 py-2.5 flex items-center justify-between text-xs font-medium text-slate-500 dark:text-white/40 hover:bg-slate-100 dark:hover:bg-white/10 transition-colors cursor-pointer select-none"
|
|
40
|
+
>
|
|
41
|
+
<div className="flex items-center gap-2">
|
|
42
|
+
{isStreaming ? (
|
|
43
|
+
<Loader2 className="w-3.5 h-3.5 animate-spin text-slate-400 dark:text-white/30" />
|
|
44
|
+
) : (
|
|
45
|
+
<Brain className="w-3.5 h-3.5 text-slate-400 dark:text-white/40" />
|
|
46
|
+
)}
|
|
47
|
+
<span>
|
|
48
|
+
{isStreaming
|
|
49
|
+
? 'Thinking...'
|
|
50
|
+
: durationSec
|
|
51
|
+
? `Thought for ${durationSec}s`
|
|
52
|
+
: 'Thinking process'}
|
|
53
|
+
</span>
|
|
54
|
+
</div>
|
|
55
|
+
<div>
|
|
56
|
+
{isExpanded ? (
|
|
57
|
+
<ChevronDown className="w-3.5 h-3.5 text-slate-400 dark:text-white/30" />
|
|
58
|
+
) : (
|
|
59
|
+
<ChevronRight className="w-3.5 h-3.5 text-slate-400 dark:text-white/30" />
|
|
60
|
+
)}
|
|
61
|
+
</div>
|
|
62
|
+
</button>
|
|
63
|
+
|
|
64
|
+
{/* Body */}
|
|
65
|
+
{isExpanded && (
|
|
66
|
+
<div
|
|
67
|
+
ref={containerRef}
|
|
68
|
+
className="px-4 pb-3 pt-1 border-t border-slate-200 dark:border-white/10 text-xs font-mono leading-relaxed text-slate-600 dark:text-white/60 bg-slate-50/20 dark:bg-transparent max-h-60 overflow-y-auto whitespace-pre-wrap select-text break-words"
|
|
69
|
+
>
|
|
70
|
+
{thinking}
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -183,7 +183,10 @@ function renderVisualization(
|
|
|
183
183
|
return <TableView data={data as TableData} isStreaming={isStreaming} />;
|
|
184
184
|
case 'product_carousel':
|
|
185
185
|
case 'carousel':
|
|
186
|
+
// Show skeleton while streaming so the user sees an indicator immediately
|
|
187
|
+
if (isStreaming) return <CarouselSkeleton />;
|
|
186
188
|
return <CarouselView data={data as CarouselProduct[]} onAddToCart={onAddToCart} primaryColor={primaryColor} />;
|
|
189
|
+
|
|
187
190
|
case 'text':
|
|
188
191
|
default:
|
|
189
192
|
return <TextView data={data} />;
|
|
@@ -194,7 +197,30 @@ function renderVisualization(
|
|
|
194
197
|
}
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
// ───
|
|
200
|
+
// ─── Carousel Skeleton ───────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
function CarouselSkeleton() {
|
|
203
|
+
return (
|
|
204
|
+
<div className="w-full py-2">
|
|
205
|
+
<div className="mb-3 h-4 w-36 rounded-full bg-slate-200 animate-pulse" />
|
|
206
|
+
<div className="flex gap-3 overflow-hidden">
|
|
207
|
+
{Array.from({ length: 4 }).map((_, i) => (
|
|
208
|
+
<div
|
|
209
|
+
key={i}
|
|
210
|
+
className="flex-shrink-0 w-44 rounded-2xl border border-slate-200 bg-white shadow-sm overflow-hidden animate-pulse"
|
|
211
|
+
>
|
|
212
|
+
<div className="h-28 bg-slate-100" />
|
|
213
|
+
<div className="p-3 space-y-2">
|
|
214
|
+
<div className="h-3 w-28 rounded-full bg-slate-200" />
|
|
215
|
+
<div className="h-3 w-16 rounded-full bg-slate-100" />
|
|
216
|
+
<div className="h-3 w-20 rounded-full bg-emerald-100" />
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
))}
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
198
224
|
|
|
199
225
|
function ChartSkeleton({ type }: { type: 'pie' | 'bar' | 'table' }) {
|
|
200
226
|
if (type === 'pie') {
|
package/src/config/RagConfig.ts
CHANGED
|
@@ -199,6 +199,36 @@ export interface RAGConfig {
|
|
|
199
199
|
vectorKeywords?: string[];
|
|
200
200
|
}
|
|
201
201
|
|
|
202
|
+
export interface RetrievalConfig {
|
|
203
|
+
/** Retrieval strategy selected by the host app. */
|
|
204
|
+
strategy?:
|
|
205
|
+
| 'vector'
|
|
206
|
+
| 'keyword'
|
|
207
|
+
| 'hybrid'
|
|
208
|
+
| 'multi-query'
|
|
209
|
+
| 'self-query'
|
|
210
|
+
| 'parent-document'
|
|
211
|
+
| 'contextual-compression'
|
|
212
|
+
| 'ensemble'
|
|
213
|
+
| 'recursive'
|
|
214
|
+
| 'agentic';
|
|
215
|
+
/** Number of documents/chunks retrieved per query. */
|
|
216
|
+
topK?: number;
|
|
217
|
+
/** Minimum similarity score to include a retrieved chunk. */
|
|
218
|
+
scoreThreshold?: number;
|
|
219
|
+
/** Whether to rerank retrieved chunks before generation. */
|
|
220
|
+
useReranking?: boolean;
|
|
221
|
+
/** Provider-specific retrieval options for future strategies. */
|
|
222
|
+
options?: Record<string, unknown>;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface WorkflowConfig {
|
|
226
|
+
/** High-level workflow selector from the SDK prompt. */
|
|
227
|
+
type?: 'simple-rag' | 'rag' | 'hybrid-rag' | 'graph-rag' | 'agentic-rag' | 'agentic';
|
|
228
|
+
/** Future workflow/provider-specific options. */
|
|
229
|
+
options?: Record<string, unknown>;
|
|
230
|
+
}
|
|
231
|
+
|
|
202
232
|
// ---------------------------------------------------------------------------
|
|
203
233
|
// Root Config
|
|
204
234
|
// ---------------------------------------------------------------------------
|
|
@@ -228,3 +258,20 @@ export interface RagConfig {
|
|
|
228
258
|
/** Optional Graph database configuration */
|
|
229
259
|
graphDb?: GraphDBConfig;
|
|
230
260
|
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Friendly SDK configuration accepted by the top-level Retrivora facade.
|
|
264
|
+
* It supports the prompt's `vectorDatabase`, `retrieval`, and `workflow`
|
|
265
|
+
* naming while normalizing to the internal RagConfig shape.
|
|
266
|
+
*/
|
|
267
|
+
export type UniversalRagConfig =
|
|
268
|
+
Partial<Omit<RagConfig, 'vectorDb' | 'rag' | 'llm' | 'embedding' | 'graphDb'>> & {
|
|
269
|
+
vectorDb?: Partial<VectorDBConfig>;
|
|
270
|
+
vectorDatabase?: Partial<VectorDBConfig>;
|
|
271
|
+
llm?: Partial<LLMConfig>;
|
|
272
|
+
embedding?: Partial<EmbeddingConfig>;
|
|
273
|
+
graphDb?: Partial<GraphDBConfig>;
|
|
274
|
+
retrieval?: RetrievalConfig;
|
|
275
|
+
rag?: RAGConfig;
|
|
276
|
+
workflow?: WorkflowConfig;
|
|
277
|
+
};
|
|
@@ -167,6 +167,8 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
167
167
|
temperature: readNumber(env, 'LLM_TEMPERATURE', 0.7),
|
|
168
168
|
options: {
|
|
169
169
|
profile: readString(env, 'LLM_UNIVERSAL_PROFILE'),
|
|
170
|
+
thinking: readString(env, 'LLM_THINKING') === 'true' || readString(env, 'LLM_THINKING') === 'enabled' ? true : (readString(env, 'LLM_THINKING') === 'false' ? false : undefined),
|
|
171
|
+
thinkingBudget: env.LLM_THINKING_BUDGET ? parseInt(env.LLM_THINKING_BUDGET, 10) : undefined,
|
|
170
172
|
},
|
|
171
173
|
},
|
|
172
174
|
embedding: {
|
|
@@ -202,6 +204,29 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
202
204
|
scoreThreshold: readNumber(env, 'RAG_SCORE_THRESHOLD', 0),
|
|
203
205
|
chunkSize: readNumber(env, 'RAG_CHUNK_SIZE', 1000),
|
|
204
206
|
chunkOverlap: readNumber(env, 'RAG_CHUNK_OVERLAP', 200),
|
|
207
|
+
filterableFields: readString(env, 'RAG_FILTERABLE_FIELDS')?.split(',').map(f => f.trim()),
|
|
208
|
+
// Query pipeline toggles — read from .env.local
|
|
209
|
+
useQueryTransformation: readString(env, 'RAG_USE_QUERY_TRANSFORMATION') === 'true',
|
|
210
|
+
useReranking: readString(env, 'RAG_USE_RERANKING') === 'true',
|
|
211
|
+
useGraphRetrieval: readString(env, 'RAG_USE_GRAPH_RETRIEVAL') === 'true',
|
|
212
|
+
architecture: (readString(env, 'RAG_ARCHITECTURE') ?? 'simple') as 'simple' | 'graph' | 'hybrid' | 'agentic',
|
|
213
|
+
chunkingStrategy: (readString(env, 'RAG_CHUNKING_STRATEGY') ?? 'recursive') as 'recursive' | 'markdown' | 'semantic' | 'llamaindex',
|
|
214
|
+
uiMapping: (() => {
|
|
215
|
+
const raw = readString(env, 'RAG_UI_MAPPING');
|
|
216
|
+
if (!raw) return undefined;
|
|
217
|
+
try { return JSON.parse(raw) as Record<string, string>; } catch { return undefined; }
|
|
218
|
+
})(),
|
|
205
219
|
},
|
|
220
|
+
// Optional graph DB — driven by GRAPH_DB_PROVIDER env var
|
|
221
|
+
...(readString(env, 'GRAPH_DB_PROVIDER') ? {
|
|
222
|
+
graphDb: {
|
|
223
|
+
provider: readString(env, 'GRAPH_DB_PROVIDER')! as 'neo4j' | 'memgraph' | 'simple' | 'custom',
|
|
224
|
+
options: {
|
|
225
|
+
uri: readString(env, 'GRAPH_DB_URI'),
|
|
226
|
+
username: readString(env, 'GRAPH_DB_USERNAME'),
|
|
227
|
+
password: readString(env, 'GRAPH_DB_PASSWORD'),
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
} : {}),
|
|
206
231
|
};
|
|
207
232
|
}
|
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
* environment variables are only parsed once per process, avoiding
|
|
7
7
|
* repeated work in serverless cold starts and per-request handler calls.
|
|
8
8
|
*/
|
|
9
|
-
import { RagConfig } from '../config/RagConfig';
|
|
9
|
+
import { RagConfig, UniversalRagConfig, RAGConfig, RetrievalConfig, WorkflowConfig } from '../config/RagConfig';
|
|
10
10
|
import { mergeDefined } from '../utils/templateUtils';
|
|
11
11
|
import { getEnvConfig } from '../config/serverConfig';
|
|
12
|
+
import { ConfigurationException } from '../exceptions';
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
|
|
@@ -64,18 +65,69 @@ export class ConfigResolver {
|
|
|
64
65
|
};
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Resolves the public SDK config shape used by `new Retrivora({...})`.
|
|
70
|
+
* Supports aliases from the product prompt while preserving existing env
|
|
71
|
+
* fallback behavior.
|
|
72
|
+
*/
|
|
73
|
+
static resolveUniversal(hostConfig?: UniversalRagConfig, env: Record<string, string | undefined> = process.env): RagConfig {
|
|
74
|
+
if (!hostConfig) return this.resolve(undefined, env);
|
|
75
|
+
|
|
76
|
+
const normalized: Partial<RagConfig> = {
|
|
77
|
+
...hostConfig,
|
|
78
|
+
vectorDb: hostConfig.vectorDb ?? hostConfig.vectorDatabase,
|
|
79
|
+
rag: this.mergeRetrievalWorkflow(hostConfig.rag, hostConfig.retrieval, hostConfig.workflow),
|
|
80
|
+
} as Partial<RagConfig>;
|
|
81
|
+
|
|
82
|
+
return this.resolve(normalized, env);
|
|
83
|
+
}
|
|
84
|
+
|
|
67
85
|
/**
|
|
68
86
|
* Validates the configuration for required fields.
|
|
69
87
|
*/
|
|
70
88
|
static validate(config: RagConfig): void {
|
|
71
89
|
if (!config.projectId) {
|
|
72
|
-
throw new
|
|
90
|
+
throw new ConfigurationException('[ConfigResolver] projectId is required');
|
|
73
91
|
}
|
|
74
92
|
if (!config.vectorDb.provider) {
|
|
75
|
-
throw new
|
|
93
|
+
throw new ConfigurationException('[ConfigResolver] vectorDb.provider is required');
|
|
76
94
|
}
|
|
77
95
|
if (!config.llm.provider) {
|
|
78
|
-
throw new
|
|
96
|
+
throw new ConfigurationException('[ConfigResolver] llm.provider is required');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private static mergeRetrievalWorkflow(
|
|
101
|
+
rag?: RAGConfig,
|
|
102
|
+
retrieval?: RetrievalConfig,
|
|
103
|
+
workflow?: WorkflowConfig,
|
|
104
|
+
): RAGConfig | undefined {
|
|
105
|
+
if (!rag && !retrieval && !workflow) return undefined;
|
|
106
|
+
|
|
107
|
+
const normalized: RAGConfig = { ...(rag ?? {}) };
|
|
108
|
+
|
|
109
|
+
if (retrieval) {
|
|
110
|
+
normalized.topK = retrieval.topK ?? normalized.topK;
|
|
111
|
+
normalized.scoreThreshold = retrieval.scoreThreshold ?? normalized.scoreThreshold;
|
|
112
|
+
normalized.useReranking = retrieval.useReranking ?? normalized.useReranking;
|
|
113
|
+
|
|
114
|
+
if (retrieval.strategy === 'hybrid') normalized.architecture = normalized.architecture ?? 'hybrid';
|
|
115
|
+
if (retrieval.strategy === 'agentic') normalized.architecture = 'agentic';
|
|
116
|
+
if (retrieval.strategy === 'contextual-compression') normalized.useReranking = true;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (workflow?.type) {
|
|
120
|
+
const type = workflow.type;
|
|
121
|
+
if (type === 'agentic' || type === 'agentic-rag') normalized.architecture = 'agentic';
|
|
122
|
+
else if (type === 'hybrid-rag') normalized.architecture = 'hybrid';
|
|
123
|
+
else if (type === 'graph-rag') {
|
|
124
|
+
normalized.architecture = 'graph';
|
|
125
|
+
normalized.useGraphRetrieval = true;
|
|
126
|
+
} else if (type === 'simple-rag' || type === 'rag') {
|
|
127
|
+
normalized.architecture = normalized.architecture ?? 'simple';
|
|
128
|
+
}
|
|
79
129
|
}
|
|
130
|
+
|
|
131
|
+
return normalized;
|
|
80
132
|
}
|
|
81
133
|
}
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
} from '../config/constants';
|
|
5
5
|
import { ProviderRegistry } from './ProviderRegistry';
|
|
6
6
|
import { LLMFactory } from '../llm/LLMFactory';
|
|
7
|
+
import { ConfigurationException } from '../exceptions';
|
|
7
8
|
|
|
8
9
|
export interface ValidationError {
|
|
9
10
|
field: string;
|
|
@@ -190,7 +191,7 @@ export class ConfigValidator {
|
|
|
190
191
|
const message = errorItems
|
|
191
192
|
.map((e) => `${e.field}: ${e.message}${e.suggestion ? ` (${e.suggestion})` : ''}`)
|
|
192
193
|
.join('\n ');
|
|
193
|
-
throw new
|
|
194
|
+
throw new ConfigurationException(`[ConfigValidator] Configuration validation failed:\n ${message}`, errorItems);
|
|
194
195
|
}
|
|
195
196
|
}
|
|
196
197
|
}
|