@retrivora-ai/rag-engine 1.9.7 → 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.
Files changed (58) hide show
  1. package/README.md +127 -135
  2. package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-B8ROITYK.d.mts} +57 -2
  3. package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-B8ROITYK.d.ts} +57 -2
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2198 -457
  7. package/dist/handlers/index.mjs +2195 -457
  8. package/dist/{index-BJ4cd-t5.d.mts → index-B8PbEFSY.d.mts} +12 -2
  9. package/dist/{index-Bu7T6xgr.d.ts → index-BCPKUAVL.d.ts} +27 -52
  10. package/dist/{index-C3SVtPYg.d.mts → index-CrxCy36A.d.mts} +27 -52
  11. package/dist/{index-B9J_XEh0.d.ts → index-DNvoi-sV.d.ts} +12 -2
  12. package/dist/index.css +578 -273
  13. package/dist/index.d.mts +29 -7
  14. package/dist/index.d.ts +29 -7
  15. package/dist/index.js +1160 -282
  16. package/dist/index.mjs +1185 -292
  17. package/dist/server.d.mts +62 -6
  18. package/dist/server.d.ts +62 -6
  19. package/dist/server.js +2061 -306
  20. package/dist/server.mjs +2055 -306
  21. package/package.json +11 -7
  22. package/src/app/constants.tsx +37 -7
  23. package/src/components/AmbientBackground.tsx +10 -10
  24. package/src/components/ArchitectureCard.tsx +43 -7
  25. package/src/components/ArchitectureCardsSection.tsx +37 -4
  26. package/src/components/ChatWidget.tsx +2 -1
  27. package/src/components/ChatWindow.tsx +4 -1
  28. package/src/components/CodeViewer.tsx +19 -14
  29. package/src/components/DocViewer.tsx +43 -49
  30. package/src/components/Documentation.tsx +71 -51
  31. package/src/components/Hero.tsx +103 -20
  32. package/src/components/Lifecycle.tsx +65 -25
  33. package/src/components/MarkdownComponents.tsx +44 -1
  34. package/src/components/MessageBubble.tsx +162 -50
  35. package/src/components/constants.tsx +4 -0
  36. package/src/config/RagConfig.ts +46 -0
  37. package/src/config/constants.ts +4 -0
  38. package/src/config/serverConfig.ts +15 -0
  39. package/src/core/ConfigResolver.ts +6 -0
  40. package/src/core/DatabaseStorage.ts +469 -0
  41. package/src/core/LicenseVerifier.ts +154 -0
  42. package/src/core/MultiAgentCoordinator.ts +239 -0
  43. package/src/core/Pipeline.ts +146 -15
  44. package/src/core/Retrivora.ts +6 -0
  45. package/src/core/VectorPlugin.ts +12 -3
  46. package/src/core/mcp.ts +261 -0
  47. package/src/handlers/index.ts +449 -63
  48. package/src/hooks/useRagChat.ts +96 -42
  49. package/src/hooks/useStoredMessages.ts +15 -4
  50. package/src/index.ts +5 -0
  51. package/src/llm/LLMFactory.ts +9 -1
  52. package/src/llm/providers/GroqProvider.ts +176 -0
  53. package/src/llm/providers/QwenProvider.ts +191 -0
  54. package/src/server.ts +7 -0
  55. package/src/types/chat.ts +14 -0
  56. package/src/types/props.ts +12 -0
  57. package/.env.example +0 -80
  58. 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
+ }
@@ -9,6 +9,8 @@ import { Reranker } from '../rag/Reranker';
9
9
  import { LlamaIndexIngestor } from '../rag/LlamaIndexIngestor';
10
10
  import { LangChainAgent } from './LangChainAgent';
11
11
  import { RagConfig } from '../config/RagConfig';
12
+ import { MCPRegistry } from './mcp';
13
+ import { MultiAgentCoordinator } from './MultiAgentCoordinator';
12
14
  import { ProviderRegistry } from './ProviderRegistry';
