@retrivora-ai/rag-engine 1.9.3 → 1.9.7

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 (69) hide show
  1. package/README.md +59 -7
  2. package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
  3. package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +737 -237
  7. package/dist/handlers/index.mjs +736 -237
  8. package/dist/index-B9J_XEh0.d.ts +187 -0
  9. package/dist/index-BJ4cd-t5.d.mts +187 -0
  10. package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
  11. package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
  12. package/dist/index.css +237 -10
  13. package/dist/index.d.mts +13 -5
  14. package/dist/index.d.ts +13 -5
  15. package/dist/index.js +365 -164
  16. package/dist/index.mjs +350 -158
  17. package/dist/server.d.mts +15 -94
  18. package/dist/server.d.ts +15 -94
  19. package/dist/server.js +850 -239
  20. package/dist/server.mjs +839 -238
  21. package/package.json +2 -4
  22. package/src/app/api/chat/route.ts +2 -2
  23. package/src/app/api/health/route.ts +3 -2
  24. package/src/app/api/ingest/route.ts +3 -2
  25. package/src/app/api/suggestions/route.ts +3 -2
  26. package/src/app/api/upload/route.ts +3 -2
  27. package/src/app/constants.tsx +168 -148
  28. package/src/app/layout.tsx +5 -18
  29. package/src/app/types.ts +17 -17
  30. package/src/components/ChatWidget.tsx +3 -1
  31. package/src/components/ChatWindow.tsx +5 -1
  32. package/src/components/DocViewer.tsx +71 -5
  33. package/src/components/Documentation.tsx +74 -11
  34. package/src/components/MessageBubble.tsx +39 -2
  35. package/src/components/ThinkingBlock.tsx +75 -0
  36. package/src/components/VisualizationRenderer.tsx +27 -1
  37. package/src/components/constants.tsx +275 -0
  38. package/src/config/RagConfig.ts +47 -0
  39. package/src/config/constants.ts +1 -0
  40. package/src/config/serverConfig.ts +25 -0
  41. package/src/core/ConfigResolver.ts +73 -22
  42. package/src/core/ConfigValidator.ts +2 -1
  43. package/src/core/Pipeline.ts +226 -68
  44. package/src/core/ProviderRegistry.ts +16 -7
  45. package/src/core/QueryProcessor.ts +38 -4
  46. package/src/core/Retrivora.ts +91 -0
  47. package/src/core/VectorPlugin.ts +62 -8
  48. package/src/exceptions/index.ts +111 -0
  49. package/src/handlers/index.ts +73 -0
  50. package/src/hooks/useRagChat.ts +30 -5
  51. package/src/index.ts +27 -1
  52. package/src/lib/plugin.ts +24 -0
  53. package/src/llm/LLMFactory.ts +8 -4
  54. package/src/llm/providers/AnthropicProvider.ts +70 -20
  55. package/src/llm/providers/GeminiProvider.ts +14 -15
  56. package/src/llm/providers/OllamaProvider.ts +13 -16
  57. package/src/llm/providers/OpenAIProvider.ts +9 -14
  58. package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
  59. package/src/llm/utils.ts +46 -0
  60. package/src/providers/vectordb/MongoDBProvider.ts +9 -4
  61. package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
  62. package/src/rag/EntityExtractor.ts +2 -2
  63. package/src/rag/Reranker.ts +9 -16
  64. package/src/server.ts +30 -1
  65. package/src/types/chat.ts +9 -0
  66. package/src/types/props.ts +38 -1
  67. package/src/utils/UITransformer.ts +73 -4
  68. package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
  69. package/dist/DocumentChunker-Dh9TvmGG.d.ts +0 -45
@@ -20,6 +20,7 @@ import { ChatMessage, ChatOptions } from '../../types';
20
20
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
21
21
  import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
22
22
  import { ValidationError } from '../../core/ConfigValidator';
23
+ import { buildSystemContent } from '../utils';
23
24
 
24
25
  const DEFAULT_EMBEDDING_MODEL = 'text-embedding-004';
25
26
  const DEFAULT_TEMPERATURE = 0.7;
