@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/handlers/index.ts
CHANGED
|
@@ -3,7 +3,147 @@ import { VectorPlugin } from '../core/VectorPlugin';
|
|
|
3
3
|
import { RagConfig } from '../config/RagConfig';
|
|
4
4
|
import { ChatMessage, ChatResponse } from '../types';
|
|
5
5
|
import { DocumentParser } from '../utils/DocumentParser';
|
|
6
|
-
import { UITransformer } from '../utils/UITransformer'
|
|
6
|
+
import { UITransformer } from '../utils/UITransformer';
|
|
7
|
+
import { DatabaseStorage, HistoryMessage } from '../core/DatabaseStorage';
|
|
8
|
+
|
|
9
|
+
export interface HandlerOptions {
|
|
10
|
+
onAuthorize?: (req: NextRequest) => boolean | Promise<boolean> | Response | Promise<Response>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async function checkAuth(
|
|
14
|
+
req: NextRequest,
|
|
15
|
+
onAuthorize?: (req: NextRequest) => boolean | Promise<boolean> | Response | Promise<Response>
|
|
16
|
+
): Promise<Response | null> {
|
|
17
|
+
if (!onAuthorize) return null;
|
|
18
|
+
try {
|
|
19
|
+
const res = await onAuthorize(req);
|
|
20
|
+
if (res === false) {
|
|
21
|
+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
|
22
|
+
}
|
|
23
|
+
if (res instanceof Response) {
|
|
24
|
+
return res;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : 'Authorization check failed';
|
|
29
|
+
return NextResponse.json({ error: msg }, { status: 401 });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
// ─── Rate Limiter ─────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* In-memory sliding-window rate limiter.
|
|
38
|
+
* Keyed by IP + licenseKey. Rejects requests that exceed the limit.
|
|
39
|
+
* Automatically prunes expired entries to avoid memory growth.
|
|
40
|
+
*/
|
|
41
|
+
class RateLimiter {
|
|
42
|
+
private readonly hits = new Map<string, number[]>();
|
|
43
|
+
private readonly windowMs: number;
|
|
44
|
+
private readonly max: number;
|
|
45
|
+
|
|
46
|
+
constructor(windowMs = 60_000, max = 30) {
|
|
47
|
+
this.windowMs = windowMs;
|
|
48
|
+
this.max = max;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Returns true if the request should be allowed, false if rate-limited. */
|
|
52
|
+
allow(key: string): boolean {
|
|
53
|
+
const now = Date.now();
|
|
54
|
+
const cutoff = now - this.windowMs;
|
|
55
|
+
const existing = (this.hits.get(key) || []).filter(t => t > cutoff);
|
|
56
|
+
existing.push(now);
|
|
57
|
+
this.hits.set(key, existing);
|
|
58
|
+
// Periodically prune stale entries to avoid memory leak
|
|
59
|
+
if (this.hits.size > 5000) {
|
|
60
|
+
for (const [k, timestamps] of this.hits.entries()) {
|
|
61
|
+
if (timestamps.every(t => t <= cutoff)) this.hits.delete(k);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return existing.length <= this.max;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
retryAfterSec(key: string): number {
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
const cutoff = now - this.windowMs;
|
|
70
|
+
const existing = (this.hits.get(key) || []).filter(t => t > cutoff);
|
|
71
|
+
if (existing.length === 0) return 0;
|
|
72
|
+
return Math.ceil((existing[0] + this.windowMs - now) / 1000);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Global singleton rate limiter: 30 requests per 60s per IP
|
|
77
|
+
const _g = global as any;
|
|
78
|
+
const rateLimiter: RateLimiter = _g.__retrivoraRateLimiter ??
|
|
79
|
+
(_g.__retrivoraRateLimiter = new RateLimiter(60_000, 30));
|
|
80
|
+
|
|
81
|
+
/** Derive a rate-limit key from request headers. */
|
|
82
|
+
function getRateLimitKey(req: NextRequest): string {
|
|
83
|
+
const ip = req.headers.get('x-forwarded-for')?.split(',')[0]?.trim()
|
|
84
|
+
|| req.headers.get('x-real-ip')
|
|
85
|
+
|| '127.0.0.1';
|
|
86
|
+
return `ip:${ip}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Check rate limit and return a 429 response if exceeded, else null. */
|
|
90
|
+
function checkRateLimit(req: NextRequest): Response | null {
|
|
91
|
+
const key = getRateLimitKey(req);
|
|
92
|
+
if (!rateLimiter.allow(key)) {
|
|
93
|
+
const retryAfter = rateLimiter.retryAfterSec(key);
|
|
94
|
+
return new Response(
|
|
95
|
+
JSON.stringify({ error: { code: 'RATE_LIMITED', message: 'Too many requests. Please slow down.' } }),
|
|
96
|
+
{
|
|
97
|
+
status: 429,
|
|
98
|
+
headers: {
|
|
99
|
+
'Content-Type': 'application/json',
|
|
100
|
+
'Retry-After': String(retryAfter),
|
|
101
|
+
'X-RateLimit-Limit': '30',
|
|
102
|
+
'X-RateLimit-Window': '60',
|
|
103
|
+
},
|
|
104
|
+
}
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ─── Input Sanitization ───────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
const MAX_MESSAGE_LENGTH = 8000;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sanitize a chat message:
|
|
116
|
+
* - Strip Unicode control characters (except standard whitespace)
|
|
117
|
+
* - Enforce max character length
|
|
118
|
+
*/
|
|
119
|
+
function sanitizeInput(raw: string): { ok: true; value: string } | { ok: false; error: string } {
|
|
120
|
+
// Strip control chars (U+0000–U+001F except tab/newline/CR, and U+007F–U+009F)
|
|
121
|
+
const stripped = raw.replace(/[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F]/g, '');
|
|
122
|
+
const trimmed = stripped.trim();
|
|
123
|
+
if (!trimmed) {
|
|
124
|
+
return { ok: false, error: 'message must not be empty' };
|
|
125
|
+
}
|
|
126
|
+
if (trimmed.length > MAX_MESSAGE_LENGTH) {
|
|
127
|
+
return { ok: false, error: `message must be ≤ ${MAX_MESSAGE_LENGTH} characters` };
|
|
128
|
+
}
|
|
129
|
+
return { ok: true, value: trimmed };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Plugin Singleton ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Get or create a module-level VectorPlugin singleton keyed by a stable hash of the config.
|
|
136
|
+
* Prevents unnecessary DB connection pool recreation on hot reloads and serverless cold starts.
|
|
137
|
+
*/
|
|
138
|
+
function getOrCreatePlugin(configOrPlugin: Partial<RagConfig> | VectorPlugin | undefined): VectorPlugin {
|
|
139
|
+
if (configOrPlugin instanceof VectorPlugin) return configOrPlugin;
|
|
140
|
+
// Use a global to survive Next.js hot reloads in dev
|
|
141
|
+
const cacheKey = '__retrivoraPlugin_' + JSON.stringify(configOrPlugin ?? {}).slice(0, 64);
|
|
142
|
+
if (!_g[cacheKey]) {
|
|
143
|
+
_g[cacheKey] = new VectorPlugin(configOrPlugin);
|
|
144
|
+
}
|
|
145
|
+
return _g[cacheKey];
|
|
146
|
+
}
|
|
7
147
|
|
|
8
148
|
// ─── SSE helpers ──────────────────────────────────────────────────────────────
|
|
9
149
|
|
|
@@ -48,6 +188,8 @@ const SSE_HEADERS: Record<string, string> = {
|
|
|
48
188
|
'X-Accel-Buffering': 'no', // Disable Nginx buffering for streaming
|
|
49
189
|
};
|
|
50
190
|
|
|
191
|
+
|
|
192
|
+
|
|
51
193
|
// ─── Handler Factories ────────────────────────────────────────────────────────
|
|
52
194
|
|
|
53
195
|
/**
|
|
@@ -64,27 +206,66 @@ const SSE_HEADERS: Record<string, string> = {
|
|
|
64
206
|
* const plugin = new VectorPlugin(getRagConfig());
|
|
65
207
|
* export const POST = createChatHandler(plugin);
|
|
66
208
|
*/
|
|
67
|
-
export function createChatHandler(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
209
|
+
export function createChatHandler(
|
|
210
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
211
|
+
options?: HandlerOptions
|
|
212
|
+
) {
|
|
213
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
214
|
+
const storage = new DatabaseStorage(plugin.getConfig());
|
|
215
|
+
const onAuthorize = options?.onAuthorize;
|
|
72
216
|
|
|
73
217
|
return async function POST(req: NextRequest) {
|
|
218
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
219
|
+
if (authResult) return authResult as any;
|
|
220
|
+
|
|
221
|
+
// 1. Rate limit check
|
|
222
|
+
const rateLimited = checkRateLimit(req);
|
|
223
|
+
if (rateLimited) return rateLimited;
|
|
224
|
+
|
|
74
225
|
try {
|
|
75
226
|
const body = await req.json();
|
|
76
|
-
const { message, history = [], namespace } = body as {
|
|
227
|
+
const { message: rawMessage, history = [], namespace, sessionId = 'default', messageId, userMessageId } = body as {
|
|
77
228
|
message: string;
|
|
78
229
|
history?: ChatMessage[];
|
|
79
230
|
namespace?: string;
|
|
231
|
+
sessionId?: string;
|
|
232
|
+
messageId?: string;
|
|
233
|
+
userMessageId?: string;
|
|
80
234
|
};
|
|
81
235
|
|
|
82
|
-
|
|
83
|
-
|
|
236
|
+
// 2. Input sanitization
|
|
237
|
+
const sanitized = sanitizeInput(rawMessage ?? '');
|
|
238
|
+
if (!sanitized.ok) {
|
|
239
|
+
return NextResponse.json({ error: { code: 'INVALID_INPUT', message: sanitized.error } }, { status: 400 });
|
|
84
240
|
}
|
|
241
|
+
const message = sanitized.value;
|
|
242
|
+
|
|
243
|
+
// Save user message to history
|
|
244
|
+
const userMsgId = userMessageId || `user_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
245
|
+
await storage.saveMessage(sessionId, {
|
|
246
|
+
id: userMsgId,
|
|
247
|
+
role: 'user',
|
|
248
|
+
content: message,
|
|
249
|
+
}).catch(err => console.warn('[createChatHandler] Failed to save user message:', err));
|
|
85
250
|
|
|
86
251
|
const result = await plugin.chat(message, history, namespace);
|
|
87
|
-
|
|
252
|
+
|
|
253
|
+
// Save assistant message to history
|
|
254
|
+
const assistantMsgId = messageId || `assistant_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
255
|
+
await storage.saveMessage(sessionId, {
|
|
256
|
+
id: assistantMsgId,
|
|
257
|
+
role: 'assistant',
|
|
258
|
+
content: result.reply,
|
|
259
|
+
sources: result.sources,
|
|
260
|
+
uiTransformation: result.ui_transformation,
|
|
261
|
+
trace: result.trace,
|
|
262
|
+
}).catch(err => console.warn('[createChatHandler] Failed to save assistant message:', err));
|
|
263
|
+
|
|
264
|
+
return NextResponse.json({
|
|
265
|
+
...result,
|
|
266
|
+
messageId: assistantMsgId,
|
|
267
|
+
userMessageId: userMsgId,
|
|
268
|
+
});
|
|
88
269
|
} catch (err) {
|
|
89
270
|
const message = err instanceof Error ? err.message : 'Internal server error';
|
|
90
271
|
return NextResponse.json({ error: message }, { status: 500 });
|
|
@@ -114,32 +295,61 @@ export function createChatHandler(configOrPlugin?: Partial<RagConfig> | VectorPl
|
|
|
114
295
|
* // }
|
|
115
296
|
* // }
|
|
116
297
|
*/
|
|
117
|
-
export function createStreamHandler(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
298
|
+
export function createStreamHandler(
|
|
299
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
300
|
+
options?: HandlerOptions
|
|
301
|
+
) {
|
|
302
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
303
|
+
const storage = new DatabaseStorage(plugin.getConfig());
|
|
304
|
+
const onAuthorize = options?.onAuthorize;
|
|
122
305
|
|
|
123
306
|
return async function POST(req: NextRequest) {
|
|
307
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
308
|
+
if (authResult) return authResult as any;
|
|
309
|
+
|
|
310
|
+
// 1. Rate limit check
|
|
311
|
+
const rateLimited = checkRateLimit(req);
|
|
312
|
+
if (rateLimited) return rateLimited;
|
|
313
|
+
|
|
124
314
|
// Parse body first so we can return a normal JSON 400 before opening the stream
|
|
125
|
-
let body: {
|
|
315
|
+
let body: {
|
|
316
|
+
message?: string;
|
|
317
|
+
history?: ChatMessage[];
|
|
318
|
+
namespace?: string;
|
|
319
|
+
sessionId?: string;
|
|
320
|
+
messageId?: string;
|
|
321
|
+
userMessageId?: string;
|
|
322
|
+
};
|
|
126
323
|
try {
|
|
127
324
|
body = await req.json();
|
|
128
325
|
} catch {
|
|
129
|
-
return new Response(JSON.stringify({ error: 'Invalid JSON body' }), {
|
|
326
|
+
return new Response(JSON.stringify({ error: { code: 'INVALID_JSON', message: 'Invalid JSON body' } }), {
|
|
130
327
|
status: 400,
|
|
131
328
|
headers: { 'Content-Type': 'application/json' },
|
|
132
329
|
});
|
|
133
330
|
}
|
|
134
331
|
|
|
135
|
-
const { message, history = [], namespace } = body;
|
|
332
|
+
const { message: rawMessage, history = [], namespace, sessionId = 'default', messageId, userMessageId } = body;
|
|
136
333
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
334
|
+
// 2. Input sanitization
|
|
335
|
+
const sanitized = sanitizeInput(rawMessage ?? '');
|
|
336
|
+
if (!sanitized.ok) {
|
|
337
|
+
return new Response(
|
|
338
|
+
JSON.stringify({ error: { code: 'INVALID_INPUT', message: sanitized.error } }),
|
|
339
|
+
{ status: 400, headers: { 'Content-Type': 'application/json' } }
|
|
340
|
+
);
|
|
142
341
|
}
|
|
342
|
+
const message = sanitized.value;
|
|
343
|
+
|
|
344
|
+
// Save user message to history
|
|
345
|
+
const userMsgId = userMessageId || `user_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
346
|
+
await storage.saveMessage(sessionId, {
|
|
347
|
+
id: userMsgId,
|
|
348
|
+
role: 'user',
|
|
349
|
+
content: message,
|
|
350
|
+
}).catch(err => console.warn('[createStreamHandler] Failed to save user message:', err));
|
|
351
|
+
|
|
352
|
+
const assistantMsgId = messageId || `assistant_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
143
353
|
|
|
144
354
|
const encoder = new TextEncoder();
|
|
145
355
|
let isActive = true;
|
|
@@ -157,10 +367,12 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
157
367
|
|
|
158
368
|
try {
|
|
159
369
|
const pipelineStream = plugin.chatStream(message, history, namespace);
|
|
370
|
+
let fullReply = '';
|
|
160
371
|
|
|
161
372
|
for await (const chunk of pipelineStream) {
|
|
162
373
|
if (!isActive) break;
|
|
163
374
|
if (typeof chunk === 'string') {
|
|
375
|
+
fullReply += chunk;
|
|
164
376
|
enqueue(sseTextFrame(chunk));
|
|
165
377
|
} else if (chunk && typeof chunk === 'object' && 'type' in chunk && (chunk as any).type === 'thinking') {
|
|
166
378
|
enqueue(`data: ${JSON.stringify(chunk)}\n\n`);
|
|
@@ -177,12 +389,13 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
177
389
|
enqueue(sseObservabilityFrame(responseChunk.trace));
|
|
178
390
|
}
|
|
179
391
|
|
|
392
|
+
let uiTransformation = responseChunk?.ui_transformation;
|
|
180
393
|
if (sources.length > 0) {
|
|
181
394
|
try {
|
|
182
395
|
// The pipeline already computed ui_transformation inside askStream().
|
|
183
396
|
// We ONLY fall back to the cheap heuristic transform (no LLM call) when
|
|
184
397
|
// the pipeline didn't produce one — never call analyzeAndDecide() here.
|
|
185
|
-
|
|
398
|
+
uiTransformation =
|
|
186
399
|
responseChunk?.ui_transformation ??
|
|
187
400
|
UITransformer.transform(message, sources, plugin.getConfig());
|
|
188
401
|
|
|
@@ -193,10 +406,23 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
193
406
|
console.warn('[createStreamHandler] UI transformation warning:', transformError);
|
|
194
407
|
try {
|
|
195
408
|
const fallback = UITransformer.transform(message, sources, plugin.getConfig());
|
|
196
|
-
if (fallback)
|
|
409
|
+
if (fallback) {
|
|
410
|
+
uiTransformation = fallback;
|
|
411
|
+
enqueue(sseUIFrame(fallback));
|
|
412
|
+
}
|
|
197
413
|
} catch { /* silent */ }
|
|
198
414
|
}
|
|
199
415
|
}
|
|
416
|
+
|
|
417
|
+
// Save final completed assistant response to history
|
|
418
|
+
await storage.saveMessage(sessionId, {
|
|
419
|
+
id: assistantMsgId,
|
|
420
|
+
role: 'assistant',
|
|
421
|
+
content: fullReply,
|
|
422
|
+
sources,
|
|
423
|
+
uiTransformation,
|
|
424
|
+
trace: responseChunk?.trace,
|
|
425
|
+
}).catch(err => console.warn('[createStreamHandler] Failed to save assistant message:', err));
|
|
200
426
|
}
|
|
201
427
|
}
|
|
202
428
|
} catch (streamError) {
|
|
@@ -224,20 +450,24 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
224
450
|
}
|
|
225
451
|
});
|
|
226
452
|
|
|
227
|
-
return new Response(stream, { headers: SSE_HEADERS });
|
|
453
|
+
return new Response(stream, { headers: { ...SSE_HEADERS, 'x-message-id': assistantMsgId, 'x-user-message-id': userMsgId } });
|
|
228
454
|
};
|
|
229
455
|
}
|
|
230
456
|
|
|
231
457
|
/**
|
|
232
458
|
* createIngestHandler — factory for the document ingestion endpoint.
|
|
233
459
|
*/
|
|
234
|
-
export function createIngestHandler(
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
460
|
+
export function createIngestHandler(
|
|
461
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
462
|
+
options?: HandlerOptions
|
|
463
|
+
) {
|
|
464
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
465
|
+
const onAuthorize = options?.onAuthorize;
|
|
239
466
|
|
|
240
467
|
return async function POST(req: NextRequest) {
|
|
468
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
469
|
+
if (authResult) return authResult as any;
|
|
470
|
+
|
|
241
471
|
try {
|
|
242
472
|
const body = await req.json();
|
|
243
473
|
const { documents, namespace } = body as {
|
|
@@ -261,13 +491,19 @@ export function createIngestHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
261
491
|
/**
|
|
262
492
|
* createHealthHandler — factory for the health-check endpoint.
|
|
263
493
|
*/
|
|
264
|
-
export function createHealthHandler(
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
494
|
+
export function createHealthHandler(
|
|
495
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
496
|
+
options?: HandlerOptions
|
|
497
|
+
) {
|
|
498
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
499
|
+
const onAuthorize = options?.onAuthorize;
|
|
500
|
+
|
|
501
|
+
return async function GET(req?: NextRequest) {
|
|
502
|
+
if (req) {
|
|
503
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
504
|
+
if (authResult) return authResult as any;
|
|
505
|
+
}
|
|
269
506
|
|
|
270
|
-
return async function GET() {
|
|
271
507
|
try {
|
|
272
508
|
const health = await plugin.checkHealth();
|
|
273
509
|
const status = health.allHealthy ? 'ok' : 'degraded';
|
|
@@ -286,13 +522,17 @@ export function createHealthHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
286
522
|
/**
|
|
287
523
|
* createUploadHandler — factory for the file upload ingestion endpoint.
|
|
288
524
|
*/
|
|
289
|
-
export function createUploadHandler(
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
525
|
+
export function createUploadHandler(
|
|
526
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
527
|
+
options?: HandlerOptions
|
|
528
|
+
) {
|
|
529
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
530
|
+
const onAuthorize = options?.onAuthorize;
|
|
294
531
|
|
|
295
532
|
return async function POST(req: NextRequest) {
|
|
533
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
534
|
+
if (authResult) return authResult as any;
|
|
535
|
+
|
|
296
536
|
try {
|
|
297
537
|
const formData = await req.formData();
|
|
298
538
|
const files = formData.getAll('files') as File[];
|
|
@@ -386,13 +626,17 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
|
|
|
386
626
|
/**
|
|
387
627
|
* createSuggestionsHandler — factory for the auto-suggestions endpoint.
|
|
388
628
|
*/
|
|
389
|
-
export function createSuggestionsHandler(
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
629
|
+
export function createSuggestionsHandler(
|
|
630
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
631
|
+
options?: HandlerOptions
|
|
632
|
+
) {
|
|
633
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
634
|
+
const onAuthorize = options?.onAuthorize;
|
|
394
635
|
|
|
395
636
|
return async function POST(req: NextRequest) {
|
|
637
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
638
|
+
if (authResult) return authResult as any;
|
|
639
|
+
|
|
396
640
|
try {
|
|
397
641
|
const body = await req.json();
|
|
398
642
|
const { query, namespace } = body as {
|
|
@@ -413,21 +657,150 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
|
|
|
413
657
|
};
|
|
414
658
|
}
|
|
415
659
|
|
|
660
|
+
/**
|
|
661
|
+
* createHistoryHandler — factory for GET /api/retrivora/history and POST /api/retrivora/history/clear.
|
|
662
|
+
*/
|
|
663
|
+
export function createHistoryHandler(
|
|
664
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
665
|
+
options?: HandlerOptions
|
|
666
|
+
) {
|
|
667
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
668
|
+
const storage = new DatabaseStorage(plugin.getConfig());
|
|
669
|
+
const onAuthorize = options?.onAuthorize;
|
|
670
|
+
|
|
671
|
+
return async function handler(req: NextRequest) {
|
|
672
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
673
|
+
if (authResult) return authResult as any;
|
|
674
|
+
|
|
675
|
+
const url = new URL(req.url);
|
|
676
|
+
const sessionId = url.searchParams.get('sessionId') || 'default';
|
|
677
|
+
|
|
678
|
+
if (req.method === 'GET') {
|
|
679
|
+
try {
|
|
680
|
+
const history = await storage.getHistory(sessionId);
|
|
681
|
+
return NextResponse.json({ history });
|
|
682
|
+
} catch (err) {
|
|
683
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch history';
|
|
684
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
685
|
+
}
|
|
686
|
+
} else if (req.method === 'POST') {
|
|
687
|
+
try {
|
|
688
|
+
const body = await req.json().catch(() => ({}));
|
|
689
|
+
const sid = body.sessionId || sessionId;
|
|
690
|
+
await storage.clearHistory(sid);
|
|
691
|
+
return NextResponse.json({ success: true });
|
|
692
|
+
} catch (err) {
|
|
693
|
+
const message = err instanceof Error ? err.message : 'Failed to clear history';
|
|
694
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 });
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* createFeedbackHandler — factory for POST /api/retrivora/feedback.
|
|
703
|
+
*/
|
|
704
|
+
export function createFeedbackHandler(
|
|
705
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
706
|
+
options?: HandlerOptions
|
|
707
|
+
) {
|
|
708
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
709
|
+
const storage = new DatabaseStorage(plugin.getConfig());
|
|
710
|
+
const onAuthorize = options?.onAuthorize;
|
|
711
|
+
|
|
712
|
+
return async function handler(req: NextRequest) {
|
|
713
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
714
|
+
if (authResult) return authResult as any;
|
|
715
|
+
|
|
716
|
+
if (req.method === 'GET') {
|
|
717
|
+
try {
|
|
718
|
+
const url = new URL(req.url);
|
|
719
|
+
const messageId = url.searchParams.get('messageId');
|
|
720
|
+
if (!messageId) {
|
|
721
|
+
const feedbackList = await storage.listFeedback();
|
|
722
|
+
return NextResponse.json({ feedback: feedbackList });
|
|
723
|
+
}
|
|
724
|
+
const feedback = await storage.getFeedback(messageId);
|
|
725
|
+
return NextResponse.json({ feedback });
|
|
726
|
+
} catch (err) {
|
|
727
|
+
const message = err instanceof Error ? err.message : 'Failed to fetch feedback';
|
|
728
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
729
|
+
}
|
|
730
|
+
} else if (req.method === 'POST') {
|
|
731
|
+
try {
|
|
732
|
+
const body = await req.json();
|
|
733
|
+
const { messageId, sessionId = 'default', rating, comment } = body as {
|
|
734
|
+
messageId: string;
|
|
735
|
+
sessionId?: string;
|
|
736
|
+
rating: 'thumbs_up' | 'thumbs_down';
|
|
737
|
+
comment?: string;
|
|
738
|
+
};
|
|
739
|
+
|
|
740
|
+
if (!messageId) {
|
|
741
|
+
return NextResponse.json({ error: 'messageId is required' }, { status: 400 });
|
|
742
|
+
}
|
|
743
|
+
if (!rating) {
|
|
744
|
+
return NextResponse.json({ error: 'rating is required' }, { status: 400 });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
await storage.saveFeedback({ messageId, sessionId, rating, comment });
|
|
748
|
+
return NextResponse.json({ success: true });
|
|
749
|
+
} catch (err) {
|
|
750
|
+
const message = err instanceof Error ? err.message : 'Failed to save feedback';
|
|
751
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 });
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* createSessionsHandler — factory for GET /api/retrivora/sessions.
|
|
760
|
+
*/
|
|
761
|
+
export function createSessionsHandler(
|
|
762
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
763
|
+
options?: HandlerOptions
|
|
764
|
+
) {
|
|
765
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
766
|
+
const storage = new DatabaseStorage(plugin.getConfig());
|
|
767
|
+
const onAuthorize = options?.onAuthorize;
|
|
768
|
+
|
|
769
|
+
return async function GET(req?: NextRequest) {
|
|
770
|
+
if (req) {
|
|
771
|
+
const authResult = await checkAuth(req, onAuthorize);
|
|
772
|
+
if (authResult) return authResult as any;
|
|
773
|
+
}
|
|
774
|
+
void req;
|
|
775
|
+
try {
|
|
776
|
+
const sessions = await storage.listSessions();
|
|
777
|
+
return NextResponse.json({ sessions });
|
|
778
|
+
} catch (err) {
|
|
779
|
+
const message = err instanceof Error ? err.message : 'Failed to list sessions';
|
|
780
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
781
|
+
}
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
416
785
|
/**
|
|
417
786
|
* createRagHandler — factory that returns GET and POST handlers for a catch-all route
|
|
418
787
|
* (e.g. `src/app/api/retrivora/[[...retrivora]]/route.ts`).
|
|
419
788
|
*/
|
|
420
|
-
export function createRagHandler(
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
const
|
|
789
|
+
export function createRagHandler(
|
|
790
|
+
configOrPlugin?: Partial<RagConfig> | VectorPlugin,
|
|
791
|
+
options?: HandlerOptions
|
|
792
|
+
) {
|
|
793
|
+
const plugin = getOrCreatePlugin(configOrPlugin);
|
|
794
|
+
const onAuthorize = options?.onAuthorize;
|
|
795
|
+
|
|
796
|
+
const chatHandler = createChatHandler(plugin, { onAuthorize });
|
|
797
|
+
const streamHandler = createStreamHandler(plugin, { onAuthorize });
|
|
798
|
+
const uploadHandler = createUploadHandler(plugin, { onAuthorize });
|
|
799
|
+
const healthHandler = createHealthHandler(plugin, { onAuthorize });
|
|
800
|
+
const suggestionsHandler = createSuggestionsHandler(plugin, { onAuthorize });
|
|
801
|
+
const historyHandler = createHistoryHandler(plugin, { onAuthorize });
|
|
802
|
+
const feedbackHandler = createFeedbackHandler(plugin, { onAuthorize });
|
|
803
|
+
const sessionsHandler = createSessionsHandler(plugin, { onAuthorize });
|
|
431
804
|
|
|
432
805
|
async function routePostRequest(req: NextRequest, segment: string) {
|
|
433
806
|
switch (segment) {
|
|
@@ -440,17 +813,30 @@ export function createRagHandler(configOrPlugin?: Partial<RagConfig> | VectorPlu
|
|
|
440
813
|
case 'suggestions':
|
|
441
814
|
return suggestionsHandler(req);
|
|
442
815
|
case 'health':
|
|
443
|
-
return healthHandler();
|
|
816
|
+
return healthHandler(req);
|
|
817
|
+
case 'history':
|
|
818
|
+
case 'history/clear':
|
|
819
|
+
return historyHandler(req);
|
|
820
|
+
case 'feedback':
|
|
821
|
+
return feedbackHandler(req);
|
|
444
822
|
default:
|
|
445
823
|
return NextResponse.json({ error: `Not Found: POST segment "${segment}" not supported.` }, { status: 404 });
|
|
446
824
|
}
|
|
447
825
|
}
|
|
448
826
|
|
|
449
827
|
async function routeGetRequest(req: NextRequest, segment: string) {
|
|
450
|
-
|
|
451
|
-
|
|
828
|
+
switch (segment) {
|
|
829
|
+
case 'health':
|
|
830
|
+
return healthHandler(req);
|
|
831
|
+
case 'history':
|
|
832
|
+
return historyHandler(req);
|
|
833
|
+
case 'feedback':
|
|
834
|
+
return feedbackHandler(req);
|
|
835
|
+
case 'sessions':
|
|
836
|
+
return sessionsHandler(req);
|
|
837
|
+
default:
|
|
838
|
+
return NextResponse.json({ error: `Method Not Allowed: GET segment "${segment}" not supported.` }, { status: 405 });
|
|
452
839
|
}
|
|
453
|
-
return NextResponse.json({ error: `Method Not Allowed: GET is only supported for "health" segment.` }, { status: 405 });
|
|
454
840
|
}
|
|
455
841
|
|
|
456
842
|
async function getSegment(context: any): Promise<string> {
|
|
@@ -459,7 +845,7 @@ export function createRagHandler(configOrPlugin?: Partial<RagConfig> | VectorPlu
|
|
|
459
845
|
: context?.params;
|
|
460
846
|
|
|
461
847
|
const segments: string[] = resolvedParams?.retrivora || [];
|
|
462
|
-
return segments
|
|
848
|
+
return segments.join('/') || 'chat';
|
|
463
849
|
}
|
|
464
850
|
|
|
465
851
|
return {
|