@retrivora-ai/rag-engine 1.8.0 → 1.8.2

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 (71) hide show
  1. package/dist/DocumentChunker-Dh9TvmGG.d.mts +45 -0
  2. package/dist/DocumentChunker-Dh9TvmGG.d.ts +45 -0
  3. package/dist/{index-DPsQodME.d.mts → ILLMProvider-BOJFz3Na.d.mts} +104 -1
  4. package/dist/{index-DPsQodME.d.ts → ILLMProvider-BOJFz3Na.d.ts} +104 -1
  5. package/dist/MultiTablePostgresProvider-ZLGSKTJR.mjs +8 -0
  6. package/dist/chunk-ICKRMZQK.mjs +76 -0
  7. package/dist/{chunk-PV3MFHWU.mjs → chunk-LZVVLSDN.mjs} +977 -516
  8. package/dist/chunk-OZFBG4BA.mjs +291 -0
  9. package/dist/handlers/index.d.mts +2 -2
  10. package/dist/handlers/index.d.ts +2 -2
  11. package/dist/handlers/index.js +1269 -656
  12. package/dist/handlers/index.mjs +4 -1
  13. package/dist/{index-Bb2yEopi.d.mts → index-BwpcaziY.d.ts} +10 -2
  14. package/dist/{index-CkbTzj9J.d.ts → index-D3V9Et2M.d.mts} +10 -2
  15. package/dist/index.d.mts +24 -4
  16. package/dist/index.d.ts +24 -4
  17. package/dist/index.js +1354 -826
  18. package/dist/index.mjs +1284 -795
  19. package/dist/server.d.mts +47 -27
  20. package/dist/server.d.ts +47 -27
  21. package/dist/server.js +1417 -829
  22. package/dist/server.mjs +164 -176
  23. package/package.json +6 -2
  24. package/src/app/api/upload/route.ts +4 -0
  25. package/src/app/constants.tsx +2 -2
  26. package/src/app/page.tsx +12 -322
  27. package/src/components/AmbientBackground.tsx +29 -0
  28. package/src/components/ArchitectureCard.tsx +17 -0
  29. package/src/components/ArchitectureCardsSection.tsx +15 -0
  30. package/src/components/ChatWindow.tsx +32 -0
  31. package/src/components/CodeViewer.tsx +51 -0
  32. package/src/components/ConfigProvider.tsx +1 -0
  33. package/src/components/DocViewer.tsx +37 -0
  34. package/src/components/DocumentUpload.tsx +44 -1
  35. package/src/components/Documentation.tsx +58 -0
  36. package/src/components/DynamicChart.tsx +27 -2
  37. package/src/components/Hero.tsx +59 -0
  38. package/src/components/HourglassLoader.tsx +87 -0
  39. package/src/components/Lifecycle.tsx +37 -0
  40. package/src/components/MarkdownComponents.tsx +140 -0
  41. package/src/components/MessageBubble.tsx +124 -904
  42. package/src/components/Navbar.tsx +55 -0
  43. package/src/components/ObservabilityPanel.tsx +374 -0
  44. package/src/components/ProductCard.tsx +5 -3
  45. package/src/components/UIDispatcher.tsx +344 -0
  46. package/src/components/VisualizationRenderer.tsx +372 -250
  47. package/src/config/RagConfig.ts +5 -0
  48. package/src/config/serverConfig.ts +3 -1
  49. package/src/core/Pipeline.ts +240 -271
  50. package/src/core/ProviderRegistry.ts +2 -2
  51. package/src/core/VectorPlugin.ts +9 -0
  52. package/src/handlers/index.ts +91 -15
  53. package/src/hooks/useRagChat.ts +21 -11
  54. package/src/index.ts +9 -1
  55. package/src/llm/LLMFactory.ts +54 -2
  56. package/src/llm/providers/AnthropicProvider.ts +12 -8
  57. package/src/llm/providers/GeminiProvider.ts +188 -143
  58. package/src/llm/providers/OllamaProvider.ts +7 -3
  59. package/src/llm/providers/OpenAIProvider.ts +12 -8
  60. package/src/providers/vectordb/MultiTablePostgresProvider.ts +150 -64
  61. package/src/types/chat.ts +8 -0
  62. package/src/types/index.ts +132 -0
  63. package/src/types/props.ts +9 -1
  64. package/src/utils/ProductExtractor.ts +347 -0
  65. package/src/utils/SchemaMapper.ts +129 -0
  66. package/src/utils/UITransformer.ts +470 -209
  67. package/src/utils/synonyms.ts +78 -0
  68. package/dist/DocumentChunker-C1GEEosY.d.ts +0 -93
  69. package/dist/DocumentChunker-CFEiRopR.d.mts +0 -93
  70. package/dist/PostgreSQLProvider-BMOETDZA.mjs +0 -8
  71. package/dist/chunk-FLOSGE6A.mjs +0 -202
