@retrivora-ai/rag-engine 1.9.3 → 1.9.7

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 (69) hide show
  1. package/README.md +59 -7
  2. package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
  3. package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +737 -237
  7. package/dist/handlers/index.mjs +736 -237
  8. package/dist/index-B9J_XEh0.d.ts +187 -0
  9. package/dist/index-BJ4cd-t5.d.mts +187 -0
  10. package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
  11. package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
  12. package/dist/index.css +237 -10
  13. package/dist/index.d.mts +13 -5
  14. package/dist/index.d.ts +13 -5
  15. package/dist/index.js +365 -164
  16. package/dist/index.mjs +350 -158
  17. package/dist/server.d.mts +15 -94
  18. package/dist/server.d.ts +15 -94
  19. package/dist/server.js +850 -239
  20. package/dist/server.mjs +839 -238
  21. package/package.json +2 -4
  22. package/src/app/api/chat/route.ts +2 -2
  23. package/src/app/api/health/route.ts +3 -2
  24. package/src/app/api/ingest/route.ts +3 -2
  25. package/src/app/api/suggestions/route.ts +3 -2
  26. package/src/app/api/upload/route.ts +3 -2
  27. package/src/app/constants.tsx +168 -148
  28. package/src/app/layout.tsx +5 -18
  29. package/src/app/types.ts +17 -17
  30. package/src/components/ChatWidget.tsx +3 -1
  31. package/src/components/ChatWindow.tsx +5 -1
  32. package/src/components/DocViewer.tsx +71 -5
  33. package/src/components/Documentation.tsx +74 -11
  34. package/src/components/MessageBubble.tsx +39 -2
  35. package/src/components/ThinkingBlock.tsx +75 -0
  36. package/src/components/VisualizationRenderer.tsx +27 -1
  37. package/src/components/constants.tsx +275 -0
  38. package/src/config/RagConfig.ts +47 -0
  39. package/src/config/constants.ts +1 -0
  40. package/src/config/serverConfig.ts +25 -0
  41. package/src/core/ConfigResolver.ts +73 -22
  42. package/src/core/ConfigValidator.ts +2 -1
  43. package/src/core/Pipeline.ts +226 -68
  44. package/src/core/ProviderRegistry.ts +16 -7
  45. package/src/core/QueryProcessor.ts +38 -4
  46. package/src/core/Retrivora.ts +91 -0
  47. package/src/core/VectorPlugin.ts +62 -8
  48. package/src/exceptions/index.ts +111 -0
  49. package/src/handlers/index.ts +73 -0
  50. package/src/hooks/useRagChat.ts +30 -5
  51. package/src/index.ts +27 -1
  52. package/src/lib/plugin.ts +24 -0
  53. package/src/llm/LLMFactory.ts +8 -4
  54. package/src/llm/providers/AnthropicProvider.ts +70 -20
  55. package/src/llm/providers/GeminiProvider.ts +14 -15
  56. package/src/llm/providers/OllamaProvider.ts +13 -16
  57. package/src/llm/providers/OpenAIProvider.ts +9 -14
  58. package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
  59. package/src/llm/utils.ts +46 -0
  60. package/src/providers/vectordb/MongoDBProvider.ts +9 -4
  61. package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
  62. package/src/rag/EntityExtractor.ts +2 -2
  63. package/src/rag/Reranker.ts +9 -16
  64. package/src/server.ts +30 -1
  65. package/src/types/chat.ts +9 -0
  66. package/src/types/props.ts +38 -1
  67. package/src/utils/UITransformer.ts +73 -4
  68. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  69. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
@@ -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 { wrapError, RetrivoraErrorCode } from '../exceptions';
8
9
 