@@ -165,18 +166,6 @@ export class GeminiProvider implements ILLMProvider {
165
166
  return sanitizeModel(optionsModel ?? this.embeddingConfig?.model ?? DEFAULT_EMBEDDING_MODEL);
166
167
  }
167
168
 
168
- /**
169
- * Build the system instruction string, inserting the RAG context either via
170
- * the `{{context}}` placeholder or by appending it.
171
- */
172
- private buildSystemInstruction(context: string): string {
173
- const base =
174
- this.llmConfig.systemPrompt ??
175
- `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
176
-
177
- return base.includes('{{context}}') ? base.replace('{{context}}', context) : `${base}\n\nContext:\n${context}`;
178
- }
179
-
180
169
  /**
181
170
  * Convert ChatMessage[] to the Gemini contents format.
182
171
  *
@@ -199,9 +188,14 @@ export class GeminiProvider implements ILLMProvider {
199
188
  // -------------------------------------------------------------------------
200
189
 
201
190
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
191
+ const basePrompt =
192
+ options?.systemPrompt ??
193
+ this.llmConfig.systemPrompt ??
194
+ 'You are a helpful assistant. Answer questions based on the provided context.';
195
+
202
196
  const model = this.client.getGenerativeModel({
203
197
  model: this.llmConfig.model,
204
- systemInstruction: this.buildSystemInstruction(context),
198
+ systemInstruction: buildSystemContent(basePrompt, context),
205
199
  });
206
200
 
207
201
  const result = await model.generateContent({
@@ -217,9 +211,14 @@ export class GeminiProvider implements ILLMProvider {
217
211
  }
218
212
 
219
213
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
214
+ const basePrompt =
215
+ options?.systemPrompt ??
216
+ this.llmConfig.systemPrompt ??
217
+ 'You are a helpful assistant. Answer questions based on the provided context.';
218
+
220
219
  const model = this.client.getGenerativeModel({
221
220
  model: this.llmConfig.model,
222
- systemInstruction: this.buildSystemInstruction(context),
221
+ systemInstruction: buildSystemContent(basePrompt, context),
223
222
  });
224
223
 
225
224
  const result = await model.generateContentStream({
@@ -309,4 +308,4 @@ export class GeminiProvider implements ILLMProvider {
309
308
  return false;
310
309
  }
311
310
  }
312
- }
311
+ }
@@ -8,6 +8,7 @@ import { ChatMessage, ChatOptions } from '../../types';
8
8
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
9
9
  import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
10
10
  import { ValidationError } from '../../core/ConfigValidator';
11
+ import { buildSystemContent } from '../utils';
11
12
 
12
13
  interface OllamaChatResponse {
13
14
  message: { content: string };
@@ -24,7 +25,8 @@ export class OllamaProvider implements ILLMProvider {
24
25
 
25
26
  constructor(llmConfig: LLMConfig, embeddingConfig?: EmbeddingConfig) {
26
27
  const baseURL = llmConfig.baseUrl ?? 'http://localhost:11434';
27
- this.http = axios.create({ baseURL, timeout: 120_000 });
28
+ const timeout = Number(llmConfig.options?.timeout) || 300_000;
29
+ this.http = axios.create({ baseURL, timeout });
28
30
  this.llmConfig = llmConfig;
29
31
  this.embeddingConfig = embeddingConfig;
30
32
  }
@@ -85,7 +87,11 @@ export class OllamaProvider implements ILLMProvider {
85
87
  }
86
88
 
87
89
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
88
- const system = this.buildSystemPrompt(context, options?.systemPrompt);
90
+ const basePrompt =
91
+ options?.systemPrompt ??
92
+ this.llmConfig.systemPrompt ??
93
+ 'You are a helpful assistant. Use the provided context to answer the user\'s question.';
94
+ const system = buildSystemContent(basePrompt, context);
89
95
 
90
96
  const { data } = await this.http.post<OllamaChatResponse>('/api/chat', {
91
97
  model: this.llmConfig.model,
@@ -104,7 +110,11 @@ export class OllamaProvider implements ILLMProvider {
104
110
  }
105
111
 
106
112
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
107
- const system = this.buildSystemPrompt(context, options?.systemPrompt);
113
+ const basePrompt =
114
+ options?.systemPrompt ??
115
+ this.llmConfig.systemPrompt ??
116
+ 'You are a helpful assistant. Use the provided context to answer the user\'s question.';
117
+ const system = buildSystemContent(basePrompt, context);
108
118
 
109
119
  const response = await this.http.post('/api/chat', {
110
120
  model: this.llmConfig.model,
@@ -159,19 +169,6 @@ export class OllamaProvider implements ILLMProvider {
159
169
  }
160
170
  }
161
171
 
162
- private buildSystemPrompt(context: string, overridePrompt?: string): string {
163
- if (overridePrompt) {
164
- // Per-call override: use as-is (context is already embedded in user message)
165
- return overridePrompt;
166
- }
167
- const systemPrompt =
168
- this.llmConfig.systemPrompt ??
169
- `You are a helpful assistant. Use the provided context to answer the user's question.\n\nContext:\n${context}`;
170
-
171
- return systemPrompt.includes('{{context}}')
172
- ? systemPrompt.replace('{{context}}', context)
173
- : `${systemPrompt}\n\nContext:\n${context}`;
174
- }
175
172
 