@@ -77,8 +77,8 @@ export class ProviderRegistry {
77
77
  }
78
78
  case 'pgvector':
79
79
  case 'postgresql': {
80
- const { PostgreSQLProvider } = await import('../providers/vectordb/PostgreSQLProvider');
81
- return PostgreSQLProvider;
80
+ const { MultiTablePostgresProvider } = await import('../providers/vectordb/MultiTablePostgresProvider');
81
+ return MultiTablePostgresProvider as unknown as VectorProviderClass;
82
82
  }
83
83
  case 'mongodb': {
84
84
  const { MongoDBProvider } = await import('../providers/vectordb/MongoDBProvider');
@@ -38,6 +38,15 @@ export class VectorPlugin {
38
38
  return this.config;
39
39
  }
40
40
 
41
+ /**
42
+ * Get the initialized LLM provider (available after the first request).
43
+ * Used by handlers to pass to UITransformer.analyzeAndDecide() for
44
+ * LLM-driven visualization decisions.
45
+ */
46
+ getLLMProvider() {
47
+ return this.pipeline.getLLMProvider();
48
+ }
49
+
41
50
  /**
42
51
  * Perform pre-flight health checks on all configured providers.
43
52
  * Useful to verify connectivity before running operations.
@@ -27,7 +27,12 @@ function sseMetaFrame(meta: unknown): string {
27
27
 
28
28
  /** Encode the UI transformation result as an SSE frame. */
29
29
  function sseUIFrame(uiTransformation: unknown): string {
30
- return `data: ${JSON.stringify({ type: 'ui_transformation', ...((uiTransformation as object) ?? {}) })}\n\n`;
30
+ return `data: ${JSON.stringify({ type: 'ui_transformation', data: uiTransformation })}\n\n`;
31
+ }
32
+
33
+ /** Encode the observability trace as an SSE frame. */
34
+ function sseObservabilityFrame(trace: unknown): string {
35
+ return `data: ${JSON.stringify({ type: 'observability', data: trace })}\n\n`;
31
36
  }
32
37
 
33
38
  /** Encode a stream error as an SSE frame. */
@@ -152,15 +157,32 @@ export function createStreamHandler(configOrPlugin?: Partial<RagConfig> | Vector
152
157
  // Retrieval metadata object — always the final frame
153
158
  enqueue(sseMetaFrame(chunk));
154
159
 
155
- // Transform retrieved sources to UI representation
156
- const sources = (chunk as ChatResponse)?.sources || [];
157
- if (sources && sources.length > 0) {
160
+ // ── LLM-driven visualization decision ─────────────────────────
161
+ const responseChunk = chunk as ChatResponse;
162
+ const sources = responseChunk?.sources || [];
163
+
164
+ // Emit observability trace if present
165
+ if (responseChunk?.trace) {
166
+ enqueue(sseObservabilityFrame(responseChunk.trace));
167
+ }
168
+
169
+ if (sources.length > 0) {
158
170
  try {
159
- const uiTransformation = UITransformer.transform(message, sources);
160
- enqueue(sseUIFrame(uiTransformation));
171
+ const llmProvider = plugin.getLLMProvider?.();
172
+ const uiTransformation = responseChunk?.ui_transformation ??
173
+ (llmProvider
174
+ ? await UITransformer.analyzeAndDecide(message, sources, llmProvider)
175
+ : UITransformer.transform(message, sources, plugin.getConfig()));
176
+
177
+ if (uiTransformation) {
178
+ enqueue(sseUIFrame(uiTransformation));
179
+ }
161
180
  } catch (transformError) {
162
181
  console.warn('[createStreamHandler] UI transformation warning:', transformError);
163
- // Don't fail the stream if transformation fails, just skip it
182
+ try {
183
+ const fallback = UITransformer.transform(message, sources, plugin.getConfig());
184
+ if (fallback) enqueue(sseUIFrame(fallback));
185
+ } catch { /* silent */ }
164
186
  }
165
187
  }
166
188
  }
@@ -249,15 +271,68 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
249
271
  const formData = await req.formData();
250
272
  const files = formData.getAll('files') as File[];
251
273
  const namespace = (formData.get('namespace') as string) || undefined;
274
+ const dimensionRaw = formData.get('dimension');
275
+ const dimension = dimensionRaw ? parseInt(dimensionRaw as string, 10) : undefined;
252
276
 
253
277
  if (!files || files.length === 0) {
254
278
  return NextResponse.json({ error: 'No files provided' }, { status: 400 });
255
279
  }
256
280
 
257
- const documents = await Promise.all(
258
- files.map(async (file) => {
281
+ const documents: { docId: string; content: string; metadata?: Record<string, unknown> }[] = [];
282
+
283
+ for (const file of files) {
284
+ if (file.name.toLowerCase().endsWith('.csv') || file.type === 'text/csv') {
285
+ const text = await file.text();
286
+ const Papa = await import('papaparse');
287
+ const parsed = Papa.default.parse(text, { header: true, skipEmptyLines: true });
288
+
289
+ if (parsed.data && parsed.data.length > 0) {
290
+ let i = 0;
291
+ let lastRowData: Record<string, string> | null = null;
292
+
293
+ for (const row of parsed.data) {
294
+ i++;
295
+ const rowData = row as Record<string, string>;
296
+
297
+ // Dynamically determine the grouping key (typically the first column like ID or Handle)
298
+ const groupingKey = Object.keys(rowData)[0];
299
+
300
+ // Forward-fill ALL empty columns for variants belonging to the same group
301
+ if (lastRowData && groupingKey && rowData[groupingKey] && rowData[groupingKey] === lastRowData[groupingKey]) {
302
+ for (const key of Object.keys(rowData)) {
303
+ // Forward-fill if the current row lacks the field but the previous row has it
304
+ if (!rowData[key] && lastRowData[key]) {
305
+ rowData[key] = lastRowData[key];
306
+ }
307
+ }
308
+ }
309
+ // Save a copy of the filled rowData for the next iteration
310
+ lastRowData = { ...rowData };
311
+
312
+ const contentParts: string[] = [];
313
+ for (const [key, val] of Object.entries(rowData)) {
314
+ if (key && val) {
315
+ contentParts.push(`${key}: ${val}`);
316
+ }
317
+ }
318
+
319
+ documents.push({
320
+ docId: `${file.name}-row-${i}`,
321
+ content: contentParts.join(', '),
322
+ metadata: {
323
+ fileName: file.name,
324
+ fileSize: file.size,
325
+ fileType: file.type,
326
+ uploadedAt: new Date().toISOString(),
327
+ ...(dimension ? { dimension } : {}),
328
+ ...rowData
329
+ }
330
+ });
331
+ }
332
+ }
333
+ } else {
259
334
  const content = await DocumentParser.parse(file, file.name, file.type);
260
- return {
335
+ documents.push({
261
336
  docId: file.name,
262
337
  content,
263
338
  metadata: {
@@ -265,10 +340,11 @@ export function createUploadHandler(configOrPlugin?: Partial<RagConfig> | Vector
265
340
  fileSize: file.size,
266
341
  fileType: file.type,
267
342
  uploadedAt: new Date().toISOString(),
343
+ ...(dimension ? { dimension } : {}),
268
344
  },
269
- };
270
- })
271
- );
345
+ });
346
+ }
347
+ }
272
348
 
273
349
  const results = await plugin.ingest(documents, namespace);
274
350
  return NextResponse.json({ message: 'Upload successful', results });
@@ -309,5 +385,5 @@ export function createSuggestionsHandler(configOrPlugin?: Partial<RagConfig> | V
309
385
  };
310
386
  }
311
387
 
312
- // Re-export SSE helper so host apps can use it in custom handlers
313
- export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame };
388
+ // Re-export SSE helpers so host apps can use them in custom handlers
389
+ export { sseFrame, sseTextFrame, sseMetaFrame, sseErrorFrame, sseUIFrame, sseObservabilityFrame };
@@ -19,16 +19,17 @@
19
19
  */
20
20
 
21
21
  import { useState, useCallback, useRef, useEffect } from 'react';
22
- import { VectorMatch, RagMessage, UseRagChatOptions, UseRagChatReturn } from '../types';
22
+ import { VectorMatch, RagMessage, UseRagChatOptions, UseRagChatReturn, ObservabilityTrace } from '../types';
23
23
  import { useStoredMessages } from './useStoredMessages';
24
24
 
25
25
  // ─── SSE Frame Parser ──────────────────────────────────────────────────────────
26
26
 
27
- interface SseTextFrame { type: 'text'; text: string }
28
- interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[] }
29
- interface SseErrorFrame { type: 'error'; error: string }
30
- interface SseUiFrame { type: 'ui_transformation'; data: unknown }
31
- type SseFrame = SseTextFrame | SseMetaFrame | SseErrorFrame | SseUiFrame;
27
+ interface SseTextFrame { type: 'text'; text: string }
28
+ interface SseMetaFrame { type: 'metadata'; sources?: VectorMatch[] }
29
+ interface SseErrorFrame { type: 'error'; error: string }
30
+ interface SseUiFrame { type: 'ui_transformation'; data: unknown }
31
+ interface SseObservabilityFrame { type: 'observability'; data: ObservabilityTrace }
32
+ type SseFrame = SseTextFrame | SseMetaFrame | SseErrorFrame | SseUiFrame | SseObservabilityFrame;
32
33
 
