@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.
- package/README.md +59 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.mts → ILLMProvider-Bhk6zJOK.d.mts} +43 -7
- package/dist/{ILLMProvider-Bw2A28nU.d.ts → ILLMProvider-Bhk6zJOK.d.ts} +43 -7
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +737 -237
- package/dist/handlers/index.mjs +736 -237
- package/dist/index-B9J_XEh0.d.ts +187 -0
- package/dist/index-BJ4cd-t5.d.mts +187 -0
- package/dist/{index-B70ZLkfG.d.mts → index-Bu7T6xgr.d.ts} +20 -3
- package/dist/{index-DVu-mkAM.d.ts → index-C3SVtPYg.d.mts} +20 -3
- package/dist/index.css +237 -10
- package/dist/index.d.mts +13 -5
- package/dist/index.d.ts +13 -5
- package/dist/index.js +365 -164
- package/dist/index.mjs +350 -158
- package/dist/server.d.mts +15 -94
- package/dist/server.d.ts +15 -94
- package/dist/server.js +850 -239
- package/dist/server.mjs +839 -238
- package/package.json +2 -4
- package/src/app/api/chat/route.ts +2 -2
- package/src/app/api/health/route.ts +3 -2
- package/src/app/api/ingest/route.ts +3 -2
- package/src/app/api/suggestions/route.ts +3 -2
- package/src/app/api/upload/route.ts +3 -2
- package/src/app/constants.tsx +168 -148
- package/src/app/layout.tsx +5 -18
- package/src/app/types.ts +17 -17
- package/src/components/ChatWidget.tsx +3 -1
- package/src/components/ChatWindow.tsx +5 -1
- package/src/components/DocViewer.tsx +71 -5
- package/src/components/Documentation.tsx +74 -11
- package/src/components/MessageBubble.tsx +39 -2
- package/src/components/ThinkingBlock.tsx +75 -0
- package/src/components/VisualizationRenderer.tsx +27 -1
- package/src/components/constants.tsx +275 -0
- package/src/config/RagConfig.ts +47 -0
- package/src/config/constants.ts +1 -0
- package/src/config/serverConfig.ts +25 -0
- package/src/core/ConfigResolver.ts +73 -22
- package/src/core/ConfigValidator.ts +2 -1
- package/src/core/Pipeline.ts +226 -68
- package/src/core/ProviderRegistry.ts +16 -7
- package/src/core/QueryProcessor.ts +38 -4
- package/src/core/Retrivora.ts +91 -0
- package/src/core/VectorPlugin.ts +62 -8
- package/src/exceptions/index.ts +111 -0
- package/src/handlers/index.ts +73 -0
- package/src/hooks/useRagChat.ts +30 -5
- package/src/index.ts +27 -1
- package/src/lib/plugin.ts +24 -0
- package/src/llm/LLMFactory.ts +8 -4
- package/src/llm/providers/AnthropicProvider.ts +70 -20
- package/src/llm/providers/GeminiProvider.ts +14 -15
- package/src/llm/providers/OllamaProvider.ts +13 -16
- package/src/llm/providers/OpenAIProvider.ts +9 -14
- package/src/llm/providers/UniversalLLMAdapter.ts +5 -5
- package/src/llm/utils.ts +46 -0
- package/src/providers/vectordb/MongoDBProvider.ts +9 -4
- package/src/providers/vectordb/MultiTablePostgresProvider.ts +45 -13
- package/src/rag/EntityExtractor.ts +2 -2
- package/src/rag/Reranker.ts +9 -16
- package/src/server.ts +30 -1
- package/src/types/chat.ts +9 -0
- package/src/types/props.ts +38 -1
- package/src/utils/UITransformer.ts +73 -4
- package/dist/DocumentChunker-Dh9TvmGG.d.mts +0 -45
- 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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
84
|
+
const basePrompt =
|
|
85
|
+
options?.systemPrompt ??
|
|
84
86
|
this.llmConfig.systemPrompt ??
|
|
85
|
-
|
|
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:
|
|
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
|
|
114
|
+
const basePrompt =
|
|
115
|
+
options?.systemPrompt ??
|
|
117
116
|
this.llmConfig.systemPrompt ??
|
|
118
|
-
|
|
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:
|
|
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,
|
package/src/llm/utils.ts
ADDED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
196
|
-
//
|
|
197
|
-
//
|
|
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->>'
|
|
301
|
-
const coalesceExprs = keysToCheck.
|
|
302
|
-
|
|
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(
|
|
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
|
-
//
|
|
333
|
+
// Optimized Subquery CTE Hybrid Search (Uses HNSW vector index & GIN fulltext index)
|
|
321
334
|
sqlQuery = `
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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
|
|
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;
|
package/src/rag/Reranker.ts
CHANGED
|
@@ -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:
|
|
60
|
+
[{ role: 'user', content: `Query: "${query}"\n\nDocuments:\n${topN.map((m, i) => `[${i}] ${m.content.replace(/\n/g, ' ')}`).join('\n')}` }],
|
|
72
61
|
'',
|
|
73
|
-
{
|
|
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 {
|
|
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 {
|
package/src/types/props.ts
CHANGED
|
@@ -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 {
|