176
173
  async embed(text: string, options?: EmbedOptions): Promise<number[]> {
177
174
  const model =
@@ -8,6 +8,7 @@ import { ChatMessage, ChatOptions } from '../../types';
8
8
  import { LLMConfig, EmbeddingConfig } from '../../config/RagConfig';
9
9
  import { IProviderValidator, IProviderHealthChecker, HealthCheckResult } from '../../core/ProviderInterfaces';
10
10
  import { ValidationError } from '../../core/ConfigValidator';
11
+ import { buildSystemContent } from '../utils';
11
12
 
12
13
  export class OpenAIProvider implements ILLMProvider {
13
14
  private readonly client: OpenAI;
@@ -80,17 +81,14 @@ export class OpenAIProvider implements ILLMProvider {
80
81
  }
81
82
 
82
83
  async chat(messages: ChatMessage[], context: string, options?: ChatOptions): Promise<string> {
83
- const resolvedSystemPrompt = options?.systemPrompt ??
84
+ const basePrompt =
85
+ options?.systemPrompt ??
84
86
  this.llmConfig.systemPrompt ??
85
- `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
87
+ 'You are a helpful assistant. Answer questions based on the provided context.';
86
88
 
87
89
  const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
88
90
  role: 'system',
89
- content: resolvedSystemPrompt.includes('{{context}}')
90
- ? resolvedSystemPrompt.replace('{{context}}', context)
91
- : options?.systemPrompt
92
- ? resolvedSystemPrompt // per-call override: use as-is, context already in user message
93
- : `${resolvedSystemPrompt}\n\nContext:\n${context}`,
91
+ content: buildSystemContent(basePrompt, context),
94
92
  };
95
93
 
96
94
  const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -113,17 +111,14 @@ export class OpenAIProvider implements ILLMProvider {
113
111
  }
114
112
 
115
113
  async *chatStream(messages: ChatMessage[], context: string, options?: ChatOptions): AsyncIterable<string> {
116
- const resolvedSystemPrompt = options?.systemPrompt ??
114
+ const basePrompt =
115
+ options?.systemPrompt ??
117
116
  this.llmConfig.systemPrompt ??
118
- `You are a helpful assistant. Answer questions based on the provided context.\n\nContext:\n${context}`;
117
+ 'You are a helpful assistant. Answer questions based on the provided context.';
119
118
 
120
119
  const systemMessage: OpenAI.Chat.ChatCompletionSystemMessageParam = {
121
120
  role: 'system',
122
- content: resolvedSystemPrompt.includes('{{context}}')
123
- ? resolvedSystemPrompt.replace('{{context}}', context)
124
- : options?.systemPrompt
125
- ? resolvedSystemPrompt
126
- : `${resolvedSystemPrompt}\n\nContext:\n${context}`,
121
+ content: buildSystemContent(basePrompt, context),
127
122
  };
128
123
 
129
124
  const formattedMessages: OpenAI.Chat.ChatCompletionMessageParam[] = [
@@ -31,20 +31,20 @@ export class UniversalLLMAdapter implements ILLMProvider {
31
31
 
32
32
  constructor(config: LLMConfig | EmbeddingConfig) {
33
33
  this.model = config.model;
34
-
34
+
35
35
  // Type narrow to extract LLM-specific fields safely
36
36
  const llmConfig = config as LLMConfig;
37
-
37
+
38
38
  // Merge profile defaults if specified in options
39
39
  const options = (llmConfig.options as Record<string, unknown>) ?? {};
40
40
  let profile: Record<string, unknown> = {};
41
-
41
+
42
42
  if (typeof options.profile === 'string') {
43
43
  profile = (LLM_PROFILES as Record<string, Record<string, unknown>>)[options.profile] || {};
44
44
  } else if (typeof options.profile === 'object' && options.profile !== null) {
45
45
  profile = options.profile as Record<string, unknown>;
46
46
  }
47
-
47
+
48
48
  this.opts = { ...profile, ...(llmConfig.options ?? {}) } as UniversalLLMOptions;
49
49
  this.systemPrompt = llmConfig.systemPrompt ?? 'You are a helpful AI assistant. Use the provided context to answer the user.';
50
50
  this.maxTokens = llmConfig.maxTokens ?? 1024;
@@ -71,7 +71,7 @@ export class UniversalLLMAdapter implements ILLMProvider {
71
71
 
72
72
  async chat(messages: ChatMessage[], context?: string): Promise<string> {
73
73
  const path = this.opts.chatPath ?? '/chat/completions';
74
-
74
+
75
75
  const formattedMessages = [
76
76
  { role: 'system', content: `${this.systemPrompt}\n\nContext:\n${context ?? 'None'}` },
77
77
  ...messages,
@@ -0,0 +1,46 @@
1
+ /**
2
+ * LLM utility helpers shared across all provider implementations.
3
+ *
4
+ * These are pure functions with no external dependencies so they can
5
+ * be imported by any provider without creating circular references.
6
+ */
7
+
8
+ /**
9
+ * Builds the final system message content by injecting the RAG context
10
+ * into a system prompt using one of two strategies:
11
+ *
12
+ * 1. **Placeholder substitution** — if the prompt contains `{{context}}`,
13
+ * that token is replaced with the retrieved context string.
14
+ *
15
+ * 2. **Appendage** — if there is no placeholder, the context is appended
16
+ * after a separator so the LLM always receives the retrieved evidence,
17
+ * regardless of whether the caller supplied a per-call `systemPrompt`
18
+ * override.
19
+ *
20
+ * This function is intentionally strict: it never silently drops context.
21
+ * If `context` is empty or the sentinel "No relevant context found." the
22
+ * prompt is returned as-is so the LLM is not polluted with an empty block.
23
+ *
24
+ * @param systemPrompt - The base system prompt (from config or per-call override).
25
+ * @param context - The retrieved RAG context string.
26
+ * @returns - The resolved system message content ready to send.
27
+ */
28
+ export function buildSystemContent(systemPrompt: string, context: string): string {
29
+ const noContext =
30
+ !context ||
31
+ context.trim() === '' ||
32
+ context.trim() === 'No relevant context found.';
33
+
34
+ // Replace {{context}} placeholder regardless of whether context is empty
35
+ // so the prompt is never sent with a raw placeholder token.
36
+ if (systemPrompt.includes('{{context}}')) {
37
+ return systemPrompt.replace('{{context}}', noContext ? '' : context);
38
+ }
39
+
40
+ // Without a placeholder: only append context when there is actual content.
41
+ if (noContext) {
42
+ return systemPrompt;
43
+ }
44
+
45
+ return `${systemPrompt}\n\n---\nContext:\n${context}`;
46
+ }
@@ -153,7 +153,11 @@ export class MongoDBProvider extends BaseVectorProvider {
153
153
  if (key === 'namespace') {
154
154
  vectorSearchFilter.namespace = value;
155
155
  } else {
156
- matchFilter[key] = value;
156
+ if (typeof value === 'string') {
157
+ matchFilter[key] = { $regex: new RegExp(`^${value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}$`, 'i') };
158
+ } else {
159
+ matchFilter[key] = value;
160
+ }
157
161
  }
158
162
  }
159
163
 
@@ -192,9 +196,10 @@ export class MongoDBProvider extends BaseVectorProvider {
192
196
  const results = await this.collection!.aggregate(pipeline).toArray();
193
197
 
194
198
  return (results as unknown as Array<{ _id: string; score: number; [key: string]: unknown }>).map((res) => {
195
- // Normalize MongoDB's vectorSearchScore (which is shifted to [0, 1] e.g. (1+cos)/2)
196
- // back to the standard [-1, 1] cosine space. This ensures the global relevance
197
- // threshold (e.g. 0.4) properly filters out random orthogonal matches.
199
+ // Atlas Vector Search returns cosine similarity mapped to [0, 1] via (1 + cos_sim) / 2.
200
+ // We reverse this mapping with (score * 2) - 1 to restore the standard cosine range
201
+ // of [-1, 1], so downstream scoreThreshold comparisons work consistently across
202
+ // all vector DB providers (which natively return values in [-1, 1]).
198
203
  const normalizedScore = (res.score * 2) - 1;
199
204
 
200
205
  return {
@@ -72,6 +72,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
72
72
  ON ${this.uploadTable}
73
73
  USING hnsw (embedding vector_cosine_ops)
74
74
  `);
75
+ await client.query(`
76
+ CREATE INDEX IF NOT EXISTS "${this.uploadTable}_fts_idx"
77
+ ON "${this.uploadTable}"
78
+ USING gin (to_tsvector('english', content))
79
+ `);
75
80
 
76
81
  const res = await client.query(`
77
82
  SELECT table_name
@@ -158,6 +163,11 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
158
163
  ON "${tableName}"
159
164
  USING hnsw (embedding vector_cosine_ops)
160
165
  `);
166
+ await client.query(`
167
+ CREATE INDEX IF NOT EXISTS "${tableName}_fts_idx"
168
+ ON "${tableName}"
169
+ USING gin (to_tsvector('english', content))
170
+ `);
161
171
 
162
172
  // Ensure this new table is added to our search scope
163
173
  if (!this.tables.includes(tableName)) {
@@ -297,9 +307,12 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
297
307
  const synonyms = FIELD_SYNONYMS[key] ?? [];
298
308
  const keysToCheck = [key, ...synonyms];
299
309
 
300
- // Map each candidate key to LOWER(metadata->>'candidate')
301
- const coalesceExprs = keysToCheck.map(k => `LOWER(metadata->>'${k}')`);
302
- return `COALESCE(${coalesceExprs.join(', ')}) = LOWER($${paramIdx})`;
310
+ // Map each candidate key to LOWER(coalesce(metadata->>'key', to_jsonb(t)->>'key'))
311
+ const coalesceExprs = keysToCheck.flatMap(k => [
312
+ `metadata->>'${k}'`,
313
+ `to_jsonb(t)->>'${k}'`
314
+ ]);
315
+ return `COALESCE(${coalesceExprs.map(expr => `LOWER(${expr})`).join(', ')}) = LOWER($${paramIdx})`;
303
316
  });
304
317
  whereClause = `WHERE ${conditions.join(' AND ')}`;
305
318
  }