33
34
  /**
34
35
  * Parse all complete SSE frames from a raw chunk string.
@@ -126,6 +127,7 @@ export function useRagChat(
126
127
  let assistantContent = '';
127
128
  let sources: VectorMatch[] = [];
128
129
  let uiTransformation: unknown = null;
130
+ let trace: ObservabilityTrace | undefined;
129
131
  // Buffer for partial SSE frames that arrive split across reads
130
132
  let buffer = '';
131
133
 
@@ -155,12 +157,14 @@ export function useRagChat(
155
157
  buffer = buffer.slice(lastBoundary + 2);
156
158
 
157
159
  for (const frame of parseSseChunk(complete)) {
158
- if (frame.type === 'text') {
160
+ if (frame.type === 'text' && frame.text) {
159
161
  assistantContent += frame.text;
160
162
  } else if (frame.type === 'metadata') {
161
163
  sources = frame.sources ?? [];
162
164
  } else if (frame.type === 'ui_transformation') {
163
165
  uiTransformation = frame.data;
166
+ } else if (frame.type === 'observability') {
167
+ trace = (frame as SseObservabilityFrame).data;
164
168
  } else if (frame.type === 'error') {
165
169
  setError(frame.error || 'Stream error');
166
170
  }
@@ -184,20 +188,26 @@ export function useRagChat(
184
188
  // Process any remaining buffered data
185
189
  if (buffer.trim()) {
186
190
  for (const frame of parseSseChunk(buffer)) {
187
- if (frame.type === 'text') assistantContent += frame.text;
191
+ if (frame.type === 'text' && frame.text) assistantContent += frame.text;
188
192
  else if (frame.type === 'metadata') sources = frame.sources ?? [];
193
+ else if (frame.type === 'observability') trace = (frame as SseObservabilityFrame).data;
189
194
  }
190
195
  }
191
196
 
192
- const finalAssistantMessage: RagMessage = {
197
+ // Final message with all accumulated data
198
+ const finalMsg: RagMessage = {
193
199
  id: assistantMessageId,
194
200
  role: 'assistant',
195
201
  content: assistantContent,
196
202
  sources: sources.length > 0 ? sources : undefined,
197
- uiTransformation: uiTransformation || undefined,
203
+ uiTransformation: uiTransformation ?? undefined,
204
+ trace,
198
205
  createdAt: new Date().toISOString(),
199
206
  };
200
- onReply?.(finalAssistantMessage);
207
+ setMessages((prev) =>
208
+ prev.map((msg) => (msg.id === assistantMessageId ? finalMsg : msg))
209
+ );
210
+ onReply?.(finalMsg);
201
211
  } catch (err) {
202
212
  const msg = err instanceof Error ? err.message : 'Something went wrong. Please try again.';
203
213
  setError(msg);
package/src/index.ts CHANGED
@@ -9,11 +9,15 @@ export { ChatWindow } from './components/ChatWindow';
9
9
  export { DocumentUpload } from './components/DocumentUpload';
10
10
  export { MessageBubble } from './components/MessageBubble';
11
11
  export { SourceCard } from './components/SourceCard';
12
+ export { ObservabilityPanel } from './components/ObservabilityPanel';
12
13
  export { ConfigProvider, useConfig } from './components/ConfigProvider';
13
14
 
14
15
  // ── Headless Hooks ────────────────────────────────────────────
15
16
  export { useRagChat } from './hooks/useRagChat';
16
17
 
18
+ // ── Utils ─────────────────────────────────────────────────────
19
+ export { addSynonyms } from './utils/synonyms';
20
+
17
21
  // ── Types (Interfaces/Types only, no Node.js deps) ─────────────
18
22
  export type { RagConfig, VectorDBConfig, VectorDBProvider, LLMConfig, LLMProvider, EmbeddingConfig, EmbeddingProvider, UIConfig, RAGConfig } from './config/RagConfig';
19
23
  export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
@@ -35,5 +39,9 @@ export type {
35
39
  ProductCardProps,
36
40
  ProductCarouselProps,
37
41
  DocumentUploadProps,
38
- SourceCardProps
42
+ SourceCardProps,
43
+ ObservabilityTrace,
44
+ LatencyBreakdown,
45
+ TokenUsage,
46
+ RetrievedChunk
39
47
  } from './types';
@@ -12,10 +12,50 @@ interface LLMProviderStatic {
12
12
  getHealthChecker?: () => IProviderHealthChecker;
13
13
  }
14
14
 
15
+ /** Registry for custom provider factories registered at runtime. */
16
+ const customProviders = new Map<string, (config: LLMConfig) => ILLMProvider>();
17
+
15
18
  /**
16
19
  * LLMFactory — instantiates the correct ILLMProvider based on llmConfig.provider.
17
20
  */