9
10
  /**
10
11
  * VectorPlugin — main orchestrator class.
@@ -27,6 +28,8 @@ export class VectorPlugin {
27
28
 
28
29
  // Start validation early
29
30
  this.validationPromise = ConfigValidator.validateAndThrow(this.config);
31
+ // Prevent unhandled promise rejection warning
32
+ this.validationPromise.catch(() => {});
30
33
 
31
34
  this.pipeline = new Pipeline(this.config);
32
35
  }
@@ -70,31 +73,82 @@ export class VectorPlugin {
70
73
  * Run a chat query.
71
74
  */
72
75
  async chat(message: string, history: ChatMessage[] = [], namespace?: string): Promise<ChatResponse> {
73
- await this.validationPromise;
74
- return this.pipeline.ask(message, history, namespace);
76
+ try {
77
+ await this.validationPromise;
78
+ } catch (err) {
79
+ throw wrapError(err, 'CONFIGURATION_ERROR');
80
+ }
81
+ try {
82
+ return await this.pipeline.ask(message, history, namespace);
83
+ } catch (err) {
84
+ const msg = String(err);
85
+ let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
86
+ if (msg.includes('Embed') || msg.includes('embed')) {
87
+ defaultCode = 'EMBEDDING_FAILED';
88
+ }
89
+ throw wrapError(err, defaultCode);
90
+ }
75
91
  }
76
92
 
77
93
  /**
78
94
  * Run a streaming chat query.
79
95
  */
80
96
  async *chatStream(message: string, history: ChatMessage[] = [], namespace?: string) {
81
- await this.validationPromise;
82
- yield* this.pipeline.askStream(message, history, namespace);
97
+ try {
98
+ await this.validationPromise;
99
+ } catch (err) {
100
+ throw wrapError(err, 'CONFIGURATION_ERROR');
101
+ }
102
+ try {
103
+ const stream = this.pipeline.askStream(message, history, namespace);
104
+ for await (const chunk of stream) {
105
+ yield chunk;
106
+ }
107
+ } catch (err) {
108
+ const msg = String(err);
109
+ let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
110
+ if (msg.includes('Embed') || msg.includes('embed')) {
111
+ defaultCode = 'EMBEDDING_FAILED';
112
+ }
113
+ throw wrapError(err, defaultCode);
114
+ }
83
115
  }
84
116
 
85
117
  /**
86
118
  * Ingest documents into the vector database.
87
119
  */
88
120
  async ingest(documents: IngestDocument[], namespace?: string): Promise<Array<{ docId: string | number; chunksIngested: number }>> {
89
- await this.validationPromise;
90
- return this.pipeline.ingest(documents, namespace);
121
+ try {
122
+ await this.validationPromise;
123
+ } catch (err) {
124
+ throw wrapError(err, 'CONFIGURATION_ERROR');
125
+ }
126
+ try {
127
+ return await this.pipeline.ingest(documents, namespace);
128
+ } catch (err) {
129
+ const msg = String(err);
130
+ let defaultCode: RetrivoraErrorCode = 'RETRIEVAL_FAILED';
131
+ if (msg.includes('Embed') || msg.includes('embed')) {
132
+ defaultCode = 'EMBEDDING_FAILED';
133
+ }
134
+ throw wrapError(err, defaultCode);
135
+ }
91
136
  }
92
137
 
93
138
  /**
94
139
  * Get auto-suggestions based on a query prefix.
95
140
  */
96
141
  async getSuggestions(query: string, namespace?: string): Promise<string[]> {
97
- await this.validationPromise;
98
- return this.pipeline.getSuggestions(query, namespace);
142
+ try {
143
+ await this.validationPromise;
144
+ } catch (err) {
145
+ throw wrapError(err, 'CONFIGURATION_ERROR');
146
+ }
147
+ try {
148
+ return await this.pipeline.getSuggestions(query, namespace);
149
+ } catch (err) {
150
+ throw wrapError(err, 'RETRIEVAL_FAILED');
151
+ }
99
152
  }
100
153
  }