@@ -312,31 +325,50 @@ export class MultiTablePostgresProvider extends BaseVectorProvider {
312
325
  CASE WHEN LOWER(val) IN (${entityHints.map((h) => `'${h.replace(/'/g, "''")}'`).join(', ')})
313
326
  THEN 1.0 ELSE 0.0 END
314
327
  ), 0)
315
- FROM jsonb_each_text(to_jsonb(t)) AS kv(key, val)
328
+ FROM jsonb_each_text(COALESCE(v.metadata, k.metadata)) AS kv(key, val)
316
329
  WHERE key IN (${this.searchFields.map(f => `'${f}'`).join(', ')})
317
330
  ) * 3.0`
318
331
  : '';
319
332
 
320
- // Actual Hybrid Search: Semantic Vector Score + Full-Text Keyword Rank
333
+ // Optimized Subquery CTE Hybrid Search (Uses HNSW vector index & GIN fulltext index)
321
334
  sqlQuery = `
322
- SELECT *,
323
- (1 - (embedding <=> $1::vector)) AS vector_score,
324
- COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) AS keyword_score,
325
- ((1 - (embedding <=> $1::vector)) + (COALESCE(ts_rank(to_tsvector('english', (to_jsonb(t) - 'embedding')::text), to_tsquery('english', $2)), 0) * 2.0) ${exactNameScoreExpr}) AS hybrid_score
326
- FROM "${table}" t
327
- ${whereClause}
335
+ WITH vector_search AS (
336
+ SELECT *, (1 - (embedding <=> $1::vector)) AS vector_score
337
+ FROM "${table}" t
338
+ ${whereClause}
339
+ ORDER BY embedding <=> $1::vector ASC
340
+ LIMIT ${tableLimit}
341
+ ),
342
+ keyword_search AS (
343
+ SELECT *, COALESCE(ts_rank(to_tsvector('english', content), to_tsquery('english', $2)), 0) AS keyword_score
344
+ FROM "${table}" t
345
+ WHERE ${whereClause ? whereClause.replace('WHERE', '') : 'TRUE'}
346
+ AND to_tsvector('english', content) @@ to_tsquery('english', $2)
347
+ ORDER BY keyword_score DESC
348
+ LIMIT ${tableLimit}
349
+ )
350
+ SELECT
351
+ COALESCE(v.id, k.id) AS id,
352
+ COALESCE(v.namespace, k.namespace) AS namespace,
353
+ COALESCE(v.content, k.content) AS content,
354
+ COALESCE(v.metadata, k.metadata) AS metadata,
355
+ COALESCE(v.vector_score, 0) AS vector_score,
356
+ COALESCE(k.keyword_score, 0) AS keyword_score,
357
+ (COALESCE(v.vector_score, 0) + COALESCE(k.keyword_score, 0) * 2.0 ${exactNameScoreExpr}) AS hybrid_score
358
+ FROM vector_search v
359
+ FULL OUTER JOIN keyword_search k ON v.id = k.id
328
360
  ORDER BY hybrid_score DESC
