@retrivora-ai/rag-engine 1.9.6 → 1.9.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +130 -113
- package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
- package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2376 -489
- package/dist/handlers/index.mjs +2372 -489
- package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
- package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
- package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
- package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
- package/dist/index.css +695 -203
- package/dist/index.d.mts +37 -7
- package/dist/index.d.ts +37 -7
- package/dist/index.js +1197 -286
- package/dist/index.mjs +1221 -297
- package/dist/server.d.mts +62 -6
- package/dist/server.d.ts +62 -6
- package/dist/server.js +2526 -574
- package/dist/server.mjs +2517 -573
- package/package.json +12 -10
- package/src/app/constants.tsx +207 -212
- package/src/app/layout.tsx +4 -28
- package/src/app/types.ts +17 -17
- package/src/components/AmbientBackground.tsx +10 -10
- package/src/components/ArchitectureCard.tsx +43 -7
- package/src/components/ArchitectureCardsSection.tsx +37 -4
- package/src/components/ChatWidget.tsx +4 -1
- package/src/components/ChatWindow.tsx +9 -2
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/DocViewer.tsx +75 -15
- package/src/components/Documentation.tsx +111 -28
- package/src/components/Hero.tsx +103 -20
- package/src/components/Lifecycle.tsx +65 -25
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +279 -0
- package/src/config/RagConfig.ts +56 -10
- package/src/config/constants.ts +5 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +30 -25
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +154 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +148 -16
- package/src/core/ProviderRegistry.ts +5 -5
- package/src/core/Retrivora.ts +52 -6
- package/src/core/VectorPlugin.ts +74 -11
- package/src/core/mcp.ts +261 -0
- package/src/exceptions/index.ts +52 -0
- package/src/handlers/index.ts +504 -47
- package/src/hooks/useRagChat.ts +100 -43
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +7 -0
- package/src/llm/LLMFactory.ts +15 -6
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +23 -13
- package/src/types/chat.ts +16 -0
- package/src/types/props.ts +50 -1
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
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,15 +117,22 @@ 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',
|
|
118
|
-
headers: {
|
|
124
|
+
headers: {
|
|
125
|
+
'Content-Type': 'application/json',
|
|
126
|
+
...(options.headers || {})
|
|
127
|
+
},
|
|
119
128
|
signal: controller.signal,
|
|
120
129
|
body: JSON.stringify({
|
|
121
130
|
message: trimmed,
|
|
122
131
|
history,
|
|
123
132
|
namespace: namespace ?? projectId,
|
|
133
|
+
sessionId: options.sessionId || 'default',
|
|
134
|
+
messageId: assistantMessageId,
|
|
135
|
+
userMessageId: userMsgId,
|
|
124
136
|
}),
|
|
125
137
|
});
|
|
126
138
|
|
|
@@ -177,7 +189,6 @@ export function useRagChat(
|
|
|
177
189
|
// ─────────────────────────────────────────────────────────────────
|
|
178
190
|
|
|
179
191
|
// Add a placeholder assistant message
|
|
180
|
-
const assistantMessageId = `assistant_${Date.now()}`;
|
|
181
192
|
setMessages((prev) => [
|
|
182
193
|
...prev,
|
|
183
194
|
{
|
|
@@ -193,58 +204,34 @@ export function useRagChat(
|
|
|
193
204
|
if (done) break;
|
|
194
205
|
|
|
195
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
|
+
}
|
|
196
214
|
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const complete = buffer.slice(0, lastBoundary + 2);
|
|
201
|
-
buffer = buffer.slice(lastBoundary + 2);
|
|
202
|
-
|
|
203
|
-
let hasNonTextFrame = false;
|
|
204
|
-
|
|
205
|
-
for (const frame of parseSseChunk(complete)) {
|
|
206
|
-
if (frame.type === 'text' && frame.text) {
|
|
215
|
+
for (const frame of frames) {
|
|
216
|
+
if (frame.type === 'text') {
|
|
207
217
|
assistantContent += frame.text;
|
|
208
218
|
pendingContentRef.current = assistantContent;
|
|
209
|
-
// Schedule a batched RAF flush instead of immediate setMessages
|
|
210
219
|
scheduleFlush(assistantMessageId);
|
|
211
|
-
} else if (frame.type === 'thinking'
|
|
220
|
+
} else if (frame.type === 'thinking') {
|
|
212
221
|
thinkingContent += frame.text;
|
|
213
222
|
pendingThinkingRef.current = thinkingContent;
|
|
214
223
|
scheduleFlush(assistantMessageId);
|
|
215
224
|
} else if (frame.type === 'metadata') {
|
|
216
225
|
sources = frame.sources ?? [];
|
|
217
|
-
thinkingMs =
|
|
218
|
-
hasNonTextFrame = true;
|
|
226
|
+
thinkingMs = frame.thinkingMs;
|
|
219
227
|
} else if (frame.type === 'ui_transformation') {
|
|
220
228
|
uiTransformation = frame.data;
|
|
221
|
-
hasNonTextFrame = true;
|
|
222
229
|
} else if (frame.type === 'observability') {
|
|
223
|
-
trace =
|
|
224
|
-
hasNonTextFrame = true;
|
|
230
|
+
trace = frame.data;
|
|
225
231
|
} else if (frame.type === 'error') {
|
|
226
|
-
|
|
232
|
+
throw new Error(frame.error);
|
|
227
233
|
}
|
|
228
234
|
}
|
|
229
|
-
|
|
230
|
-
// Flush non-text metadata immediately (sources, UI transform, trace)
|
|
231
|
-
if (hasNonTextFrame) {
|
|
232
|
-
pendingFlush = false; // cancel pending text flush — we'll do a full update
|
|
233
|
-
setMessages((prev) =>
|
|
234
|
-
prev.map((msg) =>
|
|
235
|
-
msg.id === assistantMessageId
|
|
236
|
-
? {
|
|
237
|
-
...msg,
|
|
238
|
-
content: assistantContent,
|
|
239
|
-
thinking: thinkingContent || undefined,
|
|
240
|
-
thinkingMs: thinkingMs,
|
|
241
|
-
sources: sources.length > 0 ? sources : msg.sources,
|
|
242
|
-
uiTransformation: uiTransformation || (msg as RagMessage).uiTransformation,
|
|
243
|
-
}
|
|
244
|
-
: msg
|
|
245
|
-
)
|
|
246
|
-
);
|
|
247
|
-
}
|
|
248
235
|
}
|
|
249
236
|
|
|
250
237
|
// Process any remaining buffered data
|
|
@@ -254,9 +241,9 @@ export function useRagChat(
|
|
|
254
241
|
else if (frame.type === 'thinking' && frame.text) thinkingContent += frame.text;
|
|
255
242
|
else if (frame.type === 'metadata') {
|
|
256
243
|
sources = frame.sources ?? [];
|
|
257
|
-
thinkingMs =
|
|
244
|
+
thinkingMs = frame.thinkingMs;
|
|
258
245
|
}
|
|
259
|
-
else if (frame.type === 'observability') trace =
|
|
246
|
+
else if (frame.type === 'observability') trace = frame.data;
|
|
260
247
|
}
|
|
261
248
|
}
|
|
262
249
|
|
|
@@ -289,7 +276,7 @@ export function useRagChat(
|
|
|
289
276
|
abortControllerRef.current = null;
|
|
290
277
|
}
|
|
291
278
|
},
|
|
292
|
-
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages]
|
|
279
|
+
[apiUrl, isLoading, namespace, onError, onReply, projectId, setMessages, options.headers, options.sessionId]
|
|
293
280
|
);
|
|
294
281
|
|
|
295
282
|
const clear = useCallback(() => {
|
|
@@ -315,6 +302,73 @@ export function useRagChat(
|
|
|
315
302
|
setIsLoading(false);
|
|
316
303
|
}, []);
|
|
317
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
|
+
|
|
318
372
|
return {
|
|
319
373
|
messages,
|
|
320
374
|
isLoading,
|
|
@@ -324,5 +378,8 @@ export function useRagChat(
|
|
|
324
378
|
retry,
|
|
325
379
|
setMessages,
|
|
326
380
|
stop,
|
|
381
|
+
loadHistory,
|
|
382
|
+
clearHistory,
|
|
383
|
+
submitFeedback,
|
|
327
384
|
};
|
|
328
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
|
@@ -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.
|
|
@@ -13,6 +15,11 @@ export { MessageBubble } from './components/MessageBubble';
|
|
|
13
15
|
export { SourceCard } from './components/SourceCard';
|
|
14
16
|
export { ObservabilityPanel } from './components/ObservabilityPanel';
|
|
15
17
|
export { ConfigProvider, useConfig } from './components/ConfigProvider';
|
|
18
|
+
export { Hero } from './components/Hero';
|
|
19
|
+
export { Lifecycle } from './components/Lifecycle';
|
|
20
|
+
export { ArchitectureCardsSection } from './components/ArchitectureCardsSection';
|
|
21
|
+
export { Documentation } from './components/Documentation';
|
|
22
|
+
export { AmbientBackground } from './components/AmbientBackground';
|
|
16
23
|
|
|
17
24
|
// ── Headless Hooks ────────────────────────────────────────────
|
|
18
25
|
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':
|
|
@@ -83,11 +89,12 @@ export class LLMFactory {
|
|
|
83
89
|
return new UniversalLLMAdapter(llmConfig);
|
|
84
90
|
}
|
|
85
91
|
throw new ProviderNotFoundException(
|
|
86
|
-
'
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
'llm',
|
|
93
|
+
llmConfig.provider ?? 'undefined',
|
|
94
|
+
{
|
|
95
|
+
message: `Unknown provider "${llmConfig.provider}". Register a custom provider with LLMFactory.register().`,
|
|
96
|
+
available: LLMFactory.listProviders()
|
|
97
|
+
}
|
|
91
98
|
);
|
|
92
99
|
}
|
|
93
100
|
}
|
|
@@ -109,6 +116,8 @@ export class LLMFactory {
|
|
|
109
116
|
case 'anthropic': return AnthropicProvider;
|
|
110
117
|
case 'ollama': return OllamaProvider;
|
|
111
118
|
case 'gemini': return GeminiProvider;
|
|
119
|
+
case 'groq': return GroqProvider;
|
|
120
|
+
case 'qwen': return QwenProvider;
|
|
112
121
|
case 'rest':
|
|
113
122
|
case 'universal_rest':
|
|
114
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
|
+
}
|