154
+
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Named SDK exceptions make failures machine-readable for host applications.
3
+ */
4
+
5
+ export type RetrivoraErrorCode =
6
+ | 'PROVIDER_NOT_FOUND'
7
+ | 'EMBEDDING_FAILED'
8
+ | 'RETRIEVAL_FAILED'
9
+ | 'RATE_LIMITED'
10
+ | 'CONFIGURATION_ERROR'
11
+ | 'AUTHENTICATION_ERROR';
12
+
13
+ export class RetrivoraError extends Error {
14
+ readonly code: RetrivoraErrorCode;
15
+ readonly details?: unknown;
16
+
17
+ constructor(message: string, code: RetrivoraErrorCode, details?: unknown) {
18
+ super(message);
19
+ this.name = new.target.name;
20
+ this.code = code;
21
+ this.details = details;
22
+ }
23
+ }
24
+
25
+ export class ProviderNotFoundException extends RetrivoraError {
26
+ constructor(providerType: string, provider: string, details?: unknown) {
27
+ super(`Unsupported ${providerType} provider: ${provider}`, 'PROVIDER_NOT_FOUND', details);
28
+ }
29
+ }
30
+
31
+ export class EmbeddingFailedException extends RetrivoraError {
32
+ constructor(message = 'Embedding generation failed', details?: unknown) {
33
+ super(message, 'EMBEDDING_FAILED', details);
34
+ }
35
+ }
36
+
37
+ export class RetrievalException extends RetrivoraError {
38
+ constructor(message = 'Retrieval failed', details?: unknown) {
39
+ super(message, 'RETRIEVAL_FAILED', details);
40
+ }
41
+ }
42
+
43
+ export class RateLimitException extends RetrivoraError {
44
+ constructor(message = 'Provider rate limit exceeded', details?: unknown) {
45
+ super(message, 'RATE_LIMITED', details);
46
+ }
47
+ }
48
+
49
+ export class ConfigurationException extends RetrivoraError {
50
+ constructor(message: string, details?: unknown) {
51
+ super(message, 'CONFIGURATION_ERROR', details);
52
+ }
53
+ }
54
+
55
+ export class AuthenticationException extends RetrivoraError {
56
+ constructor(message = 'Provider authentication failed', details?: unknown) {
57
+ super(message, 'AUTHENTICATION_ERROR', details);
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Wraps any unknown error into an appropriate RetrivoraError subclass.
63
+ */
64
+ export function wrapError(
65
+ err: unknown,
66
+ defaultCode: RetrivoraErrorCode,
67
+ defaultMessage?: string,
68
+ ): RetrivoraError {
69
+ if (err instanceof RetrivoraError) {
70
+ return err;
71
+ }
72
+
73
+ const error = err as any;
74
+ const message = error?.message || defaultMessage || String(err);
75
+ const status = error?.status || error?.statusCode || error?.response?.status;
76
+ const code = error?.code;
77
+
78
+ // 1. Rate Limit Recognition
79
+ if (status === 429 || /rate[- ]?limit/i.test(message) || code === 'RATE_LIMIT_EXCEEDED') {
80
+ return new RateLimitException(message, err);
81
+ }
82
+
83
+ // 2. Authentication Recognition
84
+ if (
85
+ status === 401 ||
86
+ status === 403 ||
87
+ /unauthorized|auth|api[- ]?key/i.test(message) ||
88
+ code === 'INVALID_API_KEY'
89
+ ) {
90
+ return new AuthenticationException(message, err);
91
+ }
92
+
93
+ // Fallback Mapping based on defaultCode
94
+ switch (defaultCode) {
95
+ case 'PROVIDER_NOT_FOUND':
96
+ return new ProviderNotFoundException('provider', message, err);
97
+ case 'EMBEDDING_FAILED':
98
+ return new EmbeddingFailedException(message, err);
99
+ case 'RETRIEVAL_FAILED':
100
+ return new RetrievalException(message, err);
101
+ case 'RATE_LIMITED':
102
+ return new RateLimitException(message, err);
103
+ case 'CONFIGURATION_ERROR':
104
+ return new ConfigurationException(message, err);
105
+ case 'AUTHENTICATION_ERROR':
106
+ return new AuthenticationException(message, err);
107
+ default:
108
+ return new RetrivoraError(message, defaultCode, err);
109
+ }
110
+ }
111
+
@@ -162,6 +162,8 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
162
162
  if (!isActive) break;
163
163
  if (typeof chunk === 'string') {
164
164
  enqueue(sseTextFrame(chunk));
165
+ } else if (chunk && typeof chunk === 'object' && 'type' in chunk && (chunk as any).type === 'thinking') {
166
+ enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
165
167
  } else {
166
168
  // Retrieval metadata object — always the final frame
167
169
  enqueue(sseMetaFrame(chunk));
@@ -411,5 +413,76 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
411
413
  };
412
414
  }
413
415
 
416
+ /**
417
+ * createRagHandler — factory that returns GET and POST handlers for a catch-all route
418
+ * (e.g. `src/app/api/retrivora/[[...retrivora]]/route.ts`).
419
+ */
420
+ export function createRagHandler(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
421
+ const plugin =
422
+ configOrPlugin instanceof VectorPlugin
423
+ ? configOrPlugin
424
+ : new VectorPlugin(configOrPlugin);
425
+
426
+ const chatHandler = createChatHandler(plugin);
427
+ const streamHandler = createStreamHandler(plugin);
428
+ const uploadHandler = createUploadHandler(plugin);
429
+ const healthHandler = createHealthHandler(plugin);
430
+ const suggestionsHandler = createSuggestionsHandler(plugin);
431
+
432
+ async function routePostRequest(req: NextRequest, segment: string) {
433
+ switch (segment) {
434
+ case 'chat':
435
+ return streamHandler(req);
436
+ case 'chat-sync':
437
+ return chatHandler(req);
438
+ case 'upload':
439
+ return uploadHandler(req);
440
+ case 'suggestions':
441
+ return suggestionsHandler(req);
442
+ case 'health':
443
+ return healthHandler();
444
+ default:
445
+ return NextResponse.json({ error: `Not Found: POST segment "${segment}" not supported.` }, { status: 404 });
446
+ }
447
+ }
448
+
449
+ async function routeGetRequest(req: NextRequest, segment: string) {
450
+ if (segment === 'health') {
451
+ return healthHandler();
452
+ }
453
+ return NextResponse.json({ error: `Method Not Allowed: GET is only supported for "health" segment.` }, { status: 405 });
454
+ }
455
+
456
+ async function getSegment(context: any): Promise<string> {
457
+ const resolvedParams = typeof context?.params?.then === 'function'
458
+ ? await context.params
459
+ : context?.params;
460
+
461
+ const segments: string[] = resolvedParams?.retrivora || [];
462
+ return segments[0] || 'chat';
463
+ }
464
+
465
+ return {
466
+ GET: async (req: NextRequest, context: any) => {
467
+ try {
468
+ const segment = await getSegment(context);
469
+ return await routeGetRequest(req, segment);
470
+ } catch (err) {
471
+ const msg = err instanceof Error ? err.message : 'GET Routing failed';
472
+ return NextResponse.json({ error: msg }, { status: 500 });
473
+ }
474
+ },
475
+ POST: async (req: NextRequest, context: any) => {
476
+ try {
477
+ const segment = await getSegment(context);
478
+ return await routePostRequest(req, segment);
479
+ } catch (err) {
480
+ const msg = err instanceof Error ? err.message : 'POST Routing failed';
481
+ return NextResponse.json({ error: msg }, { status: 500 });
482
+ }
483
+ }
484
+ };
485
+ }
486
+
414
487
  // Re-export SSE helpers so host apps can use them in custom handlers
415
488
  export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame, sseObservabilityFrame };