329
361
  LIMIT ${tableLimit}
330
362
  `;
331
363
  params = [vectorLiteral, dynamicKeywordQuery, ...filterParams];
332
364
  } else {
333
- // Fallback to Vector-only Search
365
+ // Fallback to Vector-only Search (ordered ASC to use HNSW index)
334
366
  sqlQuery = `
335
367
  SELECT *,
336
368
  (1 - (embedding <=> $1::vector)) AS hybrid_score
337
369
  FROM "${table}" t
338
370
  ${whereClause}
339
- ORDER BY hybrid_score DESC
371
+ ORDER BY embedding <=> $1::vector ASC
340
372
  LIMIT ${tableLimit}
341
373
  `;
342
374
  params = [vectorLiteral, ...filterParams];
@@ -41,7 +41,7 @@ Text to extract from:
41
41
  // 1. Try to find the JSON block using a regex
42
42
  const jsonMatch = response.match(/\{[\s\S]*\}/);
43
43
  const cleanJson = jsonMatch ? jsonMatch[0] : response.trim();
44
-
44
+
45
45
  return JSON.parse(cleanJson);
46
46
  } catch {
47
47
  // 2. If parsing fails, try to repair truncated JSON
@@ -62,7 +62,7 @@ Text to extract from:
62
62
  */
63
63
  private repairTruncatedJson(json: string): string | null {
64
64
  let text = json.trim();
65
-
65
+
66
66
  // Find the first { and work from there
67
67
  const startIdx = text.indexOf('{');
68
68
  if (startIdx === -1) return null;
@@ -6,8 +6,8 @@ export class Reranker {
6
6
  * Re-ranks matches based on a secondary relevance score using an LLM.
7
7
  */
8
8
  async rerank(
9
- matches: VectorMatch[],
10
- query: string,
9
+ matches: VectorMatch[],
10
+ query: string,
11
11
  limit: number = 5,
12
12
  llm?: ILLMProvider
13
13
  ): Promise<VectorMatch[]> {
@@ -27,7 +27,7 @@ export class Reranker {
27
27
  const scoredMatches = matches.map(match => {
28
28
  const contentLower = match.content.toLowerCase();
29
29
  let keywordScore = 0;
30
-
30
+
31
31
  keywords.forEach(keyword => {
32
32
  if (contentLower.includes(keyword)) {
33
33
  keywordScore += 1;
@@ -55,22 +55,15 @@ export class Reranker {
55
55
  try {
56
56
  // Take top 10 matches to avoid large context and high latency
57
57
  const topN = matches.slice(0, 10);
58
-
59
- const prompt = `You are a relevance ranking expert.
60
- Given the following user query and a list of retrieved document chunks, rank the chunks by relevance to the query.
61
- Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant.
62
- Use the indices provided in brackets like [0], [1], etc.
63
- Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.
64
-
65
- Query: "${query}"
66
-
67
- Documents:
68
- ${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}`;
69
58
 
70
59
  const response = await llm.chat(
71
- [{ role: 'user', content: prompt }],
60
+ [{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}` }],
72
61
  '',
