@retrivora-ai/rag-engine 1.9.6 → 1.9.8
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 +130 -113
- package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
- package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2376 -489
- package/dist/handlers/index.mjs +2372 -489
- package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
- package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
- package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
- package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
- package/dist/index.css +695 -203
- package/dist/index.d.mts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +1197 -286
- package/dist/index.mjs +1221 -297
- package/dist/server.d.mts +62 -6
- package/dist/server.d.ts +62 -6
- package/dist/server.js +2526 -574
- package/dist/server.mjs +2517 -573
- package/package.json +12 -10
- package/src/app/constants.tsx +207 -212
- package/src/app/layout.tsx +4 -28
- package/src/app/types.ts +17 -17
- package/src/components/AmbientBackground.tsx +10 -10
- package/src/components/ArchitectureCard.tsx +43 -7
- package/src/components/ArchitectureCardsSection.tsx +37 -4
- package/src/components/ChatWidget.tsx +4 -1
- package/src/components/ChatWindow.tsx +9 -2
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/DocViewer.tsx +75 -15
- package/src/components/Documentation.tsx +111 -28
- package/src/components/Hero.tsx +103 -20
- package/src/components/Lifecycle.tsx +65 -25
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +279 -0
- package/src/config/RagConfig.ts +56 -10
- package/src/config/constants.ts +5 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +30 -25
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +154 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +148 -16
- package/src/core/ProviderRegistry.ts +5 -5
- package/src/core/Retrivora.ts +52 -6
- package/src/core/VectorPlugin.ts +74 -11
- package/src/core/mcp.ts +261 -0
- package/src/exceptions/index.ts +52 -0
- package/src/handlers/index.ts +504 -47
- package/src/hooks/useRagChat.ts +100 -43
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +7 -0
- package/src/llm/LLMFactory.ts +15 -6
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +23 -13
- package/src/types/chat.ts +16 -0
- package/src/types/props.ts +50 -1
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
2
|
+
import { MCPRegistry } from './mcp';
|
|
3
|
+
import { ChatMessage, ChatResponse } from '../types';
|
|
4
|
+
|
|
5
|
+
interface CoordinatorOptions {
|
|
6
|
+
llmProvider: ILLMProvider;
|
|
7
|
+
mcpRegistry: MCPRegistry;
|
|
8
|
+
documentSearch: (query: string) => Promise<{ reply: string; sources: any[] }>;
|
|
9
|
+
maxIterations?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class MultiAgentCoordinator {
|
|
13
|
+
private llmProvider: ILLMProvider;
|
|
14
|
+
private mcpRegistry: MCPRegistry;
|
|
15
|
+
private documentSearch: (query: string) => Promise<{ reply: string; sources: any[] }>;
|
|
16
|
+
private maxIterations: number;
|
|
17
|
+
|
|
18
|
+
constructor(options: CoordinatorOptions) {
|
|
19
|
+
this.llmProvider = options.llmProvider;
|
|
20
|
+
this.mcpRegistry = options.mcpRegistry;
|
|
21
|
+
this.documentSearch = options.documentSearch;
|
|
22
|
+
this.maxIterations = options.maxIterations ?? 5;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Run the multi-agent coordination loop synchronously and return the final response.
|
|
27
|
+
*/
|
|
28
|
+
async run(question: string, history: ChatMessage[] = []): Promise<ChatResponse> {
|
|
29
|
+
const generator = this.runStream(question, history);
|
|
30
|
+
let finalReply = '';
|
|
31
|
+
const sources: any[] = [];
|
|
32
|
+
|
|
33
|
+
for await (const chunk of generator) {
|
|
34
|
+
if (typeof chunk === 'string') {
|
|
35
|
+
finalReply += chunk;
|
|
36
|
+
} else if (chunk && typeof chunk === 'object') {
|
|
37
|
+
if ('reply' in chunk && chunk.reply) {
|
|
38
|
+
finalReply += chunk.reply;
|
|
39
|
+
}
|
|
40
|
+
if ('sources' in chunk && chunk.sources) {
|
|
41
|
+
sources.push(...chunk.sources);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
reply: finalReply,
|
|
48
|
+
sources,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Run the multi-agent coordination loop as an async stream, yielding intermediate thought blocks and tool execution events.
|
|
54
|
+
*/
|
|
55
|
+
async *runStream(question: string, history: ChatMessage[] = []): AsyncGenerator<string | ChatResponse> {
|
|
56
|
+
// 1. Gather all tools
|
|
57
|
+
const mcpTools = await this.mcpRegistry.getAllTools();
|
|
58
|
+
|
|
59
|
+
// Construct system instructions with tools
|
|
60
|
+
let systemPrompt = `You are a goal-driven supervisor agent coordinating a RAG search engine and external MCP servers to solve the user's task.
|
|
61
|
+
You must think step-by-step before acting. Write your internal thinking process inside a \`<think>...</think>\` block first, then act or answer.
|
|
62
|
+
|
|
63
|
+
Available Tools:
|
|
64
|
+
- document_search(query: string): Search the vector database and knowledge base for documents.
|
|
65
|
+
|
|
66
|
+
${mcpTools.length > 0 ? 'MCP Server Tools:' : ''}
|
|
67
|
+
${mcpTools.map(mt => `- ${mt.tool.name}(${JSON.stringify(mt.tool.inputSchema?.properties || {})}): ${mt.tool.description || 'No description provided.'}`).join('\n')}
|
|
68
|
+
|
|
69
|
+
Tool Calling Protocol:
|
|
70
|
+
If you need to call a tool, write:
|
|
71
|
+
TOOL_CALL: <toolName>({ "argName": "value" })
|
|
72
|
+
And then STOP generating immediately.
|
|
73
|
+
|
|
74
|
+
Once you have gathered enough information to fully satisfy the user's query, write your final response directly after the closing \`</think>\` block.
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
// 2. Setup message history for the supervisor
|
|
78
|
+
const supervisorHistory: ChatMessage[] = [
|
|
79
|
+
{ role: 'system' as any, content: systemPrompt },
|
|
80
|
+
...history,
|
|
81
|
+
{ role: 'user', content: question }
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
let iteration = 0;
|
|
85
|
+
const allSources: any[] = [];
|
|
86
|
+
|
|
87
|
+
while (iteration < this.maxIterations) {
|
|
88
|
+
iteration++;
|
|
89
|
+
|
|
90
|
+
const context = ''; // Context is populated by tool call results directly in message history
|
|
91
|
+
let fullModelResponse = '';
|
|
92
|
+
let textBuffer = '';
|
|
93
|
+
let inThinking = false;
|
|
94
|
+
let toolCallDetected: { name: string; args: any } | null = null;
|
|
95
|
+
|
|
96
|
+
if (!this.llmProvider.chatStream) {
|
|
97
|
+
// Fallback for non-streaming providers
|
|
98
|
+
const reply = await this.llmProvider.chat(supervisorHistory, context);
|
|
99
|
+
fullModelResponse = reply;
|
|
100
|
+
|
|
101
|
+
// Parse simulated thinking / tool call
|
|
102
|
+
const thinkIndex = reply.indexOf('<think>');
|
|
103
|
+
const endThinkIndex = reply.indexOf('</think>');
|
|
104
|
+
|
|
105
|
+
let thinkingText = '';
|
|
106
|
+
let answerText = reply;
|
|
107
|
+
|
|
108
|
+
if (thinkIndex !== -1 && endThinkIndex !== -1) {
|
|
109
|
+
thinkingText = reply.substring(thinkIndex + 7, endThinkIndex);
|
|
110
|
+
answerText = reply.substring(endThinkIndex + 8);
|
|
111
|
+
yield { type: 'thinking', text: thinkingText } as any;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const toolMatch = answerText.match(/TOOL_CALL:\s*(\w+)\s*\(([\s\S]*)\)/);
|
|
115
|
+
if (toolMatch) {
|
|
116
|
+
const toolName = toolMatch[1];
|
|
117
|
+
const toolArgsStr = toolMatch[2];
|
|
118
|
+
try {
|
|
119
|
+
const toolArgs = JSON.parse(toolArgsStr.trim());
|
|
120
|
+
toolCallDetected = { name: toolName, args: toolArgs };
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.error('[MultiAgentCoordinator] Failed to parse tool args:', toolArgsStr, e);
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
yield answerText;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
// Run with streaming completion
|
|
129
|
+
const stream = this.llmProvider.chatStream(supervisorHistory, context);
|
|
130
|
+
|
|
131
|
+
for await (const chunk of stream) {
|
|
132
|
+
fullModelResponse += chunk;
|
|
133
|
+
textBuffer += chunk;
|
|
134
|
+
|
|
135
|
+
// Check for <think> tag
|
|
136
|
+
if (!inThinking && textBuffer.includes('<think>')) {
|
|
137
|
+
inThinking = true;
|
|
138
|
+
const idx = textBuffer.indexOf('<think>');
|
|
139
|
+
textBuffer = textBuffer.slice(idx + 7);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Check for </think> tag
|
|
143
|
+
if (inThinking && textBuffer.includes('</think>')) {
|
|
144
|
+
inThinking = false;
|
|
145
|
+
const idx = textBuffer.indexOf('</think>');
|
|
146
|
+
const thinkingDelta = textBuffer.slice(0, idx);
|
|
147
|
+
yield { type: 'thinking', text: thinkingDelta } as any;
|
|
148
|
+
textBuffer = textBuffer.slice(idx + 8);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// If we are in thinking mode, yield thinking delta
|
|
152
|
+
if (inThinking && textBuffer.length > 0) {
|
|
153
|
+
yield { type: 'thinking', text: textBuffer } as any;
|
|
154
|
+
textBuffer = '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Check for TOOL_CALL during stream
|
|
158
|
+
if (!inThinking && textBuffer.includes('TOOL_CALL:')) {
|
|
159
|
+
const idx = textBuffer.indexOf('TOOL_CALL:');
|
|
160
|
+
const precedingText = textBuffer.slice(0, idx);
|
|
161
|
+
if (precedingText.trim()) {
|
|
162
|
+
yield precedingText;
|
|
163
|
+
}
|
|
164
|
+
// Await remainder of stream to get the full tool call signature
|
|
165
|
+
textBuffer = textBuffer.slice(idx);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// If not in thinking and no tool call prefix is blocking, yield final answer text
|
|
169
|
+
if (!inThinking && !textBuffer.includes('TOOL_CALL:') && textBuffer.length > 0) {
|
|
170
|
+
yield textBuffer;
|
|
171
|
+
textBuffer = '';
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Flush any remaining buffer text
|
|
176
|
+
if (textBuffer.trim()) {
|
|
177
|
+
const toolMatch = textBuffer.match(/TOOL_CALL:\s*(\w+)\s*\(([\s\S]*)\)/);
|
|
178
|
+
if (toolMatch) {
|
|
179
|
+
const toolName = toolMatch[1];
|
|
180
|
+
const toolArgsStr = toolMatch[2];
|
|
181
|
+
try {
|
|
182
|
+
const toolArgs = JSON.parse(toolArgsStr.trim());
|
|
183
|
+
toolCallDetected = { name: toolName, args: toolArgs };
|
|
184
|
+
} catch (e) {
|
|
185
|
+
console.error('[MultiAgentCoordinator] Failed to parse tool args:', toolArgsStr, e);
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
yield textBuffer;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// 3. Append the model response to the supervisor history
|
|
194
|
+
supervisorHistory.push({ role: 'assistant', content: fullModelResponse });
|
|
195
|
+
|
|
196
|
+
// 4. Handle tool execution if requested, else break the loop
|
|
197
|
+
if (toolCallDetected) {
|
|
198
|
+
const { name: toolName, args: toolArgs } = toolCallDetected;
|
|
199
|
+
|
|
200
|
+
yield {
|
|
201
|
+
type: 'thinking',
|
|
202
|
+
text: `\n[Agent Call] Executing tool "${toolName}" with parameters ${JSON.stringify(toolArgs)}...\n`
|
|
203
|
+
} as any;
|
|
204
|
+
|
|
205
|
+
let toolResultText = '';
|
|
206
|
+
try {
|
|
207
|
+
if (toolName === 'document_search') {
|
|
208
|
+
const searchRes = await this.documentSearch(toolArgs.query || '');
|
|
209
|
+
toolResultText = `document_search returned:\n${searchRes.reply}\n\nSources count: ${searchRes.sources.length}`;
|
|
210
|
+
if (searchRes.sources && searchRes.sources.length > 0) {
|
|
211
|
+
allSources.push(...searchRes.sources);
|
|
212
|
+
yield { reply: '', sources: searchRes.sources };
|
|
213
|
+
}
|
|
214
|
+
} else {
|
|
215
|
+
// Call MCP tool
|
|
216
|
+
const mcpRes = await this.mcpRegistry.callTool(toolName, toolArgs);
|
|
217
|
+
toolResultText = `MCP Tool "${toolName}" returned:\n${JSON.stringify(mcpRes.content)}`;
|
|
218
|
+
}
|
|
219
|
+
} catch (err: any) {
|
|
220
|
+
toolResultText = `Error calling tool "${toolName}": ${err.message}`;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Append tool output to history
|
|
224
|
+
supervisorHistory.push({
|
|
225
|
+
role: 'user',
|
|
226
|
+
content: `TOOL_RESULT for ${toolName}:\n${toolResultText}`
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
yield {
|
|
230
|
+
type: 'thinking',
|
|
231
|
+
text: `[Agent Output] Tool "${toolName}" executed. Continuing reasoning...\n`
|
|
232
|
+
} as any;
|
|
233
|
+
} else {
|
|
234
|
+
// No tool call requested — response is complete
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
package/src/core/Pipeline.ts
CHANGED
|
@@ -2,12 +2,15 @@ import { BaseVectorProvider } from '../providers/vectordb/BaseVectorProvider';
|
|
|
2
2
|
import { BaseGraphProvider } from '../providers/graphdb/BaseGraphProvider';
|
|
3
3
|
import { ILLMProvider } from '../llm/ILLMProvider';
|
|
4
4
|
import { ChatMessage } from '../types';
|
|
5
|
+
import { EmbeddingFailedException } from '../exceptions';
|
|
5
6
|
import { DocumentChunker, Chunk } from '../rag/DocumentChunker';
|
|
6
7
|
import { EntityExtractor } from '../rag/EntityExtractor';
|
|
7
8
|
import { Reranker } from '../rag/Reranker';
|
|
8
9
|
import { LlamaIndexIngestor } from '../rag/LlamaIndexIngestor';
|
|
9
10
|
import { LangChainAgent } from './LangChainAgent';
|
|
10
11
|
import { RagConfig } from '../config/RagConfig';
|
|
12
|
+
import { MCPRegistry } from './mcp';
|
|
13
|
+
import { MultiAgentCoordinator } from './MultiAgentCoordinator';
|
|
11
14
|
import { ProviderRegistry } from './ProviderRegistry';
|
|
12
15
|
import { BatchProcessor, BatchOptions } from './BatchProcessor';
|
|
13
16
|
import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
|
|
@@ -164,9 +167,13 @@ export class Pipeline {
|
|
|
164
167
|
private entityExtractor?: EntityExtractor;
|
|
165
168
|
private reranker: Reranker;
|
|
166
169
|
private agent?: LangChainAgent;
|
|
170
|
+
private mcpRegistry?: MCPRegistry;
|
|
171
|
+
private multiAgentCoordinator?: MultiAgentCoordinator;
|
|
167
172
|
/** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
|
|
168
173
|
private embeddingCache = new LRUEmbeddingCache(500);
|
|
169
174
|
private initialised = false;
|
|
175
|
+
/** Namespace-specific static cold context cache for CAG */
|
|
176
|
+
private coldContexts = new Map<string, string>();
|
|
170
177
|
|
|
171
178
|
constructor(private config: RagConfig) {
|
|
172
179
|
this.chunker = new DocumentChunker(
|
|
@@ -249,14 +256,52 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
249
256
|
|
|
250
257
|
await this.vectorDB.initialize();
|
|
251
258
|
|
|
252
|
-
if (this.config.rag?.architecture === 'agentic') {
|
|
259
|
+
if (this.config.rag?.architecture === 'agentic' || (this.config.mcpServers && this.config.mcpServers.length > 0)) {
|
|
260
|
+
this.mcpRegistry = new MCPRegistry(this.config.mcpServers || []);
|
|
261
|
+
this.multiAgentCoordinator = new MultiAgentCoordinator({
|
|
262
|
+
llmProvider: this.llmProvider,
|
|
263
|
+
mcpRegistry: this.mcpRegistry,
|
|
264
|
+
documentSearch: async (query: string) => {
|
|
265
|
+
return this.runNormalQuery(query);
|
|
266
|
+
}
|
|
267
|
+
});
|
|
253
268
|
this.agent = new LangChainAgent(this, this.config);
|
|
254
269
|
await this.agent.initialize(this.llmProvider as unknown);
|
|
255
270
|
}
|
|
256
271
|
|
|
272
|
+
if (this.config.rag?.cag?.enabled) {
|
|
273
|
+
await this.loadColdContext(this.config.projectId);
|
|
274
|
+
}
|
|
275
|
+
|
|
257
276
|
this.initialised = true;
|
|
258
277
|
}
|
|
259
278
|
|
|
279
|
+
private async loadColdContext(ns: string): Promise<void> {
|
|
280
|
+
const cagConfig = this.config.rag?.cag;
|
|
281
|
+
if (!cagConfig || !cagConfig.enabled) return;
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const coldNs = cagConfig.coldNamespace || ns;
|
|
285
|
+
const limit = cagConfig.coldLimit ?? 20;
|
|
286
|
+
|
|
287
|
+
const queryText = cagConfig.coldQuery || 'documentation';
|
|
288
|
+
const queryVector = await this.embeddingProvider.embed(queryText, { taskType: 'query' });
|
|
289
|
+
const coldMatches = await this.vectorDB.query(queryVector, limit, coldNs);
|
|
290
|
+
|
|
291
|
+
if (coldMatches.length > 0) {
|
|
292
|
+
const formatted = coldMatches.map((m, i) => `[Cold Source ${i + 1}]\n${m.content}`).join('\n\n---\n\n');
|
|
293
|
+
this.coldContexts.set(ns, formatted);
|
|
294
|
+
console.log(`[Pipeline] Cold RAG (CAG) loaded ${coldMatches.length} documents into cached context for namespace: ${ns}`);
|
|
295
|
+
} else {
|
|
296
|
+
this.coldContexts.set(ns, 'No cold context found.');
|
|
297
|
+
console.warn(`[Pipeline] Cold RAG (CAG) initialized but no documents were found in namespace: ${coldNs}`);
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
console.error(`[Pipeline] Failed to load Cold RAG (CAG) context for namespace ${ns}:`, err);
|
|
301
|
+
this.coldContexts.set(ns, 'Failed to load cold context.');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
260
305
|
/**
|
|
261
306
|
* Ingest documents with automatic chunking, embedding, and batch upsert.
|
|
262
307
|
*/
|
|
@@ -327,7 +372,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
327
372
|
);
|
|
328
373
|
|
|
329
374
|
if (vectors.length !== chunks.length) {
|
|
330
|
-
throw new
|
|
375
|
+
throw new EmbeddingFailedException(
|
|
331
376
|
`[Pipeline] Embedding mismatch: got ${vectors.length} vectors for ${chunks.length} chunks. ` +
|
|
332
377
|
`Check embedding provider logs for individual chunk failures.`
|
|
333
378
|
);
|
|
@@ -385,12 +430,26 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
385
430
|
);
|
|
386
431
|
}
|
|
387
432
|
|
|
433
|
+
async runNormalQuery(question: string, history: ChatMessage[] = [], namespace?: string): Promise<{ reply: string; sources: any[] }> {
|
|
434
|
+
const stream = this.askStreamInternal(question, history, namespace);
|
|
435
|
+
let reply = '';
|
|
436
|
+
let sources: any[] = [];
|
|
437
|
+
for await (const chunk of stream) {
|
|
438
|
+
if (typeof chunk === 'string') {
|
|
439
|
+
reply += chunk;
|
|
440
|
+
} else if (typeof chunk === 'object' && chunk !== null) {
|
|
441
|
+
if ('reply' in chunk && chunk.reply) reply += chunk.reply;
|
|
442
|
+
if ('sources' in chunk && chunk.sources) sources = chunk.sources;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return { reply, sources };
|
|
446
|
+
}
|
|
447
|
+
|
|
388
448
|
async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
|
|
389
449
|
await this.initialize();
|
|
390
450
|
|
|
391
|
-
if (this.config.rag?.architecture === 'agentic' && this.
|
|
392
|
-
|
|
393
|
-
return { reply: agentReply, sources: [] };
|
|
451
|
+
if ((this.config.rag?.architecture === 'agentic' || (this.config.mcpServers && this.config.mcpServers.length > 0)) && this.multiAgentCoordinator) {
|
|
452
|
+
return await this.multiAgentCoordinator.run(question, history);
|
|
394
453
|
}
|
|
395
454
|
|
|
396
455
|
const stream = this.askStream(question, history, namespace);
|
|
@@ -414,6 +473,21 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
414
473
|
return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
|
|
415
474
|
}
|
|
416
475
|
|
|
476
|
+
async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
|
|
477
|
+
await this.initialize();
|
|
478
|
+
if ((this.config.rag?.architecture === 'agentic' || (this.config.mcpServers && this.config.mcpServers.length > 0)) && this.multiAgentCoordinator) {
|
|
479
|
+
const stream = this.multiAgentCoordinator.runStream(question, history);
|
|
480
|
+
for await (const chunk of stream) {
|
|
481
|
+
yield chunk;
|
|
482
|
+
}
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const stream = this.askStreamInternal(question, history, namespace);
|
|
486
|
+
for await (const chunk of stream) {
|
|
487
|
+
yield chunk;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
417
491
|
/**
|
|
418
492
|
* High-performance streaming RAG flow.
|
|
419
493
|
* Yields text chunks first, then the retrieval metadata + observability trace at the end.
|
|
@@ -424,7 +498,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
424
498
|
* - UITransformation is computed after text streaming and emitted with metadata
|
|
425
499
|
* - SchemaMapper.train runs while answer generation streams
|
|
426
500
|
*/
|
|
427
|
-
async *
|
|
501
|
+
async *askStreamInternal(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
|
|
428
502
|
await this.initialize();
|
|
429
503
|
const ns = namespace ?? this.config.projectId;
|
|
430
504
|
const topK = this.config.rag?.topK ?? 5;
|
|
@@ -546,6 +620,28 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
546
620
|
context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
|
|
547
621
|
}
|
|
548
622
|
|
|
623
|
+
if (this.config.rag?.cag?.enabled) {
|
|
624
|
+
if (!this.coldContexts.has(ns)) {
|
|
625
|
+
await this.loadColdContext(ns);
|
|
626
|
+
}
|
|
627
|
+
const coldContext = this.coldContexts.get(ns);
|
|
628
|
+
if (coldContext && coldContext !== 'No cold context found.' && coldContext !== 'Failed to load cold context.') {
|
|
629
|
+
context = `COLD CONTEXT (STATIC KNOWLEDGE):\n${coldContext}\n\nRETRIEVED HOT CONTEXT (REAL-TIME KNOWLEDGE):\n${context}`;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Yield sources immediately to support early UI citation rendering (highly responsive)
|
|
634
|
+
yield {
|
|
635
|
+
reply: '',
|
|
636
|
+
sources: sources.map((s) => ({
|
|
637
|
+
id: s.id,
|
|
638
|
+
score: s.score,
|
|
639
|
+
content: s.content,
|
|
640
|
+
metadata: s.metadata ?? {},
|
|
641
|
+
namespace: ns,
|
|
642
|
+
} satisfies RetrievedChunk)),
|
|
643
|
+
} as ChatResponse;
|
|
644
|
+
|
|
549
645
|
// 4.5 Schema Training — true fire-and-forget, never gates anything.
|
|
550
646
|
const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
|
|
551
647
|
const cachedSchema = allMetadataKeys.length > 0
|
|
@@ -750,15 +846,15 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
750
846
|
estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
|
|
751
847
|
};
|
|
752
848
|
|
|
753
|
-
// 7. Await
|
|
754
|
-
|
|
755
|
-
|
|
849
|
+
// 7. Await UI transformation and optionally hallucination score before yielding the metadata frame.
|
|
850
|
+
const awaitHallucination = this.config.llm.options?.awaitHallucination === true;
|
|
851
|
+
|
|
756
852
|
const [uiTransformation, hallucinationResult] = await Promise.all([
|
|
757
853
|
uiTransformationPromise,
|
|
758
|
-
hallucinationScoringPromise,
|
|
854
|
+
awaitHallucination ? hallucinationScoringPromise : Promise.resolve(undefined),
|
|
759
855
|
]);
|
|
760
856
|
|
|
761
|
-
const
|
|
857
|
+
const buildTrace = (hScore?: { score: number; reason: string }): ObservabilityTrace => ({
|
|
762
858
|
requestId,
|
|
763
859
|
query: question,
|
|
764
860
|
rewrittenQuery,
|
|
@@ -774,12 +870,48 @@ You are a helpful assistant. Use the provided context to answer questions accura
|
|
|
774
870
|
latency,
|
|
775
871
|
tokens,
|
|
776
872
|
timestamp: new Date().toISOString(),
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
hallucinationReason: hallucinationResult.reason,
|
|
873
|
+
...(hScore ? {
|
|
874
|
+
hallucinationScore: hScore.score,
|
|
875
|
+
hallucinationReason: hScore.reason,
|
|
781
876
|
} : {}),
|
|
782
|
-
};
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
const trace = buildTrace(hallucinationResult);
|
|
880
|
+
|
|
881
|
+
// Asynchronously trigger telemetry logging without blocking response yield
|
|
882
|
+
if (this.config.telemetry?.enabled) {
|
|
883
|
+
const telemetryUrl = this.config.telemetry.url || '/api/telemetry';
|
|
884
|
+
const absoluteUrl = telemetryUrl.startsWith('http')
|
|
885
|
+
? telemetryUrl
|
|
886
|
+
: (process.env.NEXT_PUBLIC_APP_URL || `http://localhost:${process.env.PORT || 3000}`) + telemetryUrl;
|
|
887
|
+
|
|
888
|
+
// Start background process for telemetry, awaiting hallucination scoring if it was backgrounded
|
|
889
|
+
(async () => {
|
|
890
|
+
try {
|
|
891
|
+
let finalTrace = trace;
|
|
892
|
+
if (!awaitHallucination && runHallucination) {
|
|
893
|
+
const backgroundScoreResult = await hallucinationScoringPromise;
|
|
894
|
+
if (backgroundScoreResult) {
|
|
895
|
+
finalTrace = buildTrace(backgroundScoreResult);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
await fetch(absoluteUrl, {
|
|
900
|
+
method: 'POST',
|
|
901
|
+
headers: {
|
|
902
|
+
'Content-Type': 'application/json',
|
|
903
|
+
},
|
|
904
|
+
body: JSON.stringify({
|
|
905
|
+
trace: finalTrace,
|
|
906
|
+
licenseKey: this.config.licenseKey,
|
|
907
|
+
projectId: ns
|
|
908
|
+
})
|
|
909
|
+
});
|
|
910
|
+
} catch (err: any) {
|
|
911
|
+
console.warn(`[Pipeline Telemetry] Failed to send trace async to ${absoluteUrl}:`, err.message);
|
|
912
|
+
}
|
|
913
|
+
})();
|
|
914
|
+
}
|
|
783
915
|
|
|
784
916
|
yield {
|
|
785
917
|
reply: '',
|
|
@@ -10,7 +10,7 @@ import { ProviderNotFoundException } from '../exceptions';
|
|
|
10
10
|
* ProviderRegistry — dynamic provider loader for Vector DBs and LLMs.
|
|
11
11
|
*/
|
|
12
12
|
type VectorProviderClass = {
|
|
13
|
-
new
|
|
13
|
+
new(config: VectorDBConfig): BaseVectorProvider;
|
|
14
14
|
getValidator?: () => IProviderValidator;
|
|
15
15
|
getHealthChecker?: () => IProviderHealthChecker;
|
|
16
16
|
};
|
|
@@ -20,10 +20,10 @@ type GraphProviderClass = new (config: GraphDBConfig) => BaseGraphProvider;
|
|
|
20
20
|
export class ProviderRegistry {
|
|
21
21
|
private static vectorProviders: Record<string, VectorProviderClass> = {};
|
|
22
22
|
private static graphProviders: Record<string, GraphProviderClass> = {};
|
|
23
|
-
|
|
23
|
+
|
|
24
24
|
private static vectorValidators: Record<string, IProviderValidator> = {};
|
|
25
25
|
private static vectorHealthCheckers: Record<string, IProviderHealthChecker> = {};
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
private static llmValidators: Record<string, IProviderValidator> = {};
|
|
28
28
|
private static llmHealthCheckers: Record<string, IProviderHealthChecker> = {};
|
|
29
29
|
|
|
@@ -39,7 +39,7 @@ export class ProviderRegistry {
|
|
|
39
39
|
|
|
40
40
|
static async getVectorValidator(provider: string): Promise<IProviderValidator | null> {
|
|
41
41
|
if (this.vectorValidators[provider]) return this.vectorValidators[provider];
|
|
42
|
-
|
|
42
|
+
|
|
43
43
|
// Try to load built-in validator
|
|
44
44
|
try {
|
|
45
45
|
const providerClass = await this.loadVectorProviderClass(provider);
|
|
@@ -55,7 +55,7 @@ export class ProviderRegistry {
|
|
|
55
55
|
|
|
56
56
|
static async getVectorHealthChecker(provider: string): Promise<IProviderHealthChecker | null> {
|
|
57
57
|
if (this.vectorHealthCheckers[provider]) return this.vectorHealthCheckers[provider];
|
|
58
|
-
|
|
58
|
+
|
|
59
59
|
try {
|
|
60
60
|
const providerClass = await this.loadVectorProviderClass(provider);
|
|
61
61
|
if (providerClass.getHealthChecker) {
|
package/src/core/Retrivora.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { RagConfig, RetrievalConfig, UniversalRagConfig } from '../config/RagCon
|
|
|
2
2
|
import { ChatMessage, ChatResponse, IngestDocument } from '../types';
|
|
3
3
|
import { ConfigResolver } from './ConfigResolver';
|
|
4
4
|
import { ConfigValidator } from './ConfigValidator';
|
|
5
|
+
import { LicenseVerifier } from './LicenseVerifier';
|
|
5
6
|
import { Pipeline } from './Pipeline';
|
|
7
|
+
import { wrapError, RetrivoraErrorCode } from '../exceptions';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Public SDK facade matching the prompt-level Retrivora API.
|
|
@@ -20,27 +22,70 @@ export class Retrivora {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
async initialize(): Promise<void> {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
try {
|
|
26
|
+
await ConfigValidator.validateAndThrow(this.config);
|
|
27
|
+
LicenseVerifier.verify(
|
|
28
|
+
this.config.licenseKey,
|
|
29
|
+
this.config.projectId,
|
|
30
|
+
this.config.vectorDb?.provider
|
|
31
|
+
);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
throw wrapError(err, 'CONFIGURATION_ERROR');
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
await this.pipeline.initialize();
|
|
37
|
+
} catch (err) {
|
|
38
|
+
throw wrapError(err, 'AUTHENTICATION_ERROR');
|
|
39
|
+
}
|
|
25
40
|
}
|
|
26
41
|
|
|
27
42
|
async ingest(
|
|
28
43
|
documents: IngestDocument[],
|
|
29
44
|
namespace?: string,
|
|
30
45
|
): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
|
|
31
|
-
|
|
46
|
+
try {
|
|
47
|
+
return await this.pipeline.ingest(documents, namespace);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
const msg = String(err);
|
|
50
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
51
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
52
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
53
|
+
}
|
|
54
|
+
throw wrapError(err, defaultCode);
|
|
55
|
+
}
|
|
32
56
|
}
|
|
33
57
|
|
|
34
58
|
async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
|
|
35
|
-
|
|
59
|
+
try {
|
|
60
|
+
return await this.pipeline.ask(question, history, namespace);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const msg = String(err);
|
|
63
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
64
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
65
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
66
|
+
}
|
|
67
|
+
throw wrapError(err, defaultCode);
|
|
68
|
+
}
|
|
36
69
|
}
|
|
37
70
|
|
|
38
|
-
askStream(
|
|
71
|
+
async *askStream(
|
|
39
72
|
question: string,
|
|
40
73
|
history: ChatMessage[] = [],
|
|
41
74
|
namespace?: string,
|
|
42
75
|
): AsyncIterable<string | ChatResponse> {
|
|
43
|
-
|
|
76
|
+
try {
|
|
77
|
+
const stream = this.pipeline.askStream(question, history, namespace);
|
|
78
|
+
for await (const chunk of stream) {
|
|
79
|
+
yield chunk;
|
|
80
|
+
}
|
|
81
|
+
} catch (err) {
|
|
82
|
+
const msg = String(err);
|
|
83
|
+
let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
|
|
84
|
+
if (msg.includes('Embed') || msg.includes('embed')) {
|
|
85
|
+
defaultCode = 'EMBEDDING_FAILED';
|
|
86
|
+
}
|
|
87
|
+
throw wrapError(err, defaultCode);
|
|
88
|
+
}
|
|
44
89
|
}
|
|
45
90
|
|
|
46
91
|
getPipeline(): Pipeline {
|
|
@@ -49,3 +94,4 @@ export class Retrivora {
|
|
|
49
94
|
}
|
|
50
95
|
|
|
51
96
|
export type { RetrievalConfig, UniversalRagConfig };
|
|
97
|
+
|