@@ -25,11 +25,12 @@ import { useStoredMessages } from './useStoredMessages';
25
25
  // ─── SSE Frame Parser ──────────────────────────────────────────────────────────
26
26
 
27
27
  interface SseTextFrame { type: 'text'; text: string }
28
- interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[] }
28
+ interface SseThinkingFrame { type: 'thinking'; text: string }
29
+ interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[]; thinkingMs?: number }
29
30
  interface SseErrorFrame { type: 'error'; error: string }
30
31
  interface SseUiFrame { type: 'ui_transformation'; data: unknown }
31
32
  interface SseObservabilityFrame { type: 'observability'; data: ObservabilityTrace }
32
- type SseFrame = SseTextFrame | SseMetaFrame | SseErrorFrame | SseUiFrame | SseObservabilityFrame;
33
+ type SseFrame = SseTextFrame | SseThinkingFrame | SseMetaFrame | SseErrorFrame | SseUiFrame | SseObservabilityFrame;
33
34
 
34
35
  /**
35
36
  * Parse all complete SSE frames from a raw chunk string.
@@ -114,7 +115,10 @@ export function useRagChat(
114
115
 
115
116
  const response = await fetch(apiUrl, {
116
117
  method: 'POST',
117
- headers: { 'Content-Type': 'application/json' },
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ ...(options.headers || {})
121
+ },
118
122
  signal: controller.signal,
119
123
  body: JSON.stringify({
120
124
  message: trimmed,
@@ -135,6 +139,8 @@ export function useRagChat(
135
139
  const reader = response.body.getReader();
136
140
  const decoder = new TextDecoder();
137
141
  let assistantContent = '';
142
+ let thinkingContent = '';
143
+ let thinkingMs: number | undefined;
138
144
  let sources: VectorMatch[] = [];
139
145
  let uiTransformation: unknown = null;
140
146
  let trace: ObservabilityTrace | undefined;
@@ -146,14 +152,20 @@ export function useRagChat(
146
152
  // O(N) ReactMarkdown re-parses where N = number of token chunks.
147
153
  let pendingFlush = false;
148
154
  const pendingContentRef = { current: '' };
155
+ const pendingThinkingRef = { current: '' };
149
156
 
150
157
  const flushTextUpdate = (msgId: string) => {
151
158
  pendingFlush = false;
152
159
  const snapshot = pendingContentRef.current;
160
+ const thinkingSnapshot = pendingThinkingRef.current;
153
161
  setMessages((prev) =>
154
162
  prev.map((msg) =>
155
163
  msg.id === msgId
156
- ? { ...msg, content: snapshot }
164
+ ? {
165
+ ...msg,
166
+ content: snapshot,
167
+ thinking: thinkingSnapshot || undefined,
168
+ }
157
169
  : msg
158
170
  )
159
171
  );
@@ -199,8 +211,13 @@ export function useRagChat(
199
211
  pendingContentRef.current = assistantContent;
200
212
  // Schedule a batched RAF flush instead of immediate setMessages
201
213
  scheduleFlush(assistantMessageId);
214
+ } else if (frame.type === 'thinking' && frame.text) {
215
+ thinkingContent += frame.text;
216
+ pendingThinkingRef.current = thinkingContent;
217
+ scheduleFlush(assistantMessageId);
202
218
  } else if (frame.type === 'metadata') {
203
219
  sources = frame.sources ?? [];
220
+ thinkingMs = (frame as any).thinkingMs;
204
221
  hasNonTextFrame = true;
205
222
  } else if (frame.type === 'ui_transformation') {
206
223
  uiTransformation = frame.data;
@@ -222,6 +239,8 @@ export function useRagChat(
222
239
  ? {
223
240
  ...msg,
224
241
  content: assistantContent,
242
+ thinking: thinkingContent || undefined,
243
+ thinkingMs: thinkingMs,
225
244
  sources: sources.length > 0 ? sources : msg.sources,
226
245
  uiTransformation: uiTransformation || (msg as RagMessage).uiTransformation,
227
246
  }
@@ -235,7 +254,11 @@ export function useRagChat(
235
254
  if (buffer.trim()) {
236
255
  for (const frame of parseSseChunk(buffer)) {
237
256
  if (frame.type === 'text' && frame.text) assistantContent += frame.text;
238
- else if (frame.type === 'metadata') sources = frame.sources ?? [];
257
+ else if (frame.type === 'thinking' && frame.text) thinkingContent += frame.text;
258
+ else if (frame.type === 'metadata') {
259
+ sources = frame.sources ?? [];
260
+ thinkingMs = (frame as any).thinkingMs;
261
+ }
239
262
  else if (frame.type === 'observability') trace = (frame as SseObservabilityFrame).data;
240
263
  }
241
264
  }
@@ -248,6 +271,8 @@ export function useRagChat(
248
271
  sources: sources.length > 0 ? sources : undefined,
249
272
  uiTransformation: uiTransformation ?? undefined,
250
273
  trace,
274
+ thinking: thinkingContent || undefined,
275
+ thinkingMs: thinkingMs,
251
276
  createdAt: new Date().toISOString(),
252
277
  };
253
278
  setMessages((prev) =>
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ 'use client';
2
+
1
3
  /**
2
4
  * Package entry point — public API exported to consumers of this npm package.
3
5
  * This entry point is CLIENT-SAFE and can be imported in React Client Components.
@@ -21,9 +23,33 @@ export { useRagChat } from './hooks/useRagChat';
21
23
  export { addSynonyms } from './utils/synonyms';
22
24
 
23
25
  // ── Types (Interfaces/Types only, no Node.js deps) ─────────────
24
- export type { RagConfig, VectorDBConfig, VectorDBProvider, LLMConfig, LLMProvider, EmbeddingConfig, EmbeddingProvider, UIConfig, RAGConfig } from './config/RagConfig';
26
+ export type {
27
+ RagConfig,
28
+ UniversalRagConfig,
29
+ RetrievalConfig,
30
+ WorkflowConfig,
31
+ VectorDBConfig,
32
+ VectorDBProvider,
33
+ LLMConfig,
34
+ LLMProvider,
35
+ EmbeddingConfig,
36
+ EmbeddingProvider,
37
+ UIConfig,
38
+ RAGConfig,
39
+ } from './config/RagConfig';
25
40
  export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
26
41
  export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
42
+ export type { Retrivora } from './core/Retrivora';
43
+ export type { RetrivoraErrorCode } from './exceptions';
44
+ export {
45
+ RetrivoraError,
46
+ ProviderNotFoundException,
47
+ EmbeddingFailedException,
48
+ RetrievalException,
49
+ RateLimitException,
50
+ ConfigurationException,
51
+ AuthenticationException,
52
+ } from './exceptions';
27
53
  export type {
28
54
  VectorMatch,
29
55
  UpsertDocument,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * lib/plugin.ts — Singleton VectorPlugin for this Next.js application.
3
+ *
4
+ * All API route handlers import this module to share ONE plugin instance
5
+ * (and therefore one pipeline + one DB connection pool) across the process.
6
+ *
7
+ * Why a singleton?
8
+ * - Prevents a new MongoDB/Postgres connection being opened on every request.
9
+ * - Ensures the LRU embedding cache is shared across all chat requests.
10
+ * - In serverless environments (Vercel) the module is cached per function
11
+ * instance, so cold-starts initialize once and stay warm.
12
+ *
13
+ * To swap provider at runtime, change .env.local and restart the dev server.
14
+ */
15
+
16
+ import { VectorPlugin } from '@/core/VectorPlugin';
17
+ import { getRagConfig } from '@/config/serverConfig';
18
+
19
+ // Build config once from environment variables at module load time.
20
+ const ragConfig = getRagConfig();
21
+
22
+ // Create and export the singleton.
23
+ // VectorPlugin is lazy — it doesn't open connections until the first request.
24
+ export const plugin = new VectorPlugin(ragConfig);
@@ -6,6 +6,7 @@ import { OllamaProvider } from './providers/OllamaProvider';
6
6
  import { GeminiProvider } from './providers/GeminiProvider';