73
- { temperature: 0, maxTokens: 50 }
62
+ {
63
+ systemPrompt: 'You are a relevance ranking expert. Given the user query and a list of retrieved document chunks, rank the chunks by relevance to the query. Return the result as a comma-separated list of document indices in order of relevance, from most relevant to least relevant. Use the indices provided in brackets like [0], [1], etc. Only return the indices (e.g., "2,0,1"), nothing else. Do not include brackets in the output.',
64
+ temperature: 0,
65
+ maxTokens: 50
66
+ }
74
67
  );
75
68
 
76
69
  const cleanedResponse = response.trim().replace(/[\[\]\s]/g, '');
package/src/server.ts CHANGED
@@ -6,7 +6,20 @@
6
6
  */
7
7
 
8
8
  // ── Shared Types ──────────────────────────────────────────────
9
- export type { RagConfig, VectorDBConfig, LLMConfig, EmbeddingConfig, UIConfig, RAGConfig, VectorDBProvider, LLMProvider, EmbeddingProvider } from './config/RagConfig';
9
+ export type {
10
+ RagConfig,
11
+ UniversalRagConfig,
12
+ RetrievalConfig,
13
+ WorkflowConfig,
14
+ VectorDBConfig,
15
+ LLMConfig,
16
+ EmbeddingConfig,
17
+ UIConfig,
18
+ RAGConfig,
19
+ VectorDBProvider,
20
+ LLMProvider,
21
+ EmbeddingProvider,
22
+ } from './config/RagConfig';
10
23
  export type { VectorMatch, UpsertDocument, IngestDocument, ChatResponse, ChatMessage, ChatOptions } from './types';