13
15
  import { BatchProcessor, BatchOptions } from './BatchProcessor';
14
16
  import { EmbeddingStrategyResolver } from '../config/EmbeddingStrategy';
@@ -165,9 +167,13 @@ export class Pipeline {
165
167
  private entityExtractor?: EntityExtractor;
166
168
  private reranker: Reranker;
167
169
  private agent?: LangChainAgent;
170
+ private mcpRegistry?: MCPRegistry;
171
+ private multiAgentCoordinator?: MultiAgentCoordinator;
168
172
  /** LRU-bounded cache: avoids re-embedding identical queries within the same process. */
169
173
  private embeddingCache = new LRUEmbeddingCache(500);
170
174
  private initialised = false;
175
+ /** Namespace-specific static cold context cache for CAG */
176
+ private coldContexts = new Map<string, string>();
171
177
 
172
178
  constructor(private config: RagConfig) {
173
179
  this.chunker = new DocumentChunker(
@@ -250,14 +256,52 @@ You are a helpful assistant. Use the provided context to answer questions accura
250
256
 
251
257
  await this.vectorDB.initialize();
252
258
 
253
- 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
+ });
254
268
  this.agent = new LangChainAgent(this, this.config);
255
269
  await this.agent.initialize(this.llmProvider as unknown);
256
270
  }
257
271
 
272
+ if (this.config.rag?.cag?.enabled) {
273
+ await this.loadColdContext(this.config.projectId);
274
+ }
275
+
258
276
  this.initialised = true;
259
277
  }
260
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
+
261
305
  /**
262
306
  * Ingest documents with automatic chunking, embedding, and batch upsert.
263
307
  */
@@ -386,12 +430,26 @@ You are a helpful assistant. Use the provided context to answer questions accura
386
430
  );
387
431
  }
388
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
+
389
448
  async ask(question: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
390
449
  await this.initialize();
391
450
 
392
- if (this.config.rag?.architecture === 'agentic' && this.agent) {
393
- const agentReply = await this.agent.run(question, history);
394
- 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);
395
453
  }
396
454
 
397
455
  const stream = this.askStream(question, history, namespace);
@@ -415,6 +473,21 @@ You are a helpful assistant. Use the provided context to answer questions accura
415
473
  return { reply, sources, graphData, ui_transformation: uiTransformation, trace };
416
474
  }
417
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
+
418
491
  /**
419
492
  * High-performance streaming RAG flow.
420
493
  * Yields text chunks first, then the retrieval metadata + observability trace at the end.
@@ -425,7 +498,7 @@ You are a helpful assistant. Use the provided context to answer questions accura
425
498
  * - UITransformation is computed after text streaming and emitted with metadata
426
499
  * - SchemaMapper.train runs while answer generation streams
427
500
  */
428
- async *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
501
+ async *askStreamInternal(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
429
502
  await this.initialize();
430
503
  const ns = namespace ?? this.config.projectId;
431
504
  const topK = this.config.rag?.topK ?? 5;
@@ -547,6 +620,28 @@ You are a helpful assistant. Use the provided context to answer questions accura
547
620
  context = `GRAPH KNOWLEDGE:\n${graphContext}\n\nVECTOR CONTEXT:\n${context}`;
548
621
  }
549
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
+
550
645
  // 4.5 Schema Training — true fire-and-forget, never gates anything.
551
646
  const allMetadataKeys = Array.from(new Set(sources.flatMap(s => Object.keys(s.metadata || {}))));
552
647
  const cachedSchema = allMetadataKeys.length > 0
@@ -751,15 +846,15 @@ You are a helpful assistant. Use the provided context to answer questions accura
751
846
  estimatedCostUsd: estimateCostUsd(promptTokens, completionTokens, this.config.llm.model),
752
847
  };
753
848
 
