@retrivora-ai/rag-engine 1.9.7 → 1.9.9
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 +136 -136
- package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
- package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2327 -474
- package/dist/handlers/index.mjs +2329 -473
- package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
- package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
- package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
- package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
- package/dist/index.css +783 -550
- package/dist/index.d.mts +35 -7
- package/dist/index.d.ts +35 -7
- package/dist/index.js +419 -282
- package/dist/index.mjs +426 -292
- package/dist/server.d.mts +65 -6
- package/dist/server.d.ts +65 -6
- package/dist/server.js +2185 -317
- package/dist/server.mjs +2185 -317
- package/package.json +13 -8
- package/src/app/constants.tsx +37 -7
- package/src/app/page.tsx +2 -0
- package/src/components/ChatWidget.tsx +2 -1
- package/src/components/ChatWindow.tsx +4 -1
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +228 -0
- package/src/config/RagConfig.ts +48 -2
- package/src/config/constants.ts +4 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +38 -6
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +260 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +151 -18
- package/src/core/Retrivora.ts +7 -0
- package/src/core/VectorPlugin.ts +12 -3
- package/src/core/mcp.ts +261 -0
- package/src/handlers/index.ts +449 -63
- package/src/hooks/useRagChat.ts +96 -42
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +3 -0
- package/src/llm/LLMFactory.ts +9 -1
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +7 -0
- package/src/types/chat.ts +14 -0
- package/src/types/props.ts +12 -0
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
- package/src/components/AmbientBackground.tsx +0 -29
- package/src/components/ArchitectureCard.tsx +0 -17
- package/src/components/ArchitectureCardsSection.tsx +0 -15
- package/src/components/DocViewer.tsx +0 -103
- package/src/components/Documentation.tsx +0 -121
- package/src/components/Hero.tsx +0 -59
- package/src/components/Lifecycle.tsx +0 -37
- package/src/components/Navbar.tsx +0 -55
package/src/hooks/useRagChat.ts
CHANGED
|
@@ -63,6 +63,7 @@ export function useRagChat(
|
|
|
63
63
|
): UseRagChatReturn {
|
|
64
64
|
const {
|
|
65
65
|
apiUrl = '/api/chat',
|
|
66
|
+
retrivoraApiBase = '/api/retrivora',
|
|
66
67
|
namespace,
|
|
67
68
|
persist = true,
|
|
68
69
|
onReply,
|
|
@@ -89,9 +90,13 @@ export function useRagChat(
|
|
|
89
90
|
|
|
90
91
|
lastInputRef.current = trimmed;
|
|
91
92
|
|
|
93
|
+
const userMsgId = opts?.skipUserMessage
|
|
94
|
+
? (messagesRef.current.filter((m) => m.role === 'user').pop()?.id || `user_${Date.now()}`)
|
|
95
|
+
: `user_${Date.now()}`;
|
|
96
|
+
|
|
92
97
|
if (!opts?.skipUserMessage) {
|
|
93
98
|
const userMessage: RagMessage = {
|
|
94
|
-
id:
|
|
99
|
+
id: userMsgId,
|
|
95
100
|
role: 'user',
|
|
96
101
|
content: trimmed,
|
|
97
102
|
createdAt: new Date().toISOString(),
|
|
@@ -112,6 +117,7 @@ export function useRagChat(
|
|
|
112
117
|
|
|
113
118
|
try {
|
|
114
119
|
const history = messagesRef.current.map(({ role, content }) => ({ role, content }));
|
|
120
|
+
const assistantMessageId = `assistant_${Date.now()}`;
|
|
115
121
|
|
|
116
122
|
const response = await fetch(apiUrl, {
|
|
117
123
|
method: 'POST',
|
|
@@ -124,6 +130,9 @@ export function useRagChat(
|
|
|
124
130
|
message: trimmed,
|
|
125
131
|
history,
|
|
126
132
|
namespace: namespace ?? projectId,
|
|
133
|
+
sessionId: options.sessionId || 'default',
|
|
134
|
+
messageId: assistantMessageId,
|
|
135
|
+
userMessageId: userMsgId,
|
|
127
136
|
}),
|
|
128
137
|
});
|
|
129
138
|
|
|
@@ -180,7 +189,6 @@ export function useRagChat(
|
|
|
180
189
|
// ─────────────────────────────────────────────────────────────────
|
|
181
190
|
|
|
182
191
|
// Add a placeholder assistant message
|
|
183
|
-
const assistantMessageId = `assistant_${Date.now()}`;
|
|
184
192
|
setMessages((prev) => [
|
|
185
193
|
...prev,
|
|
186
194
|
{
|
|
@@ -196,58 +204,34 @@ export function useRagChat(
|
|
|
196
204
|
if (done) break;
|
|
197
205
|
|
|
198
206
|
buffer += decoder.decode(value, { stream: true });
|
|
207
|
+
const frames = parseSseChunk(buffer);
|
|
208
|
+
|
|
209
|
+
// Clear processed frames from buffer (approximate by matching last found frame boundary)
|
|
210
|
+
const lastIndex = buffer.lastIndexOf('\n\n');
|
|
211
|
+
if (lastIndex !== -1) {
|
|
212
|
+
buffer = buffer.slice(lastIndex + 2);
|
|
213
|
+
}
|
|
199
214
|
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const complete = buffer.slice(0, lastBoundary + 2);
|
|
204
|
-
buffer = buffer.slice(lastBoundary + 2);
|
|
205
|
-
|
|
206
|
-
let hasNonTextFrame = false;
|
|
207
|
-
|
|
208
|
-
for (const frame of parseSseChunk(complete)) {
|
|
209
|
-
if (frame.type === 'text' && frame.text) {
|
|
215
|
+
for (const frame of frames) {
|
|
216
|
+
if (frame.type === 'text') {
|
|
210
217
|
assistantContent += frame.text;
|
|
211
218
|
pendingContentRef.current = assistantContent;
|
|
212
|
-
// Schedule a batched RAF flush instead of immediate setMessages
|
|
213
219
|
scheduleFlush(assistantMessageId);
|
|
214
|
-
} else if (frame.type === 'thinking'
|
|
220
|
+
} else if (frame.type === 'thinking') {
|
|
215
221
|
thinkingContent += frame.text;
|
|
216
222
|
pendingThinkingRef.current = thinkingContent;
|
|
217
223
|
scheduleFlush(assistantMessageId);
|
|
218
224
|
} else if (frame.type === 'metadata') {
|
|
219
225
|
sources = frame.sources ?? [];
|
|
220
|
-
thinkingMs =
|
|
221
|
-
hasNonTextFrame = true;
|
|
226
|
+
thinkingMs = frame.thinkingMs;
|
|
222
227
|
} else if (frame.type === 'ui_transformation') {
|
|
223
228
|
uiTransformation = frame.data;
|
|
224
|
-
hasNonTextFrame = true;
|
|
225
229
|
} else if (frame.type === 'observability') {
|
|
226
|
-
trace =
|
|
227
|
-
hasNonTextFrame = true;
|
|
230
|
+
trace = frame.data;
|
|
228
231
|
} else if (frame.type === 'error') {
|
|
229
|
-
|
|
232
|
+
throw new Error(frame.error);
|
|
230
233
|
}
|
|
231
234
|
}
|
|
232
|
-
|
|
233
|
-
// Flush non-text metadata immediately (sources, UI transform, trace)
|
|
234
|
-
if (hasNonTextFrame) {
|
|
235
|
-
pendingFlush = false; // cancel pending text flush — we'll do a full update
|
|
236
|
-
setMessages((prev) =>
|
|
237
|
-
prev.map((msg) =>
|
|
238
|
-
msg.id === assistantMessageId
|
|
239
|
-
? {
|
|
240
|
-
...msg,
|
|
241
|
-
content: assistantContent,
|
|
242
|
-
thinking: thinkingContent || undefined,
|
|
243
|
-
thinkingMs: thinkingMs,
|
|
244
|
-
sources: sources.length > 0 ? sources : msg.sources,
|
|
245
|
-
uiTransformation: uiTransformation || (msg as RagMessage).uiTransformation,
|
|
246
|
-
}
|
|
247
|
-
: msg
|
|
248
|
-
)
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
235
|
}
|
|
252
236
|
|
|
253
237
|
// Process any remaining buffered data
|
|
@@ -257,9 +241,9 @@ export function useRagChat(
|
|
|
257
241
|
else if (frame.type === 'thinking' && frame.text) thinkingContent += frame.text;
|
|
258
242
|
else if (frame.type === 'metadata') {
|
|
259
243
|
sources = frame.sources ?? [];
|
|
260
|
-
thinkingMs =
|
|
244
|
+
thinkingMs = frame.thinkingMs;
|
|
261
245
|
}
|
|
262
|
-
else if (frame.type === 'observability') trace =
|
|
246
|
+
else if (frame.type === 'observability') trace = frame.data;
|
|
263
247
|
}
|
|
264
248
|
}
|
|
265
249
|
|
|
@@ -292,7 +276,7 @@ export function useRagChat(
|
|
|
292
276
|
abortControllerRef.current = null;
|
|
293
277
|
}
|
|
294
278
|
},
|
|
295
|
-
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages]
|
|
279
|
+
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages, options.headers, options.sessionId]
|
|
296
280
|
);
|
|
297
281
|
|
|
298
282
|
const clear = useCallback(() => {
|
|
@@ -318,6 +302,73 @@ export function useRagChat(
|
|
|
318
302
|
setIsLoading(false);
|
|
319
303
|
}, []);
|
|
320
304
|
|
|
305
|
+
const loadHistory = useCallback(async (sessionId: string) => {
|
|
306
|
+
setIsLoading(true);
|
|
307
|
+
setError(null);
|
|
308
|
+
try {
|
|
309
|
+
const res = await fetch(`${retrivoraApiBase}/history?sessionId=${encodeURIComponent(sessionId)}`);
|
|
310
|
+
if (!res.ok) throw new Error(`Failed to load history: ${res.statusText}`);
|
|
311
|
+
const data = await res.json();
|
|
312
|
+
const loadedMessages = (data.history || []).map((msg: any) => ({
|
|
313
|
+
id: msg.id,
|
|
314
|
+
role: msg.role,
|
|
315
|
+
content: msg.content,
|
|
316
|
+
sources: msg.sources,
|
|
317
|
+
uiTransformation: msg.uiTransformation,
|
|
318
|
+
trace: msg.trace,
|
|
319
|
+
createdAt: msg.createdAt,
|
|
320
|
+
}));
|
|
321
|
+
setMessages(loadedMessages);
|
|
322
|
+
} catch (err) {
|
|
323
|
+
const msg = err instanceof Error ? err.message : 'Failed to load history';
|
|
324
|
+
setError(msg);
|
|
325
|
+
onError?.(msg);
|
|
326
|
+
} finally {
|
|
327
|
+
setIsLoading(false);
|
|
328
|
+
}
|
|
329
|
+
}, [retrivoraApiBase, onError, setMessages]);
|
|
330
|
+
|
|
331
|
+
const clearHistory = useCallback(async (sessionId: string) => {
|
|
332
|
+
setIsLoading(true);
|
|
333
|
+
setError(null);
|
|
334
|
+
try {
|
|
335
|
+
const res = await fetch(`${retrivoraApiBase}/history/clear`, {
|
|
336
|
+
method: 'POST',
|
|
337
|
+
headers: { 'Content-Type': 'application/json' },
|
|
338
|
+
body: JSON.stringify({ sessionId }),
|
|
339
|
+
});
|
|
340
|
+
if (!res.ok) throw new Error(`Failed to clear history: ${res.statusText}`);
|
|
341
|
+
setMessages([]);
|
|
342
|
+
if (persist) localStorage.removeItem(storageKey);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
const msg = err instanceof Error ? err.message : 'Failed to clear history';
|
|
345
|
+
setError(msg);
|
|
346
|
+
onError?.(msg);
|
|
347
|
+
} finally {
|
|
348
|
+
setIsLoading(false);
|
|
349
|
+
}
|
|
350
|
+
}, [retrivoraApiBase, onError, persist, setMessages, storageKey]);
|
|
351
|
+
|
|
352
|
+
const submitFeedback = useCallback(async (messageId: string, rating: 'thumbs_up' | 'thumbs_down', comment?: string) => {
|
|
353
|
+
try {
|
|
354
|
+
const res = await fetch(`${retrivoraApiBase}/feedback`, {
|
|
355
|
+
method: 'POST',
|
|
356
|
+
headers: { 'Content-Type': 'application/json' },
|
|
357
|
+
body: JSON.stringify({
|
|
358
|
+
messageId,
|
|
359
|
+
sessionId: options.sessionId || 'default',
|
|
360
|
+
rating,
|
|
361
|
+
comment,
|
|
362
|
+
}),
|
|
363
|
+
});
|
|
364
|
+
if (!res.ok) throw new Error(`Failed to submit feedback: ${res.statusText}`);
|
|
365
|
+
} catch (err) {
|
|
366
|
+
const msg = err instanceof Error ? err.message : 'Failed to submit feedback';
|
|
367
|
+
onError?.(msg);
|
|
368
|
+
throw err;
|
|
369
|
+
}
|
|
370
|
+
}, [retrivoraApiBase, onError, options.sessionId]);
|
|
371
|
+
|
|
321
372
|
return {
|
|
322
373
|
messages,
|
|
323
374
|
isLoading,
|
|
@@ -327,5 +378,8 @@ export function useRagChat(
|
|
|
327
378
|
retry,
|
|
328
379
|
setMessages,
|
|
329
380
|
stop,
|
|
381
|
+
loadHistory,
|
|
382
|
+
clearHistory,
|
|
383
|
+
submitFeedback,
|
|
330
384
|
};
|
|
331
385
|
}
|
|
@@ -32,12 +32,23 @@ export function useStoredMessages<T extends StoredMessage>(
|
|
|
32
32
|
storageKey: string,
|
|
33
33
|
persist: boolean
|
|
34
34
|
): [T[], React.Dispatch<React.SetStateAction<T[]>>] {
|
|
35
|
-
const [messages, setMessages] = React.useState<T[]>(
|
|
36
|
-
|
|
37
|
-
));
|
|
35
|
+
const [messages, setMessages] = React.useState<T[]>([]);
|
|
36
|
+
const isLoaded = React.useRef(false);
|
|
38
37
|
|
|
38
|
+
// Load from localStorage only on client-side mount to prevent hydration mismatch
|
|
39
39
|
React.useEffect(() => {
|
|
40
|
-
if (
|
|
40
|
+
if (persist) {
|
|
41
|
+
const loaded = readStoredMessages(storageKey) as T[];
|
|
42
|
+
if (loaded.length > 0) {
|
|
43
|
+
setMessages(loaded);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
isLoaded.current = true;
|
|
47
|
+
}, [storageKey, persist]);
|
|
48
|
+
|
|
49
|
+
// Save to localStorage only after initialization has completed to avoid overwriting with empty array
|
|
50
|
+
React.useEffect(() => {
|
|
51
|
+
if (!isLoaded.current || !persist || typeof window === 'undefined') {
|
|
41
52
|
return;
|
|
42
53
|
}
|
|
43
54
|
|
package/src/index.ts
CHANGED
|
@@ -15,6 +15,9 @@ export { MessageBubble } from './components/MessageBubble';
|
|
|
15
15
|
export { SourceCard } from './components/SourceCard';
|
|
16
16
|
export { ObservabilityPanel } from './components/ObservabilityPanel';
|
|
17
17
|
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
18
|
+
export { CodeViewer } from './components/CodeViewer';
|
|
19
|
+
export { ProductCard } from './components/ProductCard';
|
|
20
|
+
export { ProductCarousel } from './components/ProductCarousel';
|
|
18
21
|
|
|
19
22
|
// ── Headless Hooks ────────────────────────────────────────────
|
|
20
23
|
export { useRagChat } from './hooks/useRagChat';
|
package/src/llm/LLMFactory.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { OpenAIProvider } from './providers/OpenAIProvider';
|
|
|
4
4
|
import { AnthropicProvider } from './providers/AnthropicProvider';
|
|
5
5
|
import { OllamaProvider } from './providers/OllamaProvider';
|
|
6
6
|
import { GeminiProvider } from './providers/GeminiProvider';
|
|
7
|
+
import { GroqProvider } from './providers/GroqProvider';
|
|
8
|
+
import { QwenProvider } from './providers/QwenProvider';
|
|
7
9
|
import { UniversalLLMAdapter } from './providers/UniversalLLMAdapter';
|
|
8
10
|
import { IProviderValidator, IProviderHealthChecker } from '../core/ProviderInterfaces';
|
|
9
11
|
import { ProviderNotFoundException } from '../exceptions';
|
|
@@ -53,7 +55,7 @@ export class LLMFactory {
|
|
|
53
55
|
*/
|
|
54
56
|
static listProviders(): string[] {
|
|
55
57
|
return [
|
|
56
|
-
'openai', 'anthropic', 'ollama', 'gemini', 'rest', 'universal_rest', 'custom',
|
|
58
|
+
'openai', 'anthropic', 'ollama', 'gemini', 'groq', 'qwen', 'rest', 'universal_rest', 'custom',
|
|
57
59
|
...Array.from(customProviders.keys()),
|
|
58
60
|
];
|
|
59
61
|
}
|
|
@@ -67,6 +69,10 @@ export class LLMFactory {
|
|
|
67
69
|
return new OllamaProvider(llmConfig, embeddingConfig);
|
|
68
70
|
case 'gemini':
|
|
69
71
|
return new GeminiProvider(llmConfig, embeddingConfig);
|
|
72
|
+
case 'groq':
|
|
73
|
+
return new GroqProvider(llmConfig, embeddingConfig);
|
|
74
|
+
case 'qwen':
|
|
75
|
+
return new QwenProvider(llmConfig, embeddingConfig);
|
|
70
76
|
case 'rest':
|
|
71
77
|
case 'universal_rest':
|
|
72
78
|
case 'custom':
|
|
@@ -110,6 +116,8 @@ export class LLMFactory {
|
|
|
110
116
|
case 'anthropic': return AnthropicProvider;
|
|
111
117
|
case 'ollama': return OllamaProvider;
|
|
112
118
|
case 'gemini': return GeminiProvider;
|
|
119
|
+
case 'groq': return GroqProvider;
|
|
120
|
+
case 'qwen': return QwenProvider;
|
|
113
121
|
case 'rest':
|
|
114
122
|
case 'universal_rest':
|
|
115
123
|
case 'custom': return UniversalLLMAdapter as unknown as LLMProviderStatic;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Groq LLM Provider
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import { ILLMProvider, EmbedOptions } from '../ILLMProvider';
|
|
7
|
+
import { ChatMessage, ChatOptions } from '../../types';
|
|
8
|
+
import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
|
|
9
|
+
import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
|
|
10
|
+
import { ValidationError } from '../../core/ConfigValidator';
|
|
11
|
+
import { buildSystemContent } from '../utils';
|
|
12
|
+
|
|
13
|
+
export class GroqProvider implements ILLMProvider {
|
|
14
|
+
private readonly client: OpenAI;
|
|
15
|
+
private readonly llmConfig: LLMConfig;
|
|
16
|
+
|
|
17
|
+
constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
|
|
18
|
+
void embeddingConfig;
|
|
19
|
+
const apiKey = llmConfig.apiKey || process.env.GROQ_API_KEY;
|
|
20
|
+
if (!apiKey) throw new Error('[GroqProvider] llmConfig.apiKey or GROQ_API_KEY environment variable is required');
|
|
21
|
+
|
|
22
|
+
this.client = new OpenAI({
|
|
23
|
+
apiKey,
|
|
24
|
+
baseURL: llmConfig.baseUrl || 'https://api.groq.com/openai/v1',
|
|
25
|
+
});
|
|
26
|
+
this.llmConfig = llmConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
static getValidator(): IProviderValidator {
|
|
30
|
+
return {
|
|
31
|
+
validate(config: Record<string, unknown>): ValidationError[] {
|
|
32
|
+
const errors: ValidationError[] = [];
|
|
33
|
+
if (!config.apiKey && !process.env.GROQ_API_KEY) {
|
|
34
|
+
errors.push({
|
|
35
|
+
field: 'llm.apiKey',
|
|
36
|
+
message: 'Groq API key is required',
|
|
37
|
+
suggestion: 'Set GROQ_API_KEY environment variable',
|
|
38
|
+
severity: 'error',
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (!config.model) {
|
|
42
|
+
errors.push({
|
|
43
|
+
field: 'llm.model',
|
|
44
|
+
message: 'Groq model name is required',
|
|
45
|
+
suggestion: 'e.g., "llama-3.3-70b-versatile"',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return errors;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
static getHealthChecker(): IProviderHealthChecker {
|
|
55
|
+
return {
|
|
56
|
+
async check(config: Record<string, unknown>): Promise<HealthCheckResult> {
|
|
57
|
+
const timestamp = Date.now();
|
|
58
|
+
const apiKey = (config.apiKey as string) || process.env.GROQ_API_KEY || '';
|
|
59
|
+
const modelName = config.model as string;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const OpenAI = await import('openai');
|
|
63
|
+
const client = new OpenAI.default({
|
|
64
|
+
apiKey,
|
|
65
|
+
baseURL: (config.baseUrl as string) || 'https://api.groq.com/openai/v1',
|
|
66
|
+
});
|
|
67
|
+
const models = await client.models.list();
|
|
68
|
+
const hasModel = models.data.some((m) => m.id === modelName);
|
|
69
|
+
return {
|
|
70
|
+
healthy: true,
|
|
71
|
+
provider: 'groq',
|
|
72
|
+
capabilities: { model: modelName, available: hasModel, totalModels: models.data.length },
|
|
73
|
+
timestamp,
|
|
74
|
+
};
|
|
75
|
+
} catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
healthy: false,
|
|
78
|
+
provider: 'groq',
|
|
79
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
80
|
+
timestamp,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
88
|
+
const basePrompt =
|
|
89
|
+
options?.systemPrompt ??
|
|
90
|
+
this.llmConfig.systemPrompt ??
|
|
91
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
92
|
+
|
|
93
|
+
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
94
|
+
role: 'system',
|
|
95
|
+
content: buildSystemContent(basePrompt, context),
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
99
|
+
systemMessage,
|
|
100
|
+
...messages.map((m) => ({
|
|
101
|
+
role: m.role as 'user' | 'assistant',
|
|
102
|
+
content: m.content,
|
|
103
|
+
})),
|
|
104
|
+
];
|
|
105
|
+
|
|
106
|
+
const completion = await this.client.chat.completions.create({
|
|
107
|
+
model: this.llmConfig.model,
|
|
108
|
+
messages: formattedMessages,
|
|
109
|
+
max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
|
|
110
|
+
temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
|
|
111
|
+
stop: options?.stop,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return completion.choices[0]?.message?.content ?? '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
118
|
+
const basePrompt =
|
|
119
|
+
options?.systemPrompt ??
|
|
120
|
+
this.llmConfig.systemPrompt ??
|
|
121
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
122
|
+
|
|
123
|
+
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
124
|
+
role: 'system',
|
|
125
|
+
content: buildSystemContent(basePrompt, context),
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
129
|
+
systemMessage,
|
|
130
|
+
...messages.map((m) => ({
|
|
131
|
+
role: m.role as 'user' | 'assistant',
|
|
132
|
+
content: m.content,
|
|
133
|
+
})),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const stream = await this.client.chat.completions.create({
|
|
137
|
+
model: this.llmConfig.model,
|
|
138
|
+
messages: formattedMessages,
|
|
139
|
+
max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
|
|
140
|
+
temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
|
|
141
|
+
stop: options?.stop,
|
|
142
|
+
stream: true,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!stream) {
|
|
146
|
+
throw new Error('[GroqProvider] completions.create stream is undefined');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for await (const chunk of stream) {
|
|
150
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
151
|
+
if (content) yield content;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async embed(text: string, options?: EmbedOptions): Promise<number[]> {
|
|
156
|
+
void text;
|
|
157
|
+
void options;
|
|
158
|
+
throw new Error('[GroqProvider] Groq does not provide a native embedding API. Please configure "openai" or "gemini" for embeddings in RagConfig.');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async batchEmbed(texts: string[], options?: EmbedOptions): Promise<number[][]> {
|
|
162
|
+
void texts;
|
|
163
|
+
void options;
|
|
164
|
+
throw new Error('[GroqProvider] Groq does not provide a native embedding API. Please configure "openai" or "gemini" for embeddings in RagConfig.');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async ping(): Promise<boolean> {
|
|
168
|
+
try {
|
|
169
|
+
await this.client.models.list();
|
|
170
|
+
return true;
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('[GroqProvider] Ping failed:', err);
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen LLM Provider (via Alibaba DashScope / ModelStudio compatible mode)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import OpenAI from 'openai';
|
|
6
|
+
import { ILLMProvider, EmbedOptions } from '../ILLMProvider';
|
|
7
|
+
import { ChatMessage, ChatOptions } from '../../types';
|
|
8
|
+
import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
|
|
9
|
+
import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
|
|
10
|
+
import { ValidationError } from '../../core/ConfigValidator';
|
|
11
|
+
import { buildSystemContent } from '../utils';
|
|
12
|
+
|
|
13
|
+
export class QwenProvider implements ILLMProvider {
|
|
14
|
+
private readonly client: OpenAI;
|
|
15
|
+
private readonly llmConfig: LLMConfig;
|
|
16
|
+
private readonly embeddingConfig?: EmbeddingConfig;
|
|
17
|
+
|
|
18
|
+
constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
|
|
19
|
+
const apiKey = llmConfig.apiKey || process.env.DASHSCOPE_API_KEY || process.env.QWEN_API_KEY;
|
|
20
|
+
if (!apiKey) throw new Error('[QwenProvider] llmConfig.apiKey, DASHSCOPE_API_KEY, or QWEN_API_KEY environment variable is required');
|
|
21
|
+
|
|
22
|
+
this.client = new OpenAI({
|
|
23
|
+
apiKey,
|
|
24
|
+
baseURL: llmConfig.baseUrl || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
25
|
+
});
|
|
26
|
+
this.llmConfig = llmConfig;
|
|
27
|
+
this.embeddingConfig = embeddingConfig;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static getValidator(): IProviderValidator {
|
|
31
|
+
return {
|
|
32
|
+
validate(config: Record<string, unknown>): ValidationError[] {
|
|
33
|
+
const errors: ValidationError[] = [];
|
|
34
|
+
const isEmbedding = config.provider === 'qwen' && 'dimensions' in config;
|
|
35
|
+
const prefix = isEmbedding ? 'embedding' : 'llm';
|
|
36
|
+
|
|
37
|
+
if (!config.apiKey && !process.env.DASHSCOPE_API_KEY && !process.env.QWEN_API_KEY) {
|
|
38
|
+
errors.push({
|
|
39
|
+
field: `${prefix}.apiKey`,
|
|
40
|
+
message: 'Qwen API key is required',
|
|
41
|
+
suggestion: 'Set DASHSCOPE_API_KEY or QWEN_API_KEY environment variable',
|
|
42
|
+
severity: 'error',
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (!config.model) {
|
|
46
|
+
errors.push({
|
|
47
|
+
field: `${prefix}.model`,
|
|
48
|
+
message: 'Qwen model name is required',
|
|
49
|
+
suggestion: isEmbedding ? 'e.g., "text-embedding-v3"' : 'e.g., "qwen-plus" or "qwen-max"',
|
|
50
|
+
severity: 'error',
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return errors;
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static getHealthChecker(): IProviderHealthChecker {
|
|
59
|
+
return {
|
|
60
|
+
async check(config: Record<string, unknown>): Promise<HealthCheckResult> {
|
|
61
|
+
const timestamp = Date.now();
|
|
62
|
+
const apiKey = (config.apiKey as string) || process.env.DASHSCOPE_API_KEY || process.env.QWEN_API_KEY || '';
|
|
63
|
+
const modelName = config.model as string;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const OpenAI = await import('openai');
|
|
67
|
+
const client = new OpenAI.default({
|
|
68
|
+
apiKey,
|
|
69
|
+
baseURL: (config.baseUrl as string) || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
70
|
+
});
|
|
71
|
+
const models = await client.models.list();
|
|
72
|
+
const hasModel = models.data.some((m) => m.id === modelName);
|
|
73
|
+
return {
|
|
74
|
+
healthy: true,
|
|
75
|
+
provider: 'qwen',
|
|
76
|
+
capabilities: { model: modelName, available: hasModel, totalModels: models.data.length },
|
|
77
|
+
timestamp,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
healthy: false,
|
|
82
|
+
provider: 'qwen',
|
|
83
|
+
error: `Connection failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
84
|
+
timestamp,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
|
|
92
|
+
const basePrompt =
|
|
93
|
+
options?.systemPrompt ??
|
|
94
|
+
this.llmConfig.systemPrompt ??
|
|
95
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
96
|
+
|
|
97
|
+
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
98
|
+
role: 'system',
|
|
99
|
+
content: buildSystemContent(basePrompt, context),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
103
|
+
systemMessage,
|
|
104
|
+
...messages.map((m) => ({
|
|
105
|
+
role: m.role as 'user' | 'assistant',
|
|
106
|
+
content: m.content,
|
|
107
|
+
})),
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
const completion = await this.client.chat.completions.create({
|
|
111
|
+
model: this.llmConfig.model,
|
|
112
|
+
messages: formattedMessages,
|
|
113
|
+
max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
|
|
114
|
+
temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
|
|
115
|
+
stop: options?.stop,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
return completion.choices[0]?.message?.content ?? '';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
|
|
122
|
+
const basePrompt =
|
|
123
|
+
options?.systemPrompt ??
|
|
124
|
+
this.llmConfig.systemPrompt ??
|
|
125
|
+
'You are a helpful assistant. Answer questions based on the provided context.';
|
|
126
|
+
|
|
127
|
+
const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
|
|
128
|
+
role: 'system',
|
|
129
|
+
content: buildSystemContent(basePrompt, context),
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
|
|
133
|
+
systemMessage,
|
|
134
|
+
...messages.map((m) => ({
|
|
135
|
+
role: m.role as 'user' | 'assistant',
|
|
136
|
+
content: m.content,
|
|
137
|
+
})),
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
const stream = await this.client.chat.completions.create({
|
|
141
|
+
model: this.llmConfig.model,
|
|
142
|
+
messages: formattedMessages,
|
|
143
|
+
max_tokens: options?.maxTokens ?? this.llmConfig.maxTokens ?? 1024,
|
|
144
|
+
temperature: options?.temperature ?? this.llmConfig.temperature ?? 0.7,
|
|
145
|
+
stop: options?.stop,
|
|
146
|
+
stream: true,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
if (!stream) {
|
|
150
|
+
throw new Error('[QwenProvider] completions.create stream is undefined');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
for await (const chunk of stream) {
|
|
154
|
+
const content = chunk.choices[0]?.delta?.content || '';
|
|
155
|
+
if (content) yield content;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async embed(text: string, options?: EmbedOptions): Promise<number[]> {
|
|
160
|
+
const results = await this.batchEmbed([text], options);
|
|
161
|
+
return results[0];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async batchEmbed(texts: string[], options?: EmbedOptions): Promise<number[][]> {
|
|
165
|
+
const model =
|
|
166
|
+
options?.model ??
|
|
167
|
+
this.embeddingConfig?.model ??
|
|
168
|
+
'text-embedding-v1';
|
|
169
|
+
|
|
170
|
+
const apiKey = this.embeddingConfig?.apiKey ?? this.llmConfig.apiKey;
|
|
171
|
+
const client = apiKey !== this.llmConfig.apiKey
|
|
172
|
+
? new OpenAI({
|
|
173
|
+
apiKey,
|
|
174
|
+
baseURL: this.embeddingConfig?.baseUrl || 'https://dashscope.aliyuncs.com/compatible-mode/v1',
|
|
175
|
+
})
|
|
176
|
+
: this.client;
|
|
177
|
+
|
|
178
|
+
const response = await client.embeddings.create({ model, input: texts });
|
|
179
|
+
return response.data.map((d) => d.embedding);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async ping(): Promise<boolean> {
|
|
183
|
+
try {
|
|
184
|
+
await this.client.models.list();
|
|
185
|
+
return true;
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error('[QwenProvider] Ping failed:', err);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|