11
24
  export type { ILLMProvider, EmbedOptions } from './llm/ILLMProvider';
12
25
  export type { Chunk, ChunkOptions } from './rag/DocumentChunker';
@@ -15,6 +28,7 @@ export type { ValidationError } from './core/ConfigValidator';
15
28
  export type { BatchOptions, BatchResult } from './core/BatchProcessor';
16
29
 
17
30
  // ── Core Orchestration ─────────────────────────────────────────
31
+ export { Retrivora } from './core/Retrivora';
18
32
  export { VectorPlugin } from './core/VectorPlugin';
19
33
  export { Pipeline } from './core/Pipeline';
20
34
  export { ConfigResolver } from './core/ConfigResolver';
@@ -60,8 +74,23 @@ export {
60
74
  createIngestHandler,
61
75
  createHealthHandler,
62
76
  createUploadHandler,
77
+ createRagHandler,
63
78
  sseFrame,
64
79
  sseTextFrame,
65
80
  sseMetaFrame,
66
81
  sseErrorFrame,
67
82
  } from './handlers';
83
+
84
+ // ── Exceptions ────────────────────────────────────────────────
85
+ export type { RetrivoraErrorCode } from './exceptions';
86
+ export {
87
+ RetrivoraError,
88
+ ProviderNotFoundException,
89
+ EmbeddingFailedException,
90
+ RetrievalException,
91
+ RateLimitException,
92
+ ConfigurationException,
93
+ AuthenticationException,
94
+ wrapError,
95
+ } from './exceptions';
96
+
package/src/types/chat.ts CHANGED
@@ -14,6 +14,13 @@ export interface RagMessage extends ChatMessage {
14
14
  /** Full observability trace emitted by the backend. Only present on assistant messages. */
15
15
  trace?: import('./index').ObservabilityTrace;
16
16
  createdAt: string;
17
+ /**
18
+ * Chain-of-thought reasoning from the LLM.
19
+ * Populated by native Anthropic extended thinking or simulated <think> tag stripping.
20
+ */
21
+ thinking?: string;
22
+ /** Wall-clock ms the model spent in the thinking phase. */
23
+ thinkingMs?: number;
17
24
  }
