@retrivora-ai/rag-engine 1.9.2 → 1.9.6
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 +27 -0
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-DNhyOYoK.d.mts} +41 -1
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-DNhyOYoK.d.ts} +41 -1
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +563 -205
- package/dist/handlers/index.mjs +563 -205
- package/dist/index-C9v7-tWd.d.mts +183 -0
- package/dist/{index-B70ZLkfG.d.mts → index-CHL1jdYm.d.mts} +1 -1
- package/dist/index-CjQdL0cX.d.ts +183 -0
- package/dist/{index-DVu-mkAM.d.ts → index-Hgbwl9X4.d.ts} +1 -1
- package/dist/index.css +40 -0
- package/dist/index.d.mts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +326 -158
- package/dist/index.mjs +312 -151
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +623 -205
- package/dist/server.mjs +615 -205
- package/package.json +1 -1
- 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 +85 -30
- package/src/app/layout.tsx +18 -7
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/config/RagConfig.ts +47 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +56 -4
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +11 -2
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +51 -0
- package/src/exceptions/index.ts +59 -0
- package/src/handlers/index.ts +2 -0
- package/src/hooks/useRagChat.ts +26 -4
- package/src/index.ts +25 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +4 -1
- 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 +19 -7
- 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 +27 -1
- package/src/types/chat.ts +7 -0
- 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/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.
|
|
@@ -135,6 +136,8 @@ export function useRagChat(
|
|
|
135
136
|
const reader = response.body.getReader();
|
|
136
137
|
const decoder = new TextDecoder();
|
|
137
138
|
let assistantContent = '';
|
|
139
|
+
let thinkingContent = '';
|
|
140
|
+
let thinkingMs: number | undefined;
|
|
138
141
|
let sources: VectorMatch[] = [];
|
|
139
142
|
let uiTransformation: unknown = null;
|
|
140
143
|
let trace: ObservabilityTrace | undefined;
|
|
@@ -146,14 +149,20 @@ export function useRagChat(
|
|
|
146
149
|
// O(N) ReactMarkdown re-parses where N = number of token chunks.
|
|
147
150
|
let pendingFlush = false;
|
|
148
151
|
const pendingContentRef = { current: '' };
|
|
152
|
+
const pendingThinkingRef = { current: '' };
|
|
149
153
|
|
|
150
154
|
const flushTextUpdate = (msgId: string) => {
|
|
151
155
|
pendingFlush = false;
|
|
152
156
|
const snapshot = pendingContentRef.current;
|
|
157
|
+
const thinkingSnapshot = pendingThinkingRef.current;
|
|
153
158
|
setMessages((prev) =>
|
|
154
159
|
prev.map((msg) =>
|
|
155
160
|
msg.id === msgId
|
|
156
|
-
? {
|
|
161
|
+
? {
|
|
162
|
+
...msg,
|
|
163
|
+
content: snapshot,
|
|
164
|
+
thinking: thinkingSnapshot || undefined,
|
|
165
|
+
}
|
|
157
166
|
: msg
|
|
158
167
|
)
|
|
159
168
|
);
|
|
@@ -199,8 +208,13 @@ export function useRagChat(
|
|
|
199
208
|
pendingContentRef.current = assistantContent;
|
|
200
209
|
// Schedule a batched RAF flush instead of immediate setMessages
|
|
201
210
|
scheduleFlush(assistantMessageId);
|
|
211
|
+
} else if (frame.type === 'thinking' && frame.text) {
|
|
212
|
+
thinkingContent += frame.text;
|
|
213
|
+
pendingThinkingRef.current = thinkingContent;
|
|
214
|
+
scheduleFlush(assistantMessageId);
|
|
202
215
|
} else if (frame.type === 'metadata') {
|
|
203
216
|
sources = frame.sources ?? [];
|
|
217
|
+
thinkingMs = (frame as any).thinkingMs;
|
|
204
218
|
hasNonTextFrame = true;
|
|
205
219
|
} else if (frame.type === 'ui_transformation') {
|
|
206
220
|
uiTransformation = frame.data;
|
|
@@ -222,6 +236,8 @@ export function useRagChat(
|
|
|
222
236
|
? {
|
|
223
237
|
...msg,
|
|
224
238
|
content: assistantContent,
|
|
239
|
+
thinking: thinkingContent || undefined,
|
|
240
|
+
thinkingMs: thinkingMs,
|
|
225
241
|
sources: sources.length > 0 ? sources : msg.sources,
|
|
226
242
|
uiTransformation: uiTransformation || (msg as RagMessage).uiTransformation,
|
|
227
243
|
}
|
|
@@ -235,7 +251,11 @@ export function useRagChat(
|
|
|
235
251
|
if (buffer.trim()) {
|
|
236
252
|
for (const frame of parseSseChunk(buffer)) {
|
|
237
253
|
if (frame.type === 'text' && frame.text) assistantContent += frame.text;
|
|
238
|
-
else if (frame.type === '
|
|
254
|
+
else if (frame.type === 'thinking' && frame.text) thinkingContent += frame.text;
|
|
255
|
+
else if (frame.type === 'metadata') {
|
|
256
|
+
sources = frame.sources ?? [];
|
|
257
|
+
thinkingMs = (frame as any).thinkingMs;
|
|
258
|
+
}
|
|
239
259
|
else if (frame.type === 'observability') trace = (frame as SseObservabilityFrame).data;
|
|
240
260
|
}
|
|
241
261
|
}
|
|
@@ -248,6 +268,8 @@ export function useRagChat(
|
|
|
248
268
|
sources: sources.length > 0 ? sources : undefined,
|
|
249
269
|
uiTransformation: uiTransformation ?? undefined,
|
|
250
270
|
trace,
|
|
271
|
+
thinking: thinkingContent || undefined,
|
|
272
|
+
thinkingMs: thinkingMs,
|
|
251
273
|
createdAt: new Date().toISOString(),
|
|
252
274
|
};
|
|
253
275
|
setMessages((prev) =>
|
package/src/index.ts
CHANGED
|
@@ -21,9 +21,33 @@ export { useRagChat } from './hooks/useRagChat';
|
|
|
21
21
|
export { addSynonyms } from './utils/synonyms';
|
|
22
22
|
|
|
23
23
|
// ── Types (Interfaces/Types only, no Node.js deps) ─────────────
|
|
24
|
-
export type {
|
|
24
|
+
export type {
|
|
25
|
+
RagConfig,
|
|
26
|
+
UniversalRagConfig,
|
|
27
|
+
RetrievalConfig,
|
|
28
|
+
WorkflowConfig,
|
|
29
|
+
VectorDBConfig,
|
|
30
|
+
VectorDBProvider,
|
|
31
|
+
LLMConfig,
|
|
32
|
+
LLMProvider,
|
|
33
|
+
EmbeddingConfig,
|
|
34
|
+
EmbeddingProvider,
|
|
35
|
+
UIConfig,
|
|
36
|
+
RAGConfig,
|
|
37
|
+
} from './config/RagConfig';
|
|
25
38
|
export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
|
|
26
39
|
export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
|
|
40
|
+
export type { Retrivora } from './core/Retrivora';
|
|
41
|
+
export type { RetrivoraErrorCode } from './exceptions';
|
|
42
|
+
export {
|
|
43
|
+
RetrivoraError,
|
|
44
|
+
ProviderNotFoundException,
|
|
45
|
+
EmbeddingFailedException,
|
|
46
|
+
RetrievalException,
|
|
47
|
+
RateLimitException,
|
|
48
|
+
ConfigurationException,
|
|
49
|
+
AuthenticationException,
|
|
50
|
+
} from './exceptions';
|
|
27
51
|
export type {
|
|
28
52
|
VectorMatch,
|
|
29
53
|
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,7 +82,9 @@ 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
|
+
throw new ProviderNotFoundException(
|
|
86
|
+
'LLM',
|
|
87
|
+
String(llmConfig.provider),
|
|
85
88
|
`[LLMFactory] Unknown provider "${llmConfig.provider}". ` +
|
|
86
89
|
`Built-in providers: ${LLMFactory.listProviders().join(', ')}. ` +
|
|
87
90
|
`Register a custom provider with LLMFactory.register().`
|
|
@@ -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
|
}
|
|
@@ -20,6 +20,7 @@ import { ChatMessage, ChatOptions } from '../../types';
|
|
|
20
20
|
import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
|
|
21
21
|
import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
|
|
22
22
|
import { ValidationError } from '../../core/ConfigValidator';
|
|
23
|
+
import { buildSystemContent } from '../utils';
|
|
23
24
|
|
|
24
25
|
const DEFAULT_EMBEDDING_MODEL = 'text-embedding-004';
|
|
25
26
|
const DEFAULT_TEMPERATURE = 0.7;
|
|
@@ -165,18 +166,6 @@ export class GeminiProvider implements ILLMProvider {
|
|
|
165
166
|
return sanitizeModel(optionsModel ?? this.embeddingConfig?.model ?? DEFAULT_EMBEDDING_MODEL);
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
/**
|
|
169
|
-
* Build the system instruction string, inserting the RAG context either via
|
|
170
|
-
* the `{{context}}` placeholder or by appending it.
|
|
171
|
-
*/
|
|
172
|
-
private buildSystemInstruction(context: string): string {
|
|
173
|
-
const base =
|
|
174
|
-
this.llmConfig.systemPrompt ??
|
|
175
|
-
`You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
|
|
176
|
-
|
|
177
|
-
return base.includes('{{context}}') ? base.replace('{{context}}', context) : `${base}\n\nContext:\n${context}`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
169
|
/**
|
|
181
170
|
* Convert ChatMessage[] to the Gemini contents format.
|
|
182
171
|
*
|
|
@@ -199,9 +188,14 @@ export class GeminiProvider implements ILLMProvider {
|
|
|
199
188
|
// -------------------------------------------------------------------------
|
|
200
189
|
|
|
201
190
|
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
191
|
+
const basePrompt =
|
|
192
|
+
options?.systemPrompt ??
|
|
193
|
+
this.llmConfig.systemPrompt ??
|
|
194
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
195
|
+
|
|
202
196
|
const model = this.client.getGenerativeModel({
|
|
203
197
|
model: this.llmConfig.model,
|
|
204
|
-
systemInstruction:
|
|
198
|
+
systemInstruction: buildSystemContent(basePrompt, context),
|
|
205
199
|
});
|
|
206
200
|
|
|
207
201
|
const result = await model.generateContent({
|
|
@@ -217,9 +211,14 @@ export class GeminiProvider implements ILLMProvider {
|
|
|
217
211
|
}
|
|
218
212
|
|
|
219
213
|
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
214
|
+
const basePrompt =
|
|
215
|
+
options?.systemPrompt ??
|
|
216
|
+
this.llmConfig.systemPrompt ??
|
|
217
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
218
|
+
|
|
220
219
|
const model = this.client.getGenerativeModel({
|
|
221
220
|
model: this.llmConfig.model,
|
|
222
|
-
systemInstruction:
|
|
221
|
+
systemInstruction: buildSystemContent(basePrompt, context),
|
|
223
222
|
});
|
|
224
223
|
|
|
225
224
|
const result = await model.generateContentStream({
|
|
@@ -309,4 +308,4 @@ export class GeminiProvider implements ILLMProvider {
|
|
|
309
308
|
return false;
|
|
310
309
|
}
|
|
311
310
|
}
|
|
312
|
-
|
|
311
|
+
}
|
|
@@ -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
|
interface OllamaChatResponse {
|
|
13
14
|
message: { content: string };
|
|
@@ -24,7 +25,8 @@ export class OllamaProvider implements ILLMProvider {
|
|
|
24
25
|
|
|
25
26
|
constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
|
|
26
27
|
const baseURL = llmConfig.baseUrl ?? 'http://localhost:11434';
|
|
27
|
-
|
|
28
|
+
const timeout = Number(llmConfig.options?.timeout) || 300_000;
|
|
29
|
+
this.http = axios.create({ baseURL, timeout });
|
|
28
30
|
this.llmConfig = llmConfig;
|
|
29
31
|
this.embeddingConfig = embeddingConfig;
|
|
30
32
|
}
|
|
@@ -85,7 +87,11 @@ export class OllamaProvider implements ILLMProvider {
|
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
88
|
-
const
|
|
90
|
+
const basePrompt =
|
|
91
|
+
options?.systemPrompt ??
|
|
92
|
+
this.llmConfig.systemPrompt ??
|
|
93
|
+
'You are a helpful assistant. Use the provided context to answer the user\'s question.';
|
|
94
|
+
const system = buildSystemContent(basePrompt, context);
|
|
89
95
|
|
|
90
96
|
const { data } = await this.http.post<OllamaChatResponse>('/api/chat', {
|
|
91
97
|
model: this.llmConfig.model,
|
|
@@ -104,7 +110,11 @@ export class OllamaProvider implements ILLMProvider {
|
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
107
|
-
const
|
|
113
|
+
const basePrompt =
|
|
114
|
+
options?.systemPrompt ??
|
|
115
|
+
this.llmConfig.systemPrompt ??
|
|
116
|
+
'You are a helpful assistant. Use the provided context to answer the user\'s question.';
|
|
117
|
+
const system = buildSystemContent(basePrompt, context);
|
|
108
118
|
|
|
109
119
|
const response = await this.http.post('/api/chat', {
|
|
110
120
|
model: this.llmConfig.model,
|
|
@@ -159,19 +169,6 @@ export class OllamaProvider implements ILLMProvider {
|
|
|
159
169
|
}
|
|
160
170
|
}
|
|
161
171
|
|
|
162
|
-
private buildSystemPrompt(context: string, overridePrompt?: string): string {
|
|
163
|
-
if (overridePrompt) {
|
|
164
|
-
// Per-call override: use as-is (context is already embedded in user message)
|
|
165
|
-
return overridePrompt;
|
|
166
|
-
}
|
|
167
|
-
const systemPrompt =
|
|
168
|
-
this.llmConfig.systemPrompt ??
|
|
169
|
-
`You are a helpful assistant. Use the provided context to answer the user's question.\n\nContext:\n${context}`;
|
|
170
|
-
|
|
171
|
-
return systemPrompt.includes('{{context}}')
|
|
172
|
-
? systemPrompt.replace('{{context}}', context)
|
|
173
|
-
: `${systemPrompt}\n\nContext:\n${context}`;
|
|
174
|
-
}
|
|
175
172
|
|
|
176
173
|
async embed(text: string, options?: EmbedOptions): Promise<number[]> {
|
|
177
174
|
const model =
|
|
@@ -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 OpenAIProvider implements ILLMProvider {
|
|
13
14
|
private readonly client: OpenAI;
|
|
@@ -80,17 +81,14 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
83
|
-
const
|
|
84
|
+
const basePrompt =
|
|
85
|
+
options?.systemPrompt ??
|
|
84
86
|
this.llmConfig.systemPrompt ??
|
|
85
|
-
|
|
87
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
86
88
|
|
|
87
89
|
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
88
90
|
role: 'system',
|
|
89
|
-
content:
|
|
90
|
-
? resolvedSystemPrompt.replace('{{context}}', context)
|
|
91
|
-
: options?.systemPrompt
|
|
92
|
-
? resolvedSystemPrompt // per-call override: use as-is, context already in user message
|
|
93
|
-
: `${resolvedSystemPrompt}\n\nContext:\n${context}`,
|
|
91
|
+
content: buildSystemContent(basePrompt, context),
|
|
94
92
|
};
|
|
95
93
|
|
|
96
94
|
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
@@ -113,17 +111,14 @@ export class OpenAIProvider implements ILLMProvider {
|
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
116
|
-
const
|
|
114
|
+
const basePrompt =
|
|
115
|
+
options?.systemPrompt ??
|
|
117
116
|
this.llmConfig.systemPrompt ??
|
|
118
|
-
|
|
117
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
119
118
|
|
|
120
119
|
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
121
120
|
role: 'system',
|
|
122
|
-
content:
|
|
123
|
-
? resolvedSystemPrompt.replace('{{context}}', context)
|
|
124
|
-
: options?.systemPrompt
|
|
125
|
-
? resolvedSystemPrompt
|
|
126
|
-
: `${resolvedSystemPrompt}\n\nContext:\n${context}`,
|
|
121
|
+
content: buildSystemContent(basePrompt, context),
|
|
127
122
|
};
|
|
128
123
|
|
|
129
124
|
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
@@ -31,20 +31,20 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
31
31
|
|
|
32
32
|
constructor(config: LLMConfig | EmbeddingConfig) {
|
|
33
33
|
this.model = config.model;
|
|
34
|
-
|
|
34
|
+
|
|
35
35
|
// Type narrow to extract LLM-specific fields safely
|
|
36
36
|
const llmConfig = config as LLMConfig;
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
// Merge profile defaults if specified in options
|
|
39
39
|
const options = (llmConfig.options as Record<string, unknown>) ?? {};
|
|
40
40
|
let profile: Record<string, unknown> = {};
|
|
41
|
-
|
|
41
|
+
|
|
42
42
|
if (typeof options.profile === 'string') {
|
|
43
43
|
profile = (LLM_PROFILES as Record<string, Record<string, unknown>>)[options.profile] || {};
|
|
44
44
|
} else if (typeof options.profile === 'object' && options.profile !== null) {
|
|
45
45
|
profile = options.profile as Record<string, unknown>;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
this.opts = { ...profile, ...(llmConfig.options ?? {}) } as UniversalLLMOptions;
|
|
49
49
|
this.systemPrompt = llmConfig.systemPrompt ?? 'You are a helpful AI assistant. Use the provided context to answer the user.';
|
|
50
50
|
this.maxTokens = llmConfig.maxTokens ?? 1024;
|
|
@@ -71,7 +71,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
|
|
|
71
71
|
|
|
72
72
|
async chat(messages: ChatMessage[], context?: string): Promise<string> {
|
|
73
73
|
const path = this.opts.chatPath ?? '/chat/completions';
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
const formattedMessages = [
|
|
76
76
|
{ role: 'system', content: `${this.systemPrompt}\n\nContext:\n${context ?? 'None'}` },
|
|
77
77
|
...messages,
|
package/src/llm/utils.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LLM utility helpers shared across all provider implementations.
|
|
3
|
+
*
|
|
4
|
+
* These are pure functions with no external dependencies so they can
|
|
5
|
+
* be imported by any provider without creating circular references.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Builds the final system message content by injecting the RAG context
|
|
10
|
+
* into a system prompt using one of two strategies:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Placeholder substitution** — if the prompt contains `{{context}}`,
|
|
13
|
+
* that token is replaced with the retrieved context string.
|
|
14
|
+
*
|
|
15
|
+
* 2. **Appendage** — if there is no placeholder, the context is appended
|
|
16
|
+
* after a separator so the LLM always receives the retrieved evidence,
|
|
17
|
+
* regardless of whether the caller supplied a per-call `systemPrompt`
|
|
18
|
+
* override.
|
|
19
|
+
*
|
|
20
|
+
* This function is intentionally strict: it never silently drops context.
|
|
21
|
+
* If `context` is empty or the sentinel "No relevant context found." the
|
|
22
|
+
* prompt is returned as-is so the LLM is not polluted with an empty block.
|
|
23
|
+
*
|
|
24
|
+
* @param systemPrompt - The base system prompt (from config or per-call override).
|
|
25
|
+
* @param context - The retrieved RAG context string.
|
|
26
|
+
* @returns - The resolved system message content ready to send.
|
|
27
|
+
*/
|
|
28
|
+
export function buildSystemContent(systemPrompt: string, context: string): string {
|
|
29
|
+
const noContext =
|
|
30
|
+
!context ||
|
|
31
|
+
context.trim() === '' ||
|
|
32
|
+
context.trim() === 'No relevant context found.';
|
|
33
|
+
|
|
34
|
+
// Replace {{context}} placeholder regardless of whether context is empty
|
|
35
|
+
// so the prompt is never sent with a raw placeholder token.
|
|
36
|
+
if (systemPrompt.includes('{{context}}')) {
|
|
37
|
+
return systemPrompt.replace('{{context}}', noContext ? '' : context);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Without a placeholder: only append context when there is actual content.
|
|
41
|
+
if (noContext) {
|
|
42
|
+
return systemPrompt;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return `${systemPrompt}\n\n---\nContext:\n${context}`;
|
|
46
|
+
}
|
|
@@ -153,7 +153,11 @@ export class MongoDBProvider extends BaseVectorProvider {
|
|
|
153
153
|
if (key === 'namespace') {
|
|
154
154
|
vectorSearchFilter.namespace = value;
|
|
155
155
|
} else {
|
|
156
|
-
|
|
156
|
+
if (typeof value === 'string') {
|
|
157
|
+
matchFilter[key] = { $regex: new RegExp(`^${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') };
|
|
158
|
+
} else {
|
|
159
|
+
matchFilter[key] = value;
|
|
160
|
+
}
|
|
157
161
|
}
|
|
158
162
|
}
|
|
159
163
|
|
|
@@ -191,12 +195,20 @@ export class MongoDBProvider extends BaseVectorProvider {
|
|
|
191
195
|
|
|
192
196
|
const results = await this.collection!.aggregate(pipeline).toArray();
|
|
193
197
|
|
|
194
|
-
return (results as unknown as Array<{ _id: string; score: number; [key: string]: unknown }>).map((res) =>
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
return (results as unknown as Array<{ _id: string; score: number; [key: string]: unknown }>).map((res) => {
|
|
199
|
+
// Atlas Vector Search returns cosine similarity mapped to [0, 1] via (1 + cos_sim) / 2.
|
|
200
|
+
// We reverse this mapping with (score * 2) - 1 to restore the standard cosine range
|
|
201
|
+
// of [-1, 1], so downstream scoreThreshold comparisons work consistently across
|
|
202
|
+
// all vector DB providers (which natively return values in [-1, 1]).
|
|
203
|
+
const normalizedScore = (res.score * 2) - 1;
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
id: res._id,
|
|
207
|
+
content: res[this.contentKey] as string,
|
|
208
|
+
metadata: (res[this.metadataKey] as Record<string, unknown>) || {},
|
|
209
|
+
score: normalizedScore,
|
|
210
|
+
};
|
|
211
|
+
});
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
async delete(id: string | number, namespace?: string): Promise<void> {
|