7
7
  import { UniversalLLMAdapter } from './providers/UniversalLLMAdapter';
8
8
  import { IProviderValidator, IProviderHealthChecker } from '../core/ProviderInterfaces';
9
+ import { ProviderNotFoundException } from '../exceptions';
9
10
 
10
11
  interface LLMProviderStatic {
11
12
  getValidator?: () => IProviderValidator;
@@ -81,10 +82,13 @@ export class LLMFactory {
81
82
  if (llmConfig.baseUrl || (llmConfig.options as Record<string, unknown>)?.baseUrl) {
82
83
  return new UniversalLLMAdapter(llmConfig);
83
84
  }
84
- throw new Error(
85
- `[LLMFactory] Unknown provider "${llmConfig.provider}". ` +
86
- `Built-in providers: ${LLMFactory.listProviders().join(', ')}. ` +
87
- `Register a custom provider with LLMFactory.register().`
85
+ throw new ProviderNotFoundException(
86
+ 'llm',
87
+ llmConfig.provider ?? 'undefined',
88
+ {
89
+ message: `Unknown provider "${llmConfig.provider}". Register a custom provider with LLMFactory.register().`,
90
+ available: LLMFactory.listProviders()
91
+ }
88
92
  );
89
93
  }
90
94
  }
@@ -8,6 +8,7 @@ import { ChatMessage, ChatOptions } from '../../types';
8
8
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
9
9
  import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
10
10
  import { ValidationError } from '../../core/ConfigValidator';
11
+ import { buildSystemContent } from '../utils';
11
12
 
12
13
  export class AnthropicProvider implements ILLMProvider {
13
14
  private readonly client: Anthropic;
@@ -75,63 +76,112 @@ export class AnthropicProvider implements ILLMProvider {
75
76
  }
76
77
 
77
78
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
78
- const resolvedPrompt = options?.systemPrompt ??
79
+ const basePrompt =
80
+ options?.systemPrompt ??
79
81
  this.llmConfig.systemPrompt ??
80
- `You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
82
+ 'You are a helpful assistant. Use the context below to answer the user\'s question accurately.';
81
83
 
82
- const system = resolvedPrompt.includes('{{context}}')
83
- ? resolvedPrompt.replace('{{context}}', context)
84
- : options?.systemPrompt
85
- ? resolvedPrompt // per-call: context is already in the user message
86
- : `${resolvedPrompt}\n\nContext:\n${context}`;
84
+ const system = buildSystemContent(basePrompt, context);
87
85
 
88
86
  const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
89
87
  role: m.role === 'assistant' ? 'assistant' : 'user',
90
88
  content: m.content,
91
89
  }));
92
90
 
91
+ const isClaude37 = this.llmConfig.model.includes('claude-3-7-sonnet');
92
+ const isThinkingEnabled = this.llmConfig.options?.thinking === true ||
93
+ this.llmConfig.options?.thinking === 'enabled' ||
94
+ (isClaude37 && this.llmConfig.options?.thinking !== false);
95
+
96
+ const extraParams: any = {};
97
+ if (isThinkingEnabled && isClaude37) {
98
+ extraParams.betas = ['interleaved-thinking-2025-05-14'];
99
+ const maxTokens = options?.maxTokens ?? this.llmConfig.maxTokens ?? 4096;
100
+ const budget = Math.min(
101
+ typeof this.llmConfig.options?.thinkingBudget === 'number'
102
+ ? this.llmConfig.options?.thinkingBudget
103
+ : 2048,
104
+ maxTokens - 1
105
+ );
106
+ extraParams.thinking = {
107
+ type: 'enabled',
108
+ budget_tokens: budget,
109
+ };
110
+ extraParams.max_tokens = Math.max(maxTokens, budget + 1024);
111
+ }
112
+
93
113
  const response = await this.client.messages.create({
94
114
  model: this.llmConfig.model,
95
115
  max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
96
116
  system,
97
117
  messages: anthropicMessages,
98
- });
118
+ ...extraParams,
119
+ } as any);
99
120
 
100
- const block = response.content[0];
101
- return block.type === 'text' ? block.text : '';
121
+ let reply = '';
122
+ for (const block of response.content) {
123
+ if (block.type === 'text') {
124
+ reply += block.text;
125
+ }
126
+ }
127
+ return reply;
102
128
  }
103
129
 
104
130
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
105
- const resolvedPrompt = options?.systemPrompt ??
131
+ const basePrompt =
132
+ options?.systemPrompt ??
106
133
  this.llmConfig.systemPrompt ??
107
- `You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
134
+ 'You are a helpful assistant. Use the context below to answer the user\'s question accurately.';
108
135
 
109
- const system = resolvedPrompt.includes('{{context}}')
110
- ? resolvedPrompt.replace('{{context}}', context)
111
- : options?.systemPrompt
112
- ? resolvedPrompt
113
- : `${resolvedPrompt}\n\nContext:\n${context}`;
136
+ const system = buildSystemContent(basePrompt, context);
114
137
 
115
138
  const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
116
139
  role: m.role === 'assistant' ? 'assistant' : 'user',
117
140
  content: m.content,
118
141
  }));
