@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.
Files changed (62) hide show
  1. package/README.md +130 -113
  2. package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
  3. package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2376 -489
  7. package/dist/handlers/index.mjs +2372 -489
  8. package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
  9. package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
  10. package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
  11. package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
  12. package/dist/index.css +695 -203
  13. package/dist/index.d.mts +37 -7
  14. package/dist/index.d.ts +37 -7
  15. package/dist/index.js +1197 -286
  16. package/dist/index.mjs +1221 -297
  17. package/dist/server.d.mts +62 -6
  18. package/dist/server.d.ts +62 -6
  19. package/dist/server.js +2526 -574
  20. package/dist/server.mjs +2517 -573
  21. package/package.json +12 -10
  22. package/src/app/constants.tsx +207 -212
  23. package/src/app/layout.tsx +4 -28
  24. package/src/app/types.ts +17 -17
  25. package/src/components/AmbientBackground.tsx +10 -10
  26. package/src/components/ArchitectureCard.tsx +43 -7
  27. package/src/components/ArchitectureCardsSection.tsx +37 -4
  28. package/src/components/ChatWidget.tsx +4 -1
  29. package/src/components/ChatWindow.tsx +9 -2
  30. package/src/components/CodeViewer.tsx +19 -14
  31. package/src/components/DocViewer.tsx +75 -15
  32. package/src/components/Documentation.tsx +111 -28
  33. package/src/components/Hero.tsx +103 -20
  34. package/src/components/Lifecycle.tsx +65 -25
  35. package/src/components/MarkdownComponents.tsx +44 -1
  36. package/src/components/MessageBubble.tsx +162 -50
  37. package/src/components/constants.tsx +279 -0
  38. package/src/config/RagConfig.ts +56 -10
  39. package/src/config/constants.ts +5 -0
  40. package/src/config/serverConfig.ts +15 -0
  41. package/src/core/ConfigResolver.ts +30 -25
  42. package/src/core/DatabaseStorage.ts +469 -0
  43. package/src/core/LicenseVerifier.ts +154 -0
  44. package/src/core/MultiAgentCoordinator.ts +239 -0
  45. package/src/core/Pipeline.ts +148 -16
  46. package/src/core/ProviderRegistry.ts +5 -5
  47. package/src/core/Retrivora.ts +52 -6
  48. package/src/core/VectorPlugin.ts +74 -11
  49. package/src/core/mcp.ts +261 -0
  50. package/src/exceptions/index.ts +52 -0
  51. package/src/handlers/index.ts +504 -47
  52. package/src/hooks/useRagChat.ts +100 -43
  53. package/src/hooks/useStoredMessages.ts +15 -4
  54. package/src/index.ts +7 -0
  55. package/src/llm/LLMFactory.ts +15 -6
  56. package/src/llm/providers/GroqProvider.ts +176 -0
  57. package/src/llm/providers/QwenProvider.ts +191 -0
  58. package/src/server.ts +23 -13
  59. package/src/types/chat.ts +16 -0
  60. package/src/types/props.ts +50 -1
  61. package/.env.example +0 -80
  62. 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
+ }
@@ -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 Error(
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.agent) {
392
- const agentReply = await this.agent.run(question, history);
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 *askStream(question: string, history: ChatMessage[] = [], namespace?: string): AsyncIterable<string | ChatResponse> {
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 both UI transformation and hallucination score concurrently
754
- // before yielding the metadata frame. This guarantees the trace is always
755
- // 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
+
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 trace: ObservabilityTrace = {
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
- // Hallucination score is now always present when available
778
- ...(hallucinationResult ? {
779
- hallucinationScore: hallucinationResult.score,
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 (config: VectorDBConfig): BaseVectorProvider;
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) {
@@ -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
- await ConfigValidator.validateAndThrow(this.config);
24
- await this.pipeline.initialize();
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
- return this.pipeline.ingest(documents, namespace);
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
- return this.pipeline.ask(question, history, namespace);
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
- return this.pipeline.askStream(question, history, namespace);
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
+