@retrivora-ai/rag-engine 1.9.7 → 1.9.9
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 +136 -136
- package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
- package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2327 -474
- package/dist/handlers/index.mjs +2329 -473
- package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
- package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
- package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
- package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
- package/dist/index.css +783 -550
- package/dist/index.d.mts +35 -7
- package/dist/index.d.ts +35 -7
- package/dist/index.js +419 -282
- package/dist/index.mjs +426 -292
- package/dist/server.d.mts +65 -6
- package/dist/server.d.ts +65 -6
- package/dist/server.js +2185 -317
- package/dist/server.mjs +2185 -317
- package/package.json +13 -8
- package/src/app/constants.tsx +37 -7
- package/src/app/page.tsx +2 -0
- package/src/components/ChatWidget.tsx +2 -1
- package/src/components/ChatWindow.tsx +4 -1
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +228 -0
- package/src/config/RagConfig.ts +48 -2
- package/src/config/constants.ts +4 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +38 -6
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +260 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +151 -18
- package/src/core/Retrivora.ts +7 -0
- package/src/core/VectorPlugin.ts +12 -3
- package/src/core/mcp.ts +261 -0
- package/src/handlers/index.ts +449 -63
- package/src/hooks/useRagChat.ts +96 -42
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +3 -0
- package/src/llm/LLMFactory.ts +9 -1
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +7 -0
- package/src/types/chat.ts +14 -0
- package/src/types/props.ts +12 -0
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
- package/src/components/AmbientBackground.tsx +0 -29
- package/src/components/ArchitectureCard.tsx +0 -17
- package/src/components/ArchitectureCardsSection.tsx +0 -15
- package/src/components/DocViewer.tsx +0 -103
- package/src/components/Documentation.tsx +0 -121
- package/src/components/Hero.tsx +0 -59
- package/src/components/Lifecycle.tsx +0 -37
- package/src/components/Navbar.tsx +0 -55
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
import { ConfigurationException } from '../exceptions';
|
|
3
|
+
|
|
4
|
+
export interface LicensePayload {
|
|
5
|
+
projectId: string;
|
|
6
|
+
expiresAt: number; // UNIX timestamp in seconds
|
|
7
|
+
tier: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* LicenseVerifier — handles cryptographic validation of signed JWT license keys.
|
|
12
|
+
* Enables zero-latency local license validation without external network calls.
|
|
13
|
+
*/
|
|
14
|
+
export class LicenseVerifier {
|
|
15
|
+
// A simple in-memory cache to save CPU overhead of cryptographic signature checks.
|
|
16
|
+
private static readonly cache = new Map<
|
|
17
|
+
string,
|
|
18
|
+
{ payload: LicensePayload; cachedAt: number }
|
|
19
|
+
>();
|
|
20
|
+
private static readonly CACHE_TTL_MS = 60 * 1000; // Cache verified license for 60 seconds
|
|
21
|
+
|
|
22
|
+
// Retrivora's Public Key used to verify the license key signature.
|
|
23
|
+
// In production, this matches the private key held by Retrivora SaaS.
|
|
24
|
+
private static readonly PUBLIC_KEY = `-----BEGIN PUBLIC KEY-----
|
|
25
|
+
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArMieCkCBtTfByRNOserm
|
|
26
|
+
XM4T0Am29JyNzB4XQPe+WJJ56CN73H1HLXx9n1ByFcxkEJ9po+SURj5DnvUXwEy+
|
|
27
|
+
xstx33wDPpVmcMQe33j5CRYS0S4gICiNqIRN7XkTlJtrcXqrmh1/hdOAwfRbjO1T
|
|
28
|
+
NVtORHwUwWfI/lBLyxUPGtKPed+0fQ7NtcW4o00JXxs3EjCB5BV9CbnXoTjQb/4h
|
|
29
|
+
iwZof4l8rb7oaNzOsVg0RSyxsETX7Ex5sI0CjBz1p2mYp4oVhw/A/h8Ls7H0XzjC
|
|
30
|
+
+Eb6BLtsytt5JHoSp0jExLlCpDmpys2L+kETcBVx+IqiXtdN1n6KbkksvEDUVzLI
|
|
31
|
+
MwIDAQAB
|
|
32
|
+
-----END PUBLIC KEY-----`;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decodes, verifies signature, and checks metadata of a license key.
|
|
36
|
+
*
|
|
37
|
+
* @param licenseKey - Base64url signed JWT license key.
|
|
38
|
+
* @param currentProjectId - Project namespace ID.
|
|
39
|
+
* @param publicKeyOverride - Optional override for unit/integration tests.
|
|
40
|
+
*/
|
|
41
|
+
static verify(
|
|
42
|
+
licenseKey: string | undefined,
|
|
43
|
+
currentProjectId: string,
|
|
44
|
+
provider?: string,
|
|
45
|
+
publicKeyOverride?: string
|
|
46
|
+
): LicensePayload {
|
|
47
|
+
const isProduction = process.env.NODE_ENV === 'production';
|
|
48
|
+
const now = Date.now();
|
|
49
|
+
|
|
50
|
+
// Check verification cache to bypass crypto computations on hot paths
|
|
51
|
+
if (licenseKey) {
|
|
52
|
+
const cacheKey = `${licenseKey}:${currentProjectId}:${provider || ''}:${publicKeyOverride || ''}`;
|
|
53
|
+
const cached = this.cache.get(cacheKey);
|
|
54
|
+
if (cached && now - cached.cachedAt < this.CACHE_TTL_MS) {
|
|
55
|
+
// Enforce expiration check even for cached items
|
|
56
|
+
const currentTimestampSec = Math.floor(now / 1000);
|
|
57
|
+
if (cached.payload.expiresAt && currentTimestampSec > cached.payload.expiresAt) {
|
|
58
|
+
this.cache.delete(cacheKey);
|
|
59
|
+
} else {
|
|
60
|
+
return cached.payload;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// 1. Development Fallback (Fail-open locally but log warning)
|
|
66
|
+
if (!licenseKey) {
|
|
67
|
+
if (isProduction) {
|
|
68
|
+
throw new ConfigurationException(
|
|
69
|
+
'[Retrivora SDK] License key (licenseKey) is required in production environments.'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
console.warn(
|
|
73
|
+
`\x1b[33m[Retrivora SDK] WARNING: Running without a license key. This namespace (${currentProjectId}) is permitted for local development only.\x1b[0m`
|
|
74
|
+
);
|
|
75
|
+
return {
|
|
76
|
+
projectId: currentProjectId,
|
|
77
|
+
expiresAt: Math.floor(Date.now() / 1000) + 86400, // Valid for 24h
|
|
78
|
+
tier: 'hobby',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const parts = licenseKey.split('.');
|
|
84
|
+
if (parts.length !== 3) {
|
|
85
|
+
throw new Error('Malformed token structure (expected 3 parts).');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
89
|
+
const dataToVerify = `${headerB64}.${payloadB64}`;
|
|
90
|
+
|
|
91
|
+
const publicKey = publicKeyOverride ?? this.PUBLIC_KEY;
|
|
92
|
+
|
|
93
|
+
// 2. Cryptographic signature check
|
|
94
|
+
const signature = Buffer.from(signatureB64, 'base64url');
|
|
95
|
+
const data = Buffer.from(dataToVerify);
|
|
96
|
+
|
|
97
|
+
const isValid = crypto.verify(
|
|
98
|
+
'sha256',
|
|
99
|
+
data,
|
|
100
|
+
publicKey,
|
|
101
|
+
signature
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (!isValid) {
|
|
105
|
+
throw new Error('Signature verification failed.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 3. Decode & validate payload checks
|
|
109
|
+
const payload: LicensePayload = JSON.parse(
|
|
110
|
+
Buffer.from(payloadB64, 'base64url').toString('utf8')
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
if (!payload.projectId) {
|
|
114
|
+
throw new Error('License payload is missing "projectId".');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (payload.projectId !== currentProjectId) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`Project ID mismatch. License is bound to project namespace "${payload.projectId}" ` +
|
|
120
|
+
`but configuration has "${currentProjectId}".`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (!payload.expiresAt) {
|
|
125
|
+
throw new Error('License payload is missing expiration ("expiresAt").');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const currentTimestampSec = Math.floor(Date.now() / 1000);
|
|
129
|
+
if (currentTimestampSec > payload.expiresAt) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
`License key has expired. Expiration: ${new Date(
|
|
132
|
+
payload.expiresAt * 1000
|
|
133
|
+
).toDateString()}`
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 4. Validate Vector DB Provider against Tier (Production only)
|
|
138
|
+
if (isProduction && provider) {
|
|
139
|
+
const tier = (payload.tier || '').toLowerCase();
|
|
140
|
+
const normalizedProvider = provider.toLowerCase();
|
|
141
|
+
|
|
142
|
+
if (tier === 'hobby') {
|
|
143
|
+
// Hobby tier only supports PostgreSQL/pgvector/Supabase
|
|
144
|
+
const allowedHobby = ['postgresql', 'pgvector', 'supabase'];
|
|
145
|
+
if (!allowedHobby.includes(normalizedProvider)) {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`The database provider "${provider}" is not allowed on the Hobby tier. ` +
|
|
148
|
+
`Hobby tier is restricted to PostgreSQL and Supabase. ` +
|
|
149
|
+
`Please upgrade to a Developer Pro or Enterprise plan.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
} else if (tier === 'pro' || tier === 'developer_pro' || tier === 'premium' || tier === 'growth') {
|
|
153
|
+
// Developer Pro supports: PostgreSQL, Pinecone, Qdrant, MongoDB, Milvus, ChromaDB
|
|
154
|
+
const allowedPro = [
|
|
155
|
+
'postgresql', 'pgvector', 'supabase',
|
|
156
|
+
'pinecone', 'qdrant', 'mongodb', 'milvus',
|
|
157
|
+
'chroma', 'chromadb'
|
|
158
|
+
];
|
|
159
|
+
if (!allowedPro.includes(normalizedProvider)) {
|
|
160
|
+
throw new Error(
|
|
161
|
+
`The database provider "${provider}" is not allowed on the Developer Pro tier. ` +
|
|
162
|
+
`Please upgrade to an Enterprise plan.`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Enterprise has access to all databases (including redis, weaviate, custom ones)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (licenseKey) {
|
|
170
|
+
const cacheKey = `${licenseKey}:${currentProjectId}:${provider || ''}:${publicKeyOverride || ''}`;
|
|
171
|
+
this.cache.set(cacheKey, { payload, cachedAt: now });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return payload;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
177
|
+
throw new ConfigurationException(
|
|
178
|
+
`[Retrivora SDK] License key validation failed: ${message}`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
static async verifyIP(licenseKey: string | undefined): Promise<void> {
|
|
184
|
+
if (!licenseKey) return;
|
|
185
|
+
try {
|
|
186
|
+
const parts = licenseKey.split('.');
|
|
187
|
+
if (parts.length !== 3) return;
|
|
188
|
+
const [, payloadB64] = parts;
|
|
189
|
+
const payload: LicensePayload & { ipv4?: string; ipv6?: string } = JSON.parse(
|
|
190
|
+
Buffer.from(payloadB64, 'base64url').toString('utf8')
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const tier = (payload.tier || '').toLowerCase();
|
|
194
|
+
// Skip check if Enterprise or Custom
|
|
195
|
+
if (tier === 'enterprise' || tier === 'custom') {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// If Hobby (free_trial) or Developer Pro, perform the check
|
|
200
|
+
if (payload.ipv4 || payload.ipv6) {
|
|
201
|
+
let currentIpv4 = '';
|
|
202
|
+
let currentIpv6 = '';
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const res = await fetch('https://api4.ipify.org?format=json', { signal: AbortSignal.timeout(3000) });
|
|
206
|
+
const data = await res.json();
|
|
207
|
+
currentIpv4 = data.ip;
|
|
208
|
+
} catch (e) {}
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch('https://api6.ipify.org?format=json', { signal: AbortSignal.timeout(3000) });
|
|
212
|
+
const data = await res.json();
|
|
213
|
+
currentIpv6 = data.ip;
|
|
214
|
+
} catch (e) {}
|
|
215
|
+
|
|
216
|
+
const localIps: string[] = [];
|
|
217
|
+
try {
|
|
218
|
+
const osModule = require('os');
|
|
219
|
+
const interfaces = osModule.networkInterfaces();
|
|
220
|
+
for (const name of Object.keys(interfaces)) {
|
|
221
|
+
for (const iface of interfaces[name] || []) {
|
|
222
|
+
if (!iface.internal) {
|
|
223
|
+
localIps.push(iface.address);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (e) {}
|
|
228
|
+
|
|
229
|
+
const isIpv4Match = payload.ipv4 && (
|
|
230
|
+
currentIpv4 === payload.ipv4 ||
|
|
231
|
+
localIps.includes(payload.ipv4)
|
|
232
|
+
);
|
|
233
|
+
const isIpv6Match = payload.ipv6 && (
|
|
234
|
+
currentIpv6 === payload.ipv6 ||
|
|
235
|
+
localIps.includes(payload.ipv6)
|
|
236
|
+
);
|
|
237
|
+
|
|
238
|
+
if (payload.ipv4 && payload.ipv6) {
|
|
239
|
+
if (!isIpv4Match && !isIpv6Match) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`License key access restricted. This license key was created for a different system (authorized IPv4: ${payload.ipv4}, IPv6: ${payload.ipv6}) and cannot be used on this machine.`
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
} else if (payload.ipv4 && !isIpv4Match) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`License key access restricted. This license key was created for a different system (authorized IPv4: ${payload.ipv4}) and cannot be used on this machine.`
|
|
247
|
+
);
|
|
248
|
+
} else if (payload.ipv6 && !isIpv6Match) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`License key access restricted. This license key was created for a different system (authorized IPv6: ${payload.ipv6}) and cannot be used on this machine.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
} catch (err: any) {
|
|
255
|
+
if (err.message.includes('License key access restricted')) {
|
|
256
|
+
throw new ConfigurationException(`[Retrivora SDK] ${err.message}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -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
|
+
}
|