119
142
 
143
+ const isClaude37 = this.llmConfig.model.includes('claude-3-7-sonnet');
144
+ const isThinkingEnabled = this.llmConfig.options?.thinking === true ||
145
+ this.llmConfig.options?.thinking === 'enabled' ||
146
+ (isClaude37 && this.llmConfig.options?.thinking !== false);
147
+
148
+ const extraParams: any = {};
149
+ if (isThinkingEnabled && isClaude37) {
150
+ extraParams.betas = ['interleaved-thinking-2025-05-14'];
151
+ const maxTokens = options?.maxTokens ?? this.llmConfig.maxTokens ?? 4096;
152
+ const budget = Math.min(
153
+ typeof this.llmConfig.options?.thinkingBudget === 'number'
154
+ ? this.llmConfig.options?.thinkingBudget
155
+ : 2048,
156
+ maxTokens - 1
157
+ );
158
+ extraParams.thinking = {
159
+ type: 'enabled',
160
+ budget_tokens: budget,
161
+ };
162
+ extraParams.max_tokens = Math.max(maxTokens, budget + 1024);
163
+ }
164
+
120
165
  const stream = await this.client.messages.create({
121
166
  model: this.llmConfig.model,
122
167
  max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
123
168
  system,
124
169
  messages: anthropicMessages,
125
170
  stream: true,
126
- });
171
+ ...extraParams,
172
+ } as any) as any;
127
173
 
128
174
  if (!stream) {
129
175
  throw new Error('[AnthropicProvider] messages.create stream is undefined');
130
176
  }
131
177
 
132
178
  for await (const chunk of stream) {
133
- if (chunk.type === 'content_block_delta' && chunk.delta.type === 'text_delta') {
134
- yield chunk.delta.text;
179
+ if (chunk.type === 'content_block_delta') {
180
+ if (chunk.delta.type === 'text_delta') {
181
+ yield chunk.delta.text;
182
+ } else if ((chunk.delta as any).type === 'thinking_delta') {
183
+ yield `\x00THINKING\x00${(chunk.delta as any).thinking}`;
184
+ }
135
185
  }
136
186
  }
137
187
  }