@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.
Files changed (62) hide show
  1. package/README.md +130 -113
  2. package/dist/{ILLMProvider-DNhyOYoK.d.mts → ILLMProvider-B8ROITYK.d.mts} +59 -8
  3. package/dist/{ILLMProvider-DNhyOYoK.d.ts → ILLMProvider-B8ROITYK.d.ts} +59 -8
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2376 -489
  7. package/dist/handlers/index.mjs +2372 -489
  8. package/dist/{index-CjQdL0cX.d.ts → index-B8PbEFSY.d.mts} +17 -3
  9. package/dist/{index-CHL1jdYm.d.mts → index-BCPKUAVL.d.ts} +33 -41
  10. package/dist/{index-Hgbwl9X4.d.ts → index-CrxCy36A.d.mts} +33 -41
  11. package/dist/{index-C9v7-tWd.d.mts → index-DNvoi-sV.d.ts} +17 -3
  12. package/dist/index.css +695 -203
  13. package/dist/index.d.mts +37 -7
  14. package/dist/index.d.ts +37 -7
  15. package/dist/index.js +1197 -286
  16. package/dist/index.mjs +1221 -297
  17. package/dist/server.d.mts +62 -6
  18. package/dist/server.d.ts +62 -6
  19. package/dist/server.js +2526 -574
  20. package/dist/server.mjs +2517 -573
  21. package/package.json +12 -10
  22. package/src/app/constants.tsx +207 -212
  23. package/src/app/layout.tsx +4 -28
  24. package/src/app/types.ts +17 -17
  25. package/src/components/AmbientBackground.tsx +10 -10
  26. package/src/components/ArchitectureCard.tsx +43 -7
  27. package/src/components/ArchitectureCardsSection.tsx +37 -4
  28. package/src/components/ChatWidget.tsx +4 -1
  29. package/src/components/ChatWindow.tsx +9 -2
  30. package/src/components/CodeViewer.tsx +19 -14
  31. package/src/components/DocViewer.tsx +75 -15
  32. package/src/components/Documentation.tsx +111 -28
  33. package/src/components/Hero.tsx +103 -20
  34. package/src/components/Lifecycle.tsx +65 -25
  35. package/src/components/MarkdownComponents.tsx +44 -1
  36. package/src/components/MessageBubble.tsx +162 -50
  37. package/src/components/constants.tsx +279 -0
  38. package/src/config/RagConfig.ts +56 -10
  39. package/src/config/constants.ts +5 -0
  40. package/src/config/serverConfig.ts +15 -0
  41. package/src/core/ConfigResolver.ts +30 -25
  42. package/src/core/DatabaseStorage.ts +469 -0
  43. package/src/core/LicenseVerifier.ts +154 -0
  44. package/src/core/MultiAgentCoordinator.ts +239 -0
  45. package/src/core/Pipeline.ts +148 -16
  46. package/src/core/ProviderRegistry.ts +5 -5
  47. package/src/core/Retrivora.ts +52 -6
  48. package/src/core/VectorPlugin.ts +74 -11
  49. package/src/core/mcp.ts +261 -0
  50. package/src/exceptions/index.ts +52 -0
  51. package/src/handlers/index.ts +504 -47
  52. package/src/hooks/useRagChat.ts +100 -43
  53. package/src/hooks/useStoredMessages.ts +15 -4
  54. package/src/index.ts +7 -0
  55. package/src/llm/LLMFactory.ts +15 -6
  56. package/src/llm/providers/GroqProvider.ts +176 -0
  57. package/src/llm/providers/QwenProvider.ts +191 -0
  58. package/src/server.ts +23 -13
  59. package/src/types/chat.ts +16 -0
  60. package/src/types/props.ts +50 -1
  61. package/.env.example +0 -80
  62. package/LICENSE.txt +0 -21
