@retrivora-ai/rag-engine 1.9.9 → 2.0.1

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.
@@ -145,6 +145,64 @@ function getOrCreatePlugin(configOrPlugin: Partial<RagConfig> | VectorPlugin | u
145
145
  return _g[cacheKey];
146
146
  }
147
147
 
148
+ // ─── Telemetry Reporter Helper ───────────────────────────────────────────────
149
+
150
+ /**
151
+ * Asynchronously sends telemetry payload to telemetryUrl when telemetry is enabled
152
+ * or when a licenseKey is set. Non-blocking.
153
+ */
154
+ function reportTelemetry(
155
+ req: NextRequest,
156
+ plugin: VectorPlugin,
157
+ action: string,
158
+ status: 'success' | 'error',
159
+ details: string,
160
+ trace?: unknown
161
+ ) {
162
+ try {
163
+ const config = plugin.getConfig();
164
+ const licenseKey = config.licenseKey || process.env.RAG_LICENSE_KEY || process.env.RETRIVORA_LICENSE_KEY || process.env.NEXT_PUBLIC_RETRIVORA_LICENSE_KEY;
165
+ const telemetryConfig = config.telemetry;
166
+
167
+ const enabled = telemetryConfig?.enabled ?? Boolean(licenseKey);
168
+ const defaultUrl = process.env.NODE_ENV === 'development' && !process.env.TELEMETRY_URL && !process.env.NEXT_PUBLIC_TELEMETRY_URL
169
+ ? 'http://localhost:3001/api/telemetry'
170
+ : 'https://retrivora.com/api/telemetry';
171
+
172
+ const telemetryUrl = telemetryConfig?.url || process.env.TELEMETRY_URL || process.env.NEXT_PUBLIC_TELEMETRY_URL || defaultUrl;
173
+ const host = req.headers.get('host') || 'localhost';
174
+
175
+ let absoluteUrl = telemetryUrl;
176
+ if (!telemetryUrl.startsWith('http')) {
177
+ const proto = req.headers.get('x-forwarded-proto') || 'http';
178
+ absoluteUrl = `${proto}://${host}${telemetryUrl}`;
179
+ }
180
+
181
+ const projectId = config.projectId || 'default';
182
+
183
+ fetch(absoluteUrl, {
184
+ method: 'POST',
185
+ headers: {
186
+ 'Content-Type': 'application/json',
187
+ 'x-forwarded-for': req.headers.get('x-forwarded-for') || '',
188
+ 'x-real-ip': req.headers.get('x-real-ip') || '',
189
+ },
190
+ body: JSON.stringify({
191
+ trace,
192
+ licenseKey,
193
+ projectId,
194
+ action,
195
+ status,
196
+ details,
197
+ }),
198
+ }).catch(err => {
199
+ console.warn('[Retrivora Telemetry] Async report warning:', err.message);
200
+ });
201
+ } catch {
202
+ /* non-blocking */
203
+ }
204
+ }
205
+
148
206
  // ─── SSE helpers ──────────────────────────────────────────────────────────────
149
207
 
150
208
  /**
@@ -194,17 +252,6 @@ const SSE_HEADERS: Record<string, string> = {
194
252
 
195
253
  /**
196
254
  * createChatHandler — factory that returns a Next.js App Router POST handler.
197
- *
198
- * Accepts either a full/partial `RagConfig` **or** a pre-built `VectorPlugin`
199
- * instance (preferred for singleton/multi-tenant setups).
200
- *
201
- * @example
202
- * // Option A — pass config (plugin created once at module load time)
203
- * export const POST = createChatHandler(getRagConfig());
204
- *
205
- * // Option B — pass a pre-built plugin (most flexible)
206
- * const plugin = new VectorPlugin(getRagConfig());
207
- * export const POST = createChatHandler(plugin);
208
255
  */
209
256
  export function createChatHandler(
210
257
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -261,6 +308,8 @@ export function createChatHandler(
261
308
  trace: result.trace,
262
309
  }).catch(err => console.warn('[createChatHandler] Failed to save assistant message:', err));
263
310
 
