@retrivora-ai/rag-engine 1.9.9 → 2.0.0

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,62 @@ 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
+ if (!enabled) return;
169
+
170
+ const telemetryUrl = telemetryConfig?.url || process.env.TELEMETRY_URL || process.env.NEXT_PUBLIC_TELEMETRY_URL || '/api/telemetry';
171
+ const host = req.headers.get('host') || 'localhost';
172
+
173
+ let absoluteUrl = telemetryUrl;
174
+ if (!telemetryUrl.startsWith('http')) {
175
+ const proto = req.headers.get('x-forwarded-proto') || 'http';
176
+ absoluteUrl = `${proto}://${host}${telemetryUrl}`;
177
+ }
178
+
179
+ const projectId = config.projectId || 'default';
180
+
181
+ fetch(absoluteUrl, {
182
+ method: 'POST',
183
+ headers: {
184
+ 'Content-Type': 'application/json',
185
+ 'x-forwarded-for': req.headers.get('x-forwarded-for') || '',
186
+ 'x-real-ip': req.headers.get('x-real-ip') || '',
187
+ },
188
+ body: JSON.stringify({
189
+ trace,
190
+ licenseKey,
191
+ projectId,
192
+ action,
193
+ status,
194
+ details,
195
+ }),
196
+ }).catch(err => {
197
+ console.warn('[Retrivora Telemetry] Async report warning:', err.message);
198
+ });
199
+ } catch {
200
+ /* non-blocking */
201
+ }
202
+ }
203
+
148
204
  // ─── SSE helpers ──────────────────────────────────────────────────────────────
149
205
 
150
206
  /**
@@ -194,17 +250,6 @@ const SSE_HEADERS: Record<string, string> = {
194
250
 
195
251
  /**
196
252
  * 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
253
  */
209
254
  export function createChatHandler(
210
255
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -261,6 +306,8 @@ export function createChatHandler(
261
306
  trace: result.trace,
262
307
  }).catch(err => console.warn('[createChatHandler] Failed to save assistant message:', err));
263
308
 
309
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${result.trace?.tokens?.totalTokens || 0}, Latency: ${result.trace?.latency?.totalMs || 0}ms`, result.trace);
310
+
264
311
  return NextResponse.json({
265
312
  ...result,
266
313
  messageId: assistantMsgId,
@@ -268,6 +315,7 @@ export function createChatHandler(
268
315
  });
269
316
  } catch (err) {
270
317
  const message = err instanceof Error ? err.message : 'Internal server error';
318
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', message);
271
319
  return NextResponse.json({ error: message }, { status: 500 });
272
320
  }
273
321
  };
@@ -275,25 +323,6 @@ export function createChatHandler(
275
323
 
276
324
  /**
277
325
  * 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
326
  */
298
327
  export function createStreamHandler(
299
328
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -311,7 +340,6 @@ export function createStreamHandler(
311
340
  const rateLimited = checkRateLimit(req);
312
341
  if (rateLimited) return rateLimited;
313
342
 
314
- // Parse body first so we can return a normal JSON 400 before opening the stream
315
343
  let body: {
316
344
  message?: string;
317
345
  history?: ChatMessage[];
@@ -380,11 +408,9 @@ export function createStreamHandler(
380
408
  // Retrieval metadata object — always the final frame
381
409
  enqueue(sseMetaFrame(chunk));
382
410
 
383
- // ── LLM-driven visualization decision ─────────────────────────
384
411
  const responseChunk = chunk as ChatResponse;
385
412
  const sources = responseChunk?.sources || [];
386
413
 
387
- // Emit observability trace if present
388
414
  if (responseChunk?.trace) {
389
415
  enqueue(sseObservabilityFrame(responseChunk.trace));
390
416
  }
@@ -392,9 +418,6 @@ export function createStreamHandler(
392
418
  let uiTransformation = responseChunk?.ui_transformation;
393
419
  if (sources.length > 0) {
394
420
  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
421
  uiTransformation =
399
422
  responseChunk?.ui_transformation ??
400
423
  UITransformer.transform(message, sources, plugin.getConfig());
@@ -423,14 +446,16 @@ export function createStreamHandler(
423
446
  uiTransformation,
424
447
  trace: responseChunk?.trace,
425
448
  }).catch(err => console.warn('[createStreamHandler] Failed to save assistant message:', err));
449
+
450
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'success', `Tokens: ${responseChunk?.trace?.tokens?.totalTokens || 0}, Latency: ${responseChunk?.trace?.latency?.totalMs || 0}ms`, responseChunk?.trace);
426
451
  }
427
452
  }
428
453
  } catch (streamError) {
429
- // Do not log or enqueue errors if the client intentionally cancelled/aborted
430
454
  if (isActive) {
431
455
  const errorMessage =
432
456
  streamError instanceof Error ? streamError.message : String(streamError);
433
457
  console.error('[createStreamHandler] Stream error:', streamError);
458
+ reportTelemetry(req, plugin, 'QUERY_GENERATION', 'error', errorMessage);
434
459
  try {
435
460
  enqueue(sseErrorFrame(errorMessage));
436
461
  } catch { /* silent */ }
@@ -480,9 +505,11 @@ export function createIngestHandler(
480
505
  }
481
506
 
482
507
  const results = await plugin.ingest(documents, namespace);
508
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Ingested ${documents.length} document(s)`);
483
509
  return NextResponse.json({ results });