18
21
  export class LLMFactory {
22
+ /**
23
+ * Register a custom LLM provider factory at runtime.
24
+ *
25
+ * Use this to add support for any LLM backend (Azure OpenAI, Cohere, Mistral,
26
+ * Bedrock, etc.) without modifying this library's source code.
27
+ *
28
+ * @example
29
+ * // In your Next.js app initialization:
30
+ * import { LLMFactory } from '@retrivora-ai/rag-engine/server';
31
+ * import { MyCustomProvider } from './providers/MyCustomProvider';
32
+ *
33
+ * LLMFactory.register('my-provider', (config) => new MyCustomProvider(config));
34
+ *
35
+ * // Then set in your .env.local:
36
+ * // LLM_PROVIDER=my-provider
37
+ */
38
+ static register(name: string, factory: (config: LLMConfig) => ILLMProvider): void {
39
+ customProviders.set(name.toLowerCase(), factory);
40
+ console.log(`[LLMFactory] Registered custom provider: "${name}"`);
41
+ }
42
+
43
+ /**
44
+ * Unregister a previously registered custom provider.
45
+ */
46
+ static unregister(name: string): void {
47
+ customProviders.delete(name.toLowerCase());
48
+ }
49
+
50
+ /**
51
+ * List all registered provider names (built-in + custom).
52
+ */
53
+ static listProviders(): string[] {
54
+ return [
55
+ 'openai', 'anthropic', 'ollama', 'gemini', 'rest', 'universal_rest', 'custom',
56
+ ...Array.from(customProviders.keys()),
57
+ ];
58
+ }
19
59
  static create(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig): ILLMProvider {
20
60
  switch (llmConfig.provider) {
21
61
  case 'openai':
@@ -30,11 +70,23 @@ export class LLMFactory {
30
70
  case 'universal_rest':
31
71
  case 'custom':
32
72
  return new UniversalLLMAdapter(llmConfig);
33
- default:
73
+ default: {
74
+ // Check the runtime custom provider registry first
75
+ const providerName = String(llmConfig.provider ?? '').toLowerCase();
76
+ const customFactory = customProviders.get(providerName);
77
+ if (customFactory) {
78
+ return customFactory(llmConfig);
79
+ }
80
+ // Fall back to UniversalLLMAdapter if a baseUrl is provided
34
81
  if (llmConfig.baseUrl || (llmConfig.options as Record<string, unknown>)?.baseUrl) {
35
82
  return new UniversalLLMAdapter(llmConfig);
36
83
  }
37
- throw new Error(`[LLMFactory] Unknown provider "${llmConfig.provider}"`);
84
+ throw new Error(
85
+ `[LLMFactory] Unknown provider "${llmConfig.provider}". ` +
86
+ `Built-in providers: ${LLMFactory.listProviders().join(', ')}. ` +
87
+ `Register a custom provider with LLMFactory.register().`
88
+ );
89
+ }
38
90
  }
39
91
  }
40
92
 
@@ -75,13 +75,15 @@ export class AnthropicProvider implements ILLMProvider {
75
75
  }
76
76
 
77
77
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
78
- const systemPrompt =
78
+ const resolvedPrompt = options?.systemPrompt ??
79
79
  this.llmConfig.systemPrompt ??
80
80
  `You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
81
81
 
82
- const system = systemPrompt.includes('{{context}}')
83
- ? systemPrompt.replace('{{context}}', context)
84
- : `${systemPrompt}\n\nContext:\n${context}`;
82
+ const system = resolvedPrompt.includes('{{context}}')
83
+ ? resolvedPrompt.replace('{{context}}', context)
84
+ : options?.systemPrompt
85
+ ? resolvedPrompt // per-call: context is already in the user message
86
+ : `${resolvedPrompt}\n\nContext:\n${context}`;
85
87
 
86
88
  const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
87
89
  role: m.role === 'assistant' ? 'assistant' : 'user',
@@ -100,13 +102,15 @@ export class AnthropicProvider implements ILLMProvider {
100
102
  }
101
103
 
102
104
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
103
- const systemPrompt =
105
+ const resolvedPrompt = options?.systemPrompt ??
104
106
  this.llmConfig.systemPrompt ??
105
107
  `You are a helpful assistant. Use the context below to answer the user's question accurately.\n\nContext:\n${context}`;
106
108
 
107
- const system = systemPrompt.includes('{{context}}')
108
- ? systemPrompt.replace('{{context}}', context)
109
- : `${systemPrompt}\n\nContext:\n${context}`;
109
+ const system = resolvedPrompt.includes('{{context}}')
110
+ ? resolvedPrompt.replace('{{context}}', context)
111
+ : options?.systemPrompt
112
+ ? resolvedPrompt
113
+ : `${resolvedPrompt}\n\nContext:\n${context}`;
110
114
 
111
115
  const anthropicMessages: Anthropic.MessageParam[] = messages.map((m) => ({
112
116
  role: m.role === 'assistant' ? 'assistant' : 'user',