311
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${result.trace?.tokens?.totalTokens || 0}, Latency: ${result.trace?.latency?.totalMs || 0}ms`, result.trace);
312
+
264
313
  return NextResponse.json({
265
314
  ...result,
266
315
  messageId: assistantMsgId,
@@ -268,6 +317,7 @@ export function createChatHandler(
268
317
  });
269
318
  } catch (err) {
270
319
  const message = err instanceof Error ? err.message : 'Internal server error';
320
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', message);
271
321
  return NextResponse.json({ error: message }, { status: 500 });
272
322
  }
273
323
  };
@@ -275,25 +325,6 @@ export function createChatHandler(
275
325
 
276
326
  /**
277
327
  * createStreamHandler — factory for a streaming SSE Next.js App Router POST handler.
278
- *
279
- * Streams the assistant response as Server-Sent Events (SSE). Each frame is a
280
- * JSON object with a `type` discriminant:
281
- * - `{ type: "text", text: "..." }` — incremental text chunk
282
- * - `{ type: "metadata", sources: [...] }` — retrieval metadata (last frame)
283
- * - `{ type: "error", error: "..." }` — stream-level error
284
- *
285
- * @example
286
- * // src/app/api/chat/route.ts
287
- * export const POST = createStreamHandler(getRagConfig());
288
- *
289
- * // Client-side parsing in useRagChat.ts:
290
- * // const lines = chunk.split('\n');
291
- * // for (const line of lines) {
292
- * // if (line.startsWith('data: ')) {
293
- * // const frame = JSON.parse(line.slice(6));
294
- * // if (frame.type === 'text') { ... }
295
- * // }
296
- * // }
297
328
  */
298
329
  export function createStreamHandler(
299
330
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -311,7 +342,6 @@ export function createStreamHandler(
311
342
  const rateLimited = checkRateLimit(req);
312
343
  if (rateLimited) return rateLimited;
313
344
 
314
- // Parse body first so we can return a normal JSON 400 before opening the stream
315
345
  let body: {
316
346
  message?: string;
317
347
  history?: ChatMessage[];
@@ -380,11 +410,9 @@ export function createStreamHandler(
380
410
  // Retrieval metadata object — always the final frame
381
411
  enqueue(sseMetaFrame(chunk));
382
412
 
383
- // ── LLM-driven visualization decision ─────────────────────────
384
413
  const responseChunk = chunk as ChatResponse;
385
414
  const sources = responseChunk?.sources || [];
386
415
 
387
- // Emit observability trace if present
388
416
  if (responseChunk?.trace) {
389
417
  enqueue(sseObservabilityFrame(responseChunk.trace));
390
418
  }
@@ -392,9 +420,6 @@ export function createStreamHandler(
392
420
  let uiTransformation = responseChunk?.ui_transformation;
393
421
  if (sources.length > 0) {
394
422
  try {
395
- // The pipeline already computed ui_transformation inside askStream().
396
- // We ONLY fall back to the cheap heuristic transform (no LLM call) when
397
- // the pipeline didn't produce one — never call analyzeAndDecide() here.
398
423
  uiTransformation =
399
424
  responseChunk?.ui_transformation ??
400
425
  UITransformer.transform(message, sources, plugin.getConfig());
@@ -423,14 +448,16 @@ export function createStreamHandler(
423
448
  uiTransformation,
424
449
  trace: responseChunk?.trace,
425
450
  }).catch(err => console.warn('[createStreamHandler] Failed to save assistant message:', err));
451
+
452
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${responseChunk?.trace?.tokens?.totalTokens || 0}, Latency: ${responseChunk?.trace?.latency?.totalMs || 0}ms`, responseChunk?.trace);
426
453
  }
427
454
  }