@@ -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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
68
- const plugin =
69
- configOrPlugin instanceof VectorPlugin
70
- ? configOrPlugin
71
- : new VectorPlugin(configOrPlugin);
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
- if (!message?.trim()) {
83
- return NextResponse.json({ error: 'message is required' }, { status: 400 });
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
- return NextResponse.json(result);
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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
118
- const plugin =
119
- configOrPlugin instanceof VectorPlugin
120
- ? configOrPlugin
121
- : new VectorPlugin(configOrPlugin);
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: { message?: string; history?: ChatMessage[]; namespace?: string };
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
- if (!message?.trim()) {
138
- return new Response(JSON.stringify({ error: 'message is required' }), {
139
- status: 400,
140
- headers: { 'Content-Type': 'application/json' },
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
- const uiTransformation =
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) enqueue(sseUIFrame(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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
235
- const plugin =
236
- configOrPlugin instanceof VectorPlugin
237
- ? configOrPlugin
238
- : new VectorPlugin(configOrPlugin);
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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
265
- const plugin =
266
- configOrPlugin instanceof VectorPlugin
267
- ? configOrPlugin
268
- : new VectorPlugin(configOrPlugin);
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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
290
- const plugin =
291
- configOrPlugin instanceof VectorPlugin
292
- ? configOrPlugin
293
- : new VectorPlugin(configOrPlugin);
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(configOrPlugin?: Partial<RagConfig> | VectorPlugin) {
390
- const plugin =
391
- configOrPlugin instanceof VectorPlugin
392
- ? configOrPlugin
393
- : new VectorPlugin(configOrPlugin);
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,5 +657,218 @@ 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
+
785
+ /**
786
+ * createRagHandler — factory that returns GET and POST handlers for a catch-all route
787
+ * (e.g. `src/app/api/retrivora/[[...retrivora]]/route.ts`).
788
+ */
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 });
804
+
805
+ async function routePostRequest(req: NextRequest, segment: string) {
806
+ switch (segment) {
807
+ case 'chat':
808
+ return streamHandler(req);
809
+ case 'chat-sync':
810
+ return chatHandler(req);
811
+ case 'upload':
812
+ return uploadHandler(req);
813
+ case 'suggestions':
814
+ return suggestionsHandler(req);
815
+ case 'health':
816
+ return healthHandler(req);
817
+ case 'history':
818
+ case 'history/clear':
819
+ return historyHandler(req);
820
+ case 'feedback':
821
+ return feedbackHandler(req);
822
+ default:
823
+ return NextResponse.json({ error: `Not Found: POST segment "${segment}" not supported.` }, { status: 404 });
824
+ }
825
+ }
826
+
827
+ async function routeGetRequest(req: NextRequest, segment: string) {
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 });
839
+ }
840
+ }
841
+
842
+ async function getSegment(context: any): Promise<string> {
843
+ const resolvedParams = typeof context?.params?.then === 'function'
844
+ ? await context.params
845
+ : context?.params;
846
+
847
+ const segments: string[] = resolvedParams?.retrivora || [];
848
+ return segments.join('/') || 'chat';
849
+ }
850
+
851
+ return {
852
+ GET: async (req: NextRequest, context: any) => {
853
+ try {
854
+ const segment = await getSegment(context);
855
+ return await routeGetRequest(req, segment);
856
+ } catch (err) {
857
+ const msg = err instanceof Error ? err.message : 'GET Routing failed';
858
+ return NextResponse.json({ error: msg }, { status: 500 });
859
+ }
860
+ },
861
+ POST: async (req: NextRequest, context: any) => {
862
+ try {
863
+ const segment = await getSegment(context);
864
+ return await routePostRequest(req, segment);
865
+ } catch (err) {
866
+ const msg = err instanceof Error ? err.message : 'POST Routing failed';
867
+ return NextResponse.json({ error: msg }, { status: 500 });
868
+ }
869
+ }
870
+ };
871
+ }
872
+
416
873
  // Re-export SSE helpers so host apps can use them in custom handlers
417
874
  export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame, sseObservabilityFrame };