18
25
 
19
26
  export interface ChatOptions {
@@ -42,6 +49,8 @@ export interface UseRagChatOptions {
42
49
  onReply?: (message: RagMessage) => void;
43
50
  /** Called on error */
44
51
  onError?: (error: string) => void;
52
+ /** Optional custom headers to send with the chat request */
53
+ headers?: Record<string, string>;
45
54
  }
46
55
 
47
56
  export interface UseRagChatReturn {
@@ -1,8 +1,37 @@
1
- import { ReactNode, CSSProperties, MouseEvent } from 'react';
1
+ import { ReactNode, CSSProperties, MouseEvent, ElementType } from 'react';
2
2
  import { Product, VectorMatch } from './index';
3
3
  import { RagMessage } from './chat';
4
4
  import { UIConfig } from '../config/RagConfig';
5
5
 
6
+ export interface ArchitectureCardProps {
7
+ icon: ReactNode;
8
+ title: string;
9
+ description: string;
10
+ badge: string;
11
+ badgeColor: string;
12
+ }
13
+
14
+ export interface Snippet {
15
+ id: string;
16
+ title: string;
17
+ description: string;
18
+ code: string;
19
+ language: string;
20
+ }
21
+
22
+ export interface PipelineStep {
23
+ step: string;
24
+ Icon: ElementType;
25
+ title: string;
26
+ desc: string;
27
+ colors: { from: string; to: string };
28
+ }
29
+
30
+ export interface ProviderPill {
31
+ Icon: ElementType;
32
+ label: string;
33
+ }
34
+
6
35
  export type ChatViewportSize = 'compact' | 'medium' | 'large';
7
36
 
8
37
  export interface ChatWindowProps {
@@ -26,6 +55,10 @@ export interface ChatWindowProps {
26
55
  isMaximized?: boolean;
27
56
  /** Called when the user clicks 'Add to Cart' on a product */
28
57
  onAddToCart?: (product: Product) => void;
58
+ /** Optional custom API URL to send chat messages to */
59
+ apiUrl?: string;
60
+ /** Optional custom headers to send with the chat request */
61
+ headers?: Record<string, string>;
29
62
  }
30
63
 
31
64
  export interface ChatWidgetProps {
@@ -33,6 +66,10 @@ export interface ChatWidgetProps {
33
66
  position?: 'bottom-right' | 'bottom-left';
34
67
  /** Called when the user clicks 'Add to Cart' on a product */
35
68
  onAddToCart?: (product: Product) => void;
69
+ /** Optional custom API URL to send chat messages to */
70
+ apiUrl?: string;
71
+ /** Optional custom headers to send with the chat request */
72
+ headers?: Record<string, string>;
36
73
  }
37
74
 
38
75
  export interface MessageBubbleProps {