484
510
  } catch (err) {
485
511
  const message = err instanceof Error ? err.message : 'Internal server error';
512
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
486
513
  return NextResponse.json({ error: message }, { status: 500 });
487
514
  }
488
515
  };
@@ -560,20 +587,15 @@ export function createUploadHandler(
560
587
  for (const row of parsed.data) {
561
588
  i++;
562
589
  const rowData = row as Record<string, string>;
563
-
564
- // Dynamically determine the grouping key (typically the first column like ID or Handle)
565
590
  const groupingKey = Object.keys(rowData)[0];
566
591
 
567
- // Forward-fill ALL empty columns for variants belonging to the same group
568
592
  if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
569
593
  for (const key of Object.keys(rowData)) {
570
- // Forward-fill if the current row lacks the field but the previous row has it
571
594
  if (!rowData[key] && lastRowData[key]) {
572
595
  rowData[key] = lastRowData[key];
573
596
  }
574
597
  }
575
598
  }
576
- // Save a copy of the filled rowData for the next iteration
577
599
  lastRowData = { ...rowData };
578
600
 
579
601
  const contentParts: string[] = [];
@@ -615,9 +637,11 @@ export function createUploadHandler(
615
637
  }
616
638
 
617
639
  const results = await plugin.ingest(documents, namespace);
640
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'success', `Uploaded & ingested ${files.length} file(s)`);
618
641
  return NextResponse.json({ message: 'Upload successful', results });
619
642
  } catch (err) {
620
643
  const message = err instanceof Error ? err.message : 'Upload failed';
644
+ reportTelemetry(req, plugin, 'DOCUMENT_INGESTION', 'error', message);
621
645
  return NextResponse.json({ error: message }, { status: 500 });
622
646
  }
623
647
  };
@@ -625,6 +649,7 @@ export function createUploadHandler(
625
649
 
626
650
  /**
627
651
  * createSuggestionsHandler — factory for the auto-suggestions endpoint.
652
+ * Supports both POST (JSON body) and GET (URL search parameters).
628
653
  */
629
654
  export function createSuggestionsHandler(
630
655
  configOrPlugin?: Partial<RagConfig> | VectorPlugin,
@@ -633,25 +658,34 @@ export function createSuggestionsHandler(
633
658
  const plugin = getOrCreatePlugin(configOrPlugin);
634
659
  const onAuthorize = options?.onAuthorize;
635
660
 
636
- return async function POST(req: NextRequest) {
661
+ return async function handler(req: NextRequest) {
637
662
  const authResult = await checkAuth(req, onAuthorize);
638
663
  if (authResult) return authResult as any;
639
664
 
640
665
  try {
641
- const body = await req.json();
642
- const { query, namespace } = body as {
643
- query: string;
644
- namespace?: string;
645
- };
666
+ let query = '';
667
+ let namespace: string | undefined = undefined;
668
+
669
+ if (req.method === 'POST') {
670
+ const body = await req.json().catch(() => ({}));
671
+ query = body.query || '';
672
+ namespace = body.namespace;
673
+ } else {
674
+ const url = new URL(req.url);
675
+ query = url.searchParams.get('query') || '';
676
+ namespace = url.searchParams.get('namespace') || undefined;
677
+ }
646
678
 
647
679
  if (typeof query !== 'string') {
648
680
  return NextResponse.json({ error: 'query is required' }, { status: 400 });
649
681
  }
650
682
 
651
683
  const suggestions = await plugin.getSuggestions(query, namespace);
684
+ reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'success', `Fetched ${suggestions.length} suggestions for: ${query.slice(0, 30)}`);
652
685
  return NextResponse.json({ suggestions });
653
686
  } catch (err) {
654
687
  const message = err instanceof Error ? err.message : 'Internal server error';
688
+ reportTelemetry(req, plugin, 'AUTO_SUGGESTIONS', 'error', message);
655
689
  return NextResponse.json({ error: message }, { status: 500 });
656
690
  }
657
691
  };
@@ -828,6 +862,8 @@ export function createRagHandler(
828
862
  switch (segment) {
829
863
  case 'health':
830
864
  return healthHandler(req);
865
+ case 'suggestions':
866
+ return suggestionsHandler(req);
831
867
  case 'history':
832
868
  return historyHandler(req);
833
869
  case 'feedback':