428
455
  } catch (streamError) {
429
- // Do not log or enqueue errors if the client intentionally cancelled/aborted
430
456
  if (isActive) {
431
457
  const errorMessage =
432
458
  streamError instanceof Error ? streamError.message : String(streamError);
433
459
  console.error('[createStreamHandler] Stream error:', streamError);
460
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', errorMessage);
434
461
  try {
435
462
  enqueue(sseErrorFrame(errorMessage));
436
463
  } catch { /* silent */ }
@@ -480,9 +507,11 @@ export function createIngestHandler(
480
507
  }
481
508
 
482
509
  const results = await plugin.ingest(documents, namespace);
510
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Ingested ${documents.length} document(s)`);
483
511
  return NextResponse.json({ results });
484
512
  } catch (err) {
485
513
  const message = err instanceof Error ? err.message : 'Internal server error';
514
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
486
515
  return NextResponse.json({ error: message }, { status: 500 });
487
516
  }
488
517
  };
@@ -551,38 +580,33 @@ export function createUploadHandler(
551
580
  const text = await file.text();
552
581
  const Papa = await import('papaparse');
553
582
  const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
554
-
583
+
555
584
  if (parsed.data && parsed.data.length > 0) {
556
585
  let i = 0;
557
586
  let lastRowData: Record<string, string> | null = null;
558
587
  const csvCols = Object.keys(parsed.data[0] as Record<string, string>);
559
-
588
+
560
589
  for (const row of parsed.data) {
561
590
  i++;
562
591
  const rowData = row as Record<string, string>;
563
-
564
- // Dynamically determine the grouping key (typically the first column like ID or Handle)
565
592
  const groupingKey = Object.keys(rowData)[0];
566
-
567
- // Forward-fill ALL empty columns for variants belonging to the same group
593
+
568
594
  if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
569
595
  for (const key of Object.keys(rowData)) {
570
- // Forward-fill if the current row lacks the field but the previous row has it
571
596
  if (!rowData[key] && lastRowData[key]) {
572
597
  rowData[key] = lastRowData[key];
573
598
  }
574
599
  }
575
600
  }
576
- // Save a copy of the filled rowData for the next iteration
577
601
  lastRowData = { ...rowData };
578
-
602
+
579
603
  const contentParts: string[] = [];
580
604
  for (const [key, val] of Object.entries(rowData)) {
581
605
  if (key && val) {
582
606
  contentParts.push(`${key}: ${val}`);
583
607
  }
584
608
  }
585
-
609
+
586
610
  documents.push({
587
611
  docId: `${file.name}-row-${i}`,
588
612
  content: contentParts.join(', '),
@@ -615,9 +639,11 @@ export function createUploadHandler(
615
639
  }
616
640
 
617
641
  const results = await plugin.ingest(documents, namespace);
642
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Uploaded & ingested ${files.length} file(s)`);
618
643
  return NextResponse.json({ message: 'Upload successful', results });
619
644
  } catch (err) {
620
645
  const message = err instanceof Error ? err.message : 'Upload failed';
646
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
621
647
  return NextResponse.json({ error: message }, { status: 500 });
622
648
  }
623
649
  };
@@ -625,6 +651,7 @@ export function createUploadHandler(
625
651
 
626
652
  /**
627
653
  * createSuggestionsHandler — factory for the auto-suggestions endpoint.
654
+ * Supports both POST (JSON body) and GET (URL search parameters).
628
655
  */
629
656
  export function createSuggestionsHandler(
630
657
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -633,25 +660,34 @@ export function createSuggestionsHandler(
633
660
  const plugin = getOrCreatePlugin(configOrPlugin);
634
661
  const onAuthorize = options?.onAuthorize;
635
662
 
636
- return async function POST(req: NextRequest) {
663
+ return async function handler(req: NextRequest) {
637
664
  const authResult = await checkAuth(req, onAuthorize);
638
665
  if (authResult) return authResult as any;
639
666
 
640
667
  try {
641
- const body = await req.json();
642
- const { query, namespace } = body as {
643
- query: string;
644
- namespace?: string;
645
- };
668
+ let query = '';
669
+ let namespace: string | undefined = undefined;
670
+
671
+ if (req.method === 'POST') {
672
+ const body = await req.json().catch(() => ({}));
673
+ query = body.query || '';
674
+ namespace = body.namespace;
675
+ } else {
676
+ const url = new URL(req.url);
677
+ query = url.searchParams.get('query') || '';
678
+ namespace = url.searchParams.get('namespace') || undefined;
679
+ }
646
680
 
647
681
  if (typeof query !== 'string') {
648
682
  return NextResponse.json({ error: 'query is required' }, { status: 400 });
649
683
  }
650
684
 
651
685
  const suggestions = await plugin.getSuggestions(query, namespace);
686
+ reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'success', `Fetched ${suggestions.length} suggestions for: ${query.slice(0, 30)}`);
652
687
  return NextResponse.json({ suggestions });
653
688
  } catch (err) {
654
689
  const message = err instanceof Error ? err.message : 'Internal server error';
690
+ reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'error', message);
655
691
  return NextResponse.json({ error: message }, { status: 500 });
656
692
  }
657
693
  };
@@ -828,6 +864,8 @@ export function createRagHandler(
828
864
  switch (segment) {
829
865
  case 'health':
830
866
  return healthHandler(req);
867
+ case 'suggestions':
868
+ return suggestionsHandler(req);
831
869
  case 'history':
832
870
  return historyHandler(req);
833
871
  case 'feedback':
@@ -843,7 +881,7 @@ export function createRagHandler(
843
881
  const resolvedParams = typeof context?.params?.then === 'function'
844
882
  ? await context.params
845
883
  : context?.params;
846
-
884
+
847
885
  const segments: string[] = resolvedParams?.retrivora || [];
848
886
  return segments.join('/') || 'chat';
849
887
  }