@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.
- package/README.md +59 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +737 -237
- package/dist/handlers/index.mjs +736 -237
- package/dist/index-B9J_XEh0.d.ts +187 -0
- package/dist/index-BJ4cd-t5.d.mts +187 -0
- package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
- package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
- package/dist/index.css +237 -10
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +365 -164
- package/dist/index.mjs +350 -158
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +850 -239
- package/dist/server.mjs +839 -238
- package/package.json +2 -4
- package/src/app/api/chat/route.ts +2 -2
- package/src/app/api/health/route.ts +3 -2
- package/src/app/api/ingest/route.ts +3 -2
- package/src/app/api/suggestions/route.ts +3 -2
- package/src/app/api/upload/route.ts +3 -2
- package/src/app/constants.tsx +168 -148
- package/src/app/layout.tsx +5 -18
- package/src/app/types.ts +17 -17
- package/src/components/ChatWidget.tsx +3 -1
- package/src/components/ChatWindow.tsx +5 -1
- package/src/components/DocViewer.tsx +71 -5
- package/src/components/Documentation.tsx +74 -11
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/components/constants.tsx +275 -0
- package/src/config/RagConfig.ts +47 -0
- package/src/config/constants.ts +1 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +73 -22
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +16 -7
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +91 -0
- package/src/core/VectorPlugin.ts +62 -8
- package/src/exceptions/index.ts +111 -0
- package/src/handlers/index.ts +73 -0
- package/src/hooks/useRagChat.ts +30 -5
- package/src/index.ts +27 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +8 -4
- package/src/llm/providers/AnthropicProvider.ts +70 -20
- package/src/llm/providers/GeminiProvider.ts +14 -15
- package/src/llm/providers/OllamaProvider.ts +13 -16
- package/src/llm/providers/OpenAIProvider.ts +9 -14
- package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
- package/src/llm/utils.ts +46 -0
- package/src/providers/vectordb/MongoDBProvider.ts +9 -4
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
- package/src/rag/EntityExtractor.ts +2 -2
- package/src/rag/Reranker.ts +9 -16
- package/src/server.ts +30 -1
- package/src/types/chat.ts +9 -0
- package/src/types/props.ts +38 -1
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
package/src/core/VectorPlugin.ts
CHANGED
|
@@ -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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
+
|
package/src/handlers/index.ts
CHANGED
|
@@ -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 };
|
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -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
|
|
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: {
|
|
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
|
-
? {
|
|
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 === '
|
|
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 {
|
|
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);
|
package/src/llm/LLMFactory.ts
CHANGED
|
@@ -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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
79
|
+
const basePrompt =
|
|
80
|
+
options?.systemPrompt ??
|
|
79
81
|
this.llmConfig.systemPrompt ??
|
|
80
|
-
|
|
82
|
+
'You are a helpful assistant. Use the context below to answer the user\'s question accurately.';
|
|
81
83
|
|
|
82
|
-
const system =
|
|
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
|
-
|
|
101
|
-
|
|
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
|
|
131
|
+
const basePrompt =
|
|
132
|
+
options?.systemPrompt ??
|
|
106
133
|
this.llmConfig.systemPrompt ??
|
|
107
|
-
|
|
134
|
+
'You are a helpful assistant. Use the context below to answer the user\'s question accurately.';
|
|
108
135
|
|
|
109
|
-
const system =
|
|
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'
|
|
134
|
-
|
|
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
|
}
|