754
- // 7. Await both UI transformation and hallucination score concurrently
755
- // before yielding the metadata frame. This guarantees the trace is always
756
- // complete when the consumer receives it — no post-yield mutation needed.
849
+ // 7. Await UI transformation and optionally hallucination score before yielding the metadata frame.
850
+ const awaitHallucination = this.config.llm.options?.awaitHallucination === true;
851
+
757
852
  const [uiTransformation, hallucinationResult] = await Promise.all([
758
853
  uiTransformationPromise,
759
- hallucinationScoringPromise,
854
+ awaitHallucination ? hallucinationScoringPromise : Promise.resolve(undefined),
760
855
  ]);
761
856
 
762
- const trace: ObservabilityTrace = {
857
+ const buildTrace = (hScore?: { score: number; reason: string }): ObservabilityTrace => ({
763
858
  requestId,
764
859
  query: question,
765
860
  rewrittenQuery,
@@ -775,12 +870,48 @@ You are a helpful assistant. Use the provided context to answer questions accura
775
870
  latency,
776
871
  tokens,
777
872
  timestamp: new Date().toISOString(),
778
- // Hallucination score is now always present when available
779
- ...(hallucinationResult ? {
780
- hallucinationScore: hallucinationResult.score,
781
- hallucinationReason: hallucinationResult.reason,
873
+ ...(hScore ? {
874
+ hallucinationScore: hScore.score,
875
+ hallucinationReason: hScore.reason,
782
876
  } : {}),
783
- };
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
+ }
784
915
 
785
916
  yield {
786
917
  reply: '',
@@ -2,6 +2,7 @@ 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';
6
7
  import { wrapError, RetrivoraErrorCode } from '../exceptions';
7
8
 
@@ -23,6 +24,11 @@ export class Retrivora {
23
24
  async initialize(): Promise<void> {
24
25
  try {
25
26
  await ConfigValidator.validateAndThrow(this.config);
27
+ LicenseVerifier.verify(
28
+ this.config.licenseKey,
29
+ this.config.projectId,
30
+ this.config.vectorDb?.provider
31
+ );
26
32
  } catch (err) {
27
33
  throw wrapError(err, 'CONFIGURATION_ERROR');
28
34
  }
@@ -5,6 +5,7 @@ import { Pipeline } from './Pipeline';
5
5
  import { ProviderHealthCheck, HealthCheckResult } from './ProviderHealthCheck';
6
6
  import { IngestDocument, ChatResponse } from '../types';
7
7
  import { ChatMessage } from '../types';
8
+ import { LicenseVerifier } from './LicenseVerifier';
8
9
  import { wrapError, RetrivoraErrorCode } from '../exceptions';
9
10
 
10
11
  /**
@@ -24,14 +25,22 @@ export class VectorPlugin {
24
25
  private validationPromise: Promise<void>;
25
26
 
26
27
  constructor(hostConfig?: Partial<RagConfig>) {
27
- this.config = ConfigResolver.resolve(hostConfig);
28
+ const resolvedConfig = ConfigResolver.resolve(hostConfig);
29
+ this.config = resolvedConfig;
28
30
 
29
31
  // Start validation early
30
- this.validationPromise = ConfigValidator.validateAndThrow(this.config);
32
+ this.validationPromise = (async () => {
33
+ await ConfigValidator.validateAndThrow(resolvedConfig);
34
+ LicenseVerifier.verify(
35
+ resolvedConfig.licenseKey,
36
+ resolvedConfig.projectId,
37
+ resolvedConfig.vectorDb?.provider
38
+ );
39
+ })();
31
40
  // Prevent unhandled promise rejection warning
32
41
  this.validationPromise.catch(() => {});
33
42
 
34
- this.pipeline = new Pipeline(this.config);
43
+ this.pipeline = new Pipeline(resolvedConfig);
35
44
  }
36
45
 
37
46
  /**