@retrivora-ai/rag-engine 1.9.7 → 1.9.9
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 +136 -136
- package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
- package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
- package/dist/handlers/index.d.mts +2 -2
- package/dist/handlers/index.d.ts +2 -2
- package/dist/handlers/index.js +2327 -474
- package/dist/handlers/index.mjs +2329 -473
- package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
- package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
- package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
- package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
- package/dist/index.css +783 -550
- package/dist/index.d.mts +35 -7
- package/dist/index.d.ts +35 -7
- package/dist/index.js +419 -282
- package/dist/index.mjs +426 -292
- package/dist/server.d.mts +65 -6
- package/dist/server.d.ts +65 -6
- package/dist/server.js +2185 -317
- package/dist/server.mjs +2185 -317
- package/package.json +13 -8
- package/src/app/constants.tsx +37 -7
- package/src/app/page.tsx +2 -0
- package/src/components/ChatWidget.tsx +2 -1
- package/src/components/ChatWindow.tsx +4 -1
- package/src/components/CodeViewer.tsx +19 -14
- package/src/components/MarkdownComponents.tsx +44 -1
- package/src/components/MessageBubble.tsx +162 -50
- package/src/components/constants.tsx +228 -0
- package/src/config/RagConfig.ts +48 -2
- package/src/config/constants.ts +4 -0
- package/src/config/serverConfig.ts +15 -0
- package/src/core/ConfigResolver.ts +38 -6
- package/src/core/DatabaseStorage.ts +469 -0
- package/src/core/LicenseVerifier.ts +260 -0
- package/src/core/MultiAgentCoordinator.ts +239 -0
- package/src/core/Pipeline.ts +151 -18
- package/src/core/Retrivora.ts +7 -0
- package/src/core/VectorPlugin.ts +12 -3
- package/src/core/mcp.ts +261 -0
- package/src/handlers/index.ts +449 -63
- package/src/hooks/useRagChat.ts +96 -42
- package/src/hooks/useStoredMessages.ts +15 -4
- package/src/index.ts +3 -0
- package/src/llm/LLMFactory.ts +9 -1
- package/src/llm/providers/GroqProvider.ts +176 -0
- package/src/llm/providers/QwenProvider.ts +191 -0
- package/src/server.ts +7 -0
- package/src/types/chat.ts +14 -0
- package/src/types/props.ts +12 -0
- package/.env.example +0 -80
- package/LICENSE.txt +0 -21
- package/src/components/AmbientBackground.tsx +0 -29
- package/src/components/ArchitectureCard.tsx +0 -17
- package/src/components/ArchitectureCardsSection.tsx +0 -15
- package/src/components/DocViewer.tsx +0 -103
- package/src/components/Documentation.tsx +0 -121
- package/src/components/Hero.tsx +0 -59
- package/src/components/Lifecycle.tsx +0 -37
- package/src/components/Navbar.tsx +0 -55
package/src/config/RagConfig.ts
CHANGED
|
@@ -157,6 +157,19 @@ export interface UIConfig {
|
|
|
157
157
|
// RAG Pipeline Tuning
|
|
158
158
|
// ---------------------------------------------------------------------------
|
|
159
159
|
|
|
160
|
+
export interface CAGConfig {
|
|
161
|
+
/** Enable Cold RAG (Cache Augmented Generation) */
|
|
162
|
+
enabled?: boolean;
|
|
163
|
+
/** Namespace containing the cold data (e.g., static manuals/guides) */
|
|
164
|
+
coldNamespace?: string;
|
|
165
|
+
/** Limit of cold documents to retrieve at initialization and keep in cache/context */
|
|
166
|
+
coldLimit?: number;
|
|
167
|
+
/** Specific search query to retrieve the cold documents (e.g., "manual", "documentation") */
|
|
168
|
+
coldQuery?: string;
|
|
169
|
+
/** Specific document IDs to load as cold context */
|
|
170
|
+
coldDocumentIds?: string[];
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
export interface RAGConfig {
|
|
161
174
|
/** Number of top-K chunks retrieved per query */
|
|
162
175
|
topK?: number;
|
|
@@ -169,7 +182,7 @@ export interface RAGConfig {
|
|
|
169
182
|
/** Characters used to split text, in order of priority */
|
|
170
183
|
separators?: string[];
|
|
171
184
|
/** Overall RAG architecture pattern */
|
|
172
|
-
architecture?: 'simple' | 'graph' | 'hybrid' | 'agentic';
|
|
185
|
+
architecture?: 'simple' | 'graph' | 'hybrid' | 'agentic' | 'naive' | 'retrieve-and-rerank' | 'multimodal' | 'agentic-router' | 'multi-agent';
|
|
173
186
|
/** Chunking strategy to use during ingestion */
|
|
174
187
|
chunkingStrategy?: 'recursive' | 'markdown' | 'semantic' | 'llamaindex';
|
|
175
188
|
/** Whether to use query transformation (e.g. HyDE) */
|
|
@@ -197,6 +210,18 @@ export interface RAGConfig {
|
|
|
197
210
|
* Defaults to built-in list (find, search, tell me about, etc.).
|
|
198
211
|
*/
|
|
199
212
|
vectorKeywords?: string[];
|
|
213
|
+
/** Cold RAG (Cache Augmented Generation) configuration */
|
|
214
|
+
cag?: CAGConfig;
|
|
215
|
+
/** Chat history configuration */
|
|
216
|
+
history?: {
|
|
217
|
+
enabled?: boolean;
|
|
218
|
+
tableName?: string;
|
|
219
|
+
};
|
|
220
|
+
/** User feedback configuration */
|
|
221
|
+
feedback?: {
|
|
222
|
+
enabled?: boolean;
|
|
223
|
+
tableName?: string;
|
|
224
|
+
};
|
|
200
225
|
}
|
|
201
226
|
|
|
202
227
|
export interface RetrievalConfig {
|
|
@@ -224,16 +249,28 @@ export interface RetrievalConfig {
|
|
|
224
249
|
|
|
225
250
|
export interface WorkflowConfig {
|
|
226
251
|
/** High-level workflow selector from the SDK prompt. */
|
|
227
|
-
type?: 'simple-rag' | 'rag' | 'hybrid-rag' | 'graph-rag' | 'agentic-rag' | 'agentic';
|
|
252
|
+
type?: 'simple-rag' | 'rag' | 'hybrid-rag' | 'graph-rag' | 'agentic-rag' | 'agentic' | 'naive-rag' | 'retrieve-and-rerank' | 'multimodal-rag' | 'agentic-router' | 'multi-agent-rag' | 'multi-agent';
|
|
228
253
|
/** Future workflow/provider-specific options. */
|
|
229
254
|
options?: Record<string, unknown>;
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
export interface MCPServerConfig {
|
|
258
|
+
name: string;
|
|
259
|
+
transport: 'stdio' | 'sse';
|
|
260
|
+
url?: string; // Required for SSE transport
|
|
261
|
+
command?: string; // Required for Stdio transport
|
|
262
|
+
args?: string[]; // Optional for Stdio transport
|
|
263
|
+
env?: Record<string, string>; // Optional env variables for Stdio child process
|
|
264
|
+
}
|
|
265
|
+
|
|
232
266
|
// ---------------------------------------------------------------------------
|
|
233
267
|
// Root Config
|
|
234
268
|
// ---------------------------------------------------------------------------
|
|
235
269
|
|
|
236
270
|
export interface RagConfig {
|
|
271
|
+
/** License key for commercial production validation */
|
|
272
|
+
licenseKey?: string;
|
|
273
|
+
|
|
237
274
|
/**
|
|
238
275
|
* Unique identifier for the consuming project.
|
|
239
276
|
* Used as the vector DB namespace to achieve multi-tenancy.
|
|
@@ -257,6 +294,15 @@ export interface RagConfig {
|
|
|
257
294
|
|
|
258
295
|
/** Optional Graph database configuration */
|
|
259
296
|
graphDb?: GraphDBConfig;
|
|
297
|
+
|
|
298
|
+
/** Optional Telemetry configuration for observability logging */
|
|
299
|
+
telemetry?: {
|
|
300
|
+
enabled?: boolean;
|
|
301
|
+
url?: string;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/** Optional Model Context Protocol (MCP) server configurations */
|
|
305
|
+
mcpServers?: MCPServerConfig[];
|
|
260
306
|
}
|
|
261
307
|
|
|
262
308
|
/**
|
package/src/config/constants.ts
CHANGED
|
@@ -26,6 +26,8 @@ export const LLM_PROVIDERS = [
|
|
|
26
26
|
'anthropic',
|
|
27
27
|
'ollama',
|
|
28
28
|
'gemini',
|
|
29
|
+
'groq',
|
|
30
|
+
'qwen',
|
|
29
31
|
'rest',
|
|
30
32
|
'universal_rest',
|
|
31
33
|
'custom',
|
|
@@ -35,6 +37,7 @@ export const EMBEDDING_PROVIDERS = [
|
|
|
35
37
|
'openai',
|
|
36
38
|
'ollama',
|
|
37
39
|
'gemini',
|
|
40
|
+
'qwen',
|
|
38
41
|
'rest',
|
|
39
42
|
'universal_rest',
|
|
40
43
|
'custom',
|
|
@@ -51,6 +54,7 @@ export const PROVIDERS_WITH_EMBEDDINGS = [
|
|
|
51
54
|
'openai',
|
|
52
55
|
'ollama',
|
|
53
56
|
'gemini',
|
|
57
|
+
'qwen',
|
|
54
58
|
'rest',
|
|
55
59
|
'universal_rest',
|
|
56
60
|
] as const;
|
|
@@ -149,6 +149,7 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
149
149
|
|
|
150
150
|
return {
|
|
151
151
|
projectId,
|
|
152
|
+
licenseKey: readString(env, 'RETRIVORA_LICENSE_KEY') ?? readString(env, 'LICENSE_KEY') ?? base?.licenseKey,
|
|
152
153
|
vectorDb: {
|
|
153
154
|
provider: vectorProvider,
|
|
154
155
|
indexName:
|
|
@@ -217,6 +218,10 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
217
218
|
try { return JSON.parse(raw) as Record<string, string>; } catch { return undefined; }
|
|
218
219
|
})(),
|
|
219
220
|
},
|
|
221
|
+
telemetry: {
|
|
222
|
+
enabled: readString(env, 'TELEMETRY_ENABLED') === 'true' || readString(env, 'NEXT_PUBLIC_TELEMETRY_ENABLED') === 'true' || base?.telemetry?.enabled || false,
|
|
223
|
+
url: readString(env, 'TELEMETRY_URL') ?? readString(env, 'NEXT_PUBLIC_TELEMETRY_URL') ?? base?.telemetry?.url ?? '/api/telemetry',
|
|
224
|
+
},
|
|
220
225
|
// Optional graph DB — driven by GRAPH_DB_PROVIDER env var
|
|
221
226
|
...(readString(env, 'GRAPH_DB_PROVIDER') ? {
|
|
222
227
|
graphDb: {
|
|
@@ -228,5 +233,15 @@ export function getEnvConfig(env: Record<string, string | undefined> = process.e
|
|
|
228
233
|
},
|
|
229
234
|
},
|
|
230
235
|
} : {}),
|
|
236
|
+
mcpServers: (() => {
|
|
237
|
+
const raw = readString(env, 'MCP_SERVERS') ?? readString(env, 'NEXT_PUBLIC_MCP_SERVERS');
|
|
238
|
+
if (!raw) return undefined;
|
|
239
|
+
try {
|
|
240
|
+
return JSON.parse(raw);
|
|
241
|
+
} catch (e) {
|
|
242
|
+
console.warn('[serverConfig] Failed to parse MCP_SERVERS env:', e);
|
|
243
|
+
return undefined;
|
|
244
|
+
}
|
|
245
|
+
})(),
|
|
231
246
|
};
|
|
232
247
|
}
|
|
@@ -61,6 +61,12 @@ export class ConfigResolver {
|
|
|
61
61
|
: envConfig.embedding,
|
|
62
62
|
ui: hostConfig.ui ? mergeDefined(envConfig.ui, hostConfig.ui) : envConfig.ui,
|
|
63
63
|
rag: hostConfig.rag ? { ...envConfig.rag, ...hostConfig.rag } : envConfig.rag,
|
|
64
|
+
telemetry: hostConfig.telemetry
|
|
65
|
+
? {
|
|
66
|
+
...envConfig.telemetry,
|
|
67
|
+
...hostConfig.telemetry,
|
|
68
|
+
}
|
|
69
|
+
: envConfig.telemetry,
|
|
64
70
|
};
|
|
65
71
|
}
|
|
66
72
|
|
|
@@ -111,22 +117,48 @@ export class ConfigResolver {
|
|
|
111
117
|
normalized.useReranking = retrieval.useReranking ?? normalized.useReranking;
|
|
112
118
|
|
|
113
119
|
if (retrieval.strategy === 'hybrid') normalized.architecture = normalized.architecture ?? 'hybrid';
|
|
114
|
-
if (retrieval.strategy === 'agentic') normalized.architecture = '
|
|
115
|
-
if (retrieval.strategy === 'contextual-compression')
|
|
120
|
+
if (retrieval.strategy === 'agentic') normalized.architecture = 'multi-agent';
|
|
121
|
+
if (retrieval.strategy === 'contextual-compression') {
|
|
122
|
+
normalized.useReranking = true;
|
|
123
|
+
normalized.architecture = normalized.architecture ?? 'retrieve-and-rerank';
|
|
124
|
+
}
|
|
116
125
|
}
|
|
117
126
|
|
|
118
127
|
if (workflow?.type) {
|
|
119
128
|
const type = workflow.type;
|
|
120
|
-
if (type === '
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
if (type === 'naive-rag') {
|
|
130
|
+
normalized.architecture = 'naive';
|
|
131
|
+
normalized.useReranking = false;
|
|
132
|
+
normalized.useGraphRetrieval = false;
|
|
133
|
+
} else if (type === 'retrieve-and-rerank') {
|
|
134
|
+
normalized.architecture = 'retrieve-and-rerank';
|
|
135
|
+
normalized.useReranking = true;
|
|
136
|
+
} else if (type === 'multimodal-rag') {
|
|
137
|
+
normalized.architecture = 'multimodal';
|
|
138
|
+
} else if (type === 'graph-rag') {
|
|
123
139
|
normalized.architecture = 'graph';
|
|
124
140
|
normalized.useGraphRetrieval = true;
|
|
141
|
+
} else if (type === 'hybrid-rag') {
|
|
142
|
+
normalized.architecture = 'hybrid';
|
|
143
|
+
} else if (type === 'agentic-router') {
|
|
144
|
+
normalized.architecture = 'agentic-router';
|
|
145
|
+
} else if (type === 'multi-agent-rag' || type === 'multi-agent' || type === 'agentic' || type === 'agentic-rag') {
|
|
146
|
+
normalized.architecture = 'multi-agent';
|
|
125
147
|
} else if (type === 'simple-rag' || type === 'rag') {
|
|
126
|
-
normalized.architecture = normalized.architecture ?? '
|
|
148
|
+
normalized.architecture = normalized.architecture ?? 'naive';
|
|
127
149
|
}
|
|
128
150
|
}
|
|
129
151
|
|
|
152
|
+
// Default fallbacks based on architecture configuration
|
|
153
|
+
if (normalized.architecture === 'retrieve-and-rerank') {
|
|
154
|
+
normalized.useReranking = true;
|
|
155
|
+
} else if (normalized.architecture === 'graph') {
|
|
156
|
+
normalized.useGraphRetrieval = true;
|
|
157
|
+
} else if (normalized.architecture === 'naive') {
|
|
158
|
+
normalized.useReranking = false;
|
|
159
|
+
normalized.useGraphRetrieval = false;
|
|
160
|
+
}
|
|
161
|
+
|
|
130
162
|
return normalized;
|
|
131
163
|
}
|
|
132
164
|
}
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { RagConfig } from '../config/RagConfig';
|
|
4
|
+
import { LicenseVerifier } from './LicenseVerifier';
|
|
5
|
+
|
|
6
|
+
export interface HistoryMessage {
|
|
7
|
+
id: string;
|
|
8
|
+
role: string;
|
|
9
|
+
content: string;
|
|
10
|
+
sources?: any;
|
|
11
|
+
uiTransformation?: any;
|
|
12
|
+
trace?: any;
|
|
13
|
+
createdAt?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface FeedbackItem {
|
|
17
|
+
id: string;
|
|
18
|
+
messageId: string;
|
|
19
|
+
sessionId: string;
|
|
20
|
+
rating: string;
|
|
21
|
+
comment?: string;
|
|
22
|
+
createdAt?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export class DatabaseStorage {
|
|
26
|
+
private config: RagConfig;
|
|
27
|
+
private provider: string;
|
|
28
|
+
// Dynamic PG Pool
|
|
29
|
+
private pgPool: any = null;
|
|
30
|
+
// Dynamic MongoDB Client
|
|
31
|
+
private mongoClient: any = null;
|
|
32
|
+
private mongoDbName = '';
|
|
33
|
+
|
|
34
|
+
private historyTableName: string;
|
|
35
|
+
private feedbackTableName: string;
|
|
36
|
+
|
|
37
|
+
// Filesystem fallback configuration
|
|
38
|
+
private fallbackDir = path.join(process.cwd(), '.retrivora');
|
|
39
|
+
private historyFile = path.join(this.fallbackDir, 'history.json');
|
|
40
|
+
private feedbackFile = path.join(this.fallbackDir, 'feedback.json');
|
|
41
|
+
|
|
42
|
+
constructor(config: RagConfig) {
|
|
43
|
+
this.config = config;
|
|
44
|
+
this.provider = config.vectorDb?.provider || 'fs';
|
|
45
|
+
|
|
46
|
+
// Retrieve and sanitize table/collection names (alphanumeric + underscores only)
|
|
47
|
+
const rawHistoryTable = config.rag?.history?.tableName || 'retrivora_history';
|
|
48
|
+
const rawFeedbackTable = config.rag?.feedback?.tableName || 'retrivora_feedback';
|
|
49
|
+
this.historyTableName = rawHistoryTable.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
50
|
+
this.feedbackTableName = rawFeedbackTable.replace(/[^a-zA-Z0-9_]/g, '_');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Asserts the user is running a valid Premium/Enterprise license in production.
|
|
55
|
+
* In non-production (development / test) environments this is a complete no-op —
|
|
56
|
+
* all History and Feedback features are freely accessible for local development.
|
|
57
|
+
*/
|
|
58
|
+
private checkPremiumSubscription() {
|
|
59
|
+
// ── Development / test: fully open, no checks ────────────────────────────
|
|
60
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ── Production: license key required ────────────────────────────────────
|
|
65
|
+
if (!this.config.licenseKey) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'[Retrivora SDK] Chat History and Feedback features require a Premium or Enterprise license key in production.'
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const payload = LicenseVerifier.verify(this.config.licenseKey, this.config.projectId);
|
|
73
|
+
const tier = (payload.tier || '').toLowerCase();
|
|
74
|
+
if (
|
|
75
|
+
tier !== 'premium' &&
|
|
76
|
+
tier !== 'enterprise' &&
|
|
77
|
+
tier !== 'growth' &&
|
|
78
|
+
tier !== 'pro' &&
|
|
79
|
+
tier !== 'developer_pro'
|
|
80
|
+
) {
|
|
81
|
+
throw new Error(
|
|
82
|
+
`[Retrivora SDK] Chat History and Feedback features are not available on the "${tier}" tier. ` +
|
|
83
|
+
`Please upgrade to a Developer Pro or Enterprise plan.`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
} catch (err) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`[Retrivora SDK] Subscription check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private checkHistoryEnabled() {
|
|
94
|
+
this.checkPremiumSubscription();
|
|
95
|
+
if (this.config.rag?.history?.enabled === false) {
|
|
96
|
+
throw new Error('[Retrivora SDK] Chat History is disabled in RagConfig.');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private checkFeedbackEnabled() {
|
|
101
|
+
this.checkPremiumSubscription();
|
|
102
|
+
if (this.config.rag?.feedback?.enabled === false) {
|
|
103
|
+
throw new Error('[Retrivora SDK] User Feedback is disabled in RagConfig.');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async getPGPool() {
|
|
108
|
+
const globalKey = `__retrivora_pg_pool_${this.config.projectId}`;
|
|
109
|
+
const _g = global as any;
|
|
110
|
+
if (_g[globalKey]) {
|
|
111
|
+
this.pgPool = _g[globalKey];
|
|
112
|
+
return this.pgPool;
|
|
113
|
+
}
|
|
114
|
+
const opts = this.config.vectorDb.options as Record<string, any>;
|
|
115
|
+
if (!opts.connectionString) {
|
|
116
|
+
throw new Error('[DatabaseStorage] pg connectionString option is missing.');
|
|
117
|
+
}
|
|
118
|
+
const { Pool } = await import('pg');
|
|
119
|
+
this.pgPool = new Pool({ connectionString: opts.connectionString });
|
|
120
|
+
_g[globalKey] = this.pgPool;
|
|
121
|
+
|
|
122
|
+
// Initialize tables using dynamic sanitized table names
|
|
123
|
+
const client = await this.pgPool.connect();
|
|
124
|
+
try {
|
|
125
|
+
await client.query(`
|
|
126
|
+
CREATE TABLE IF NOT EXISTS ${this.historyTableName} (
|
|
127
|
+
id TEXT PRIMARY KEY,
|
|
128
|
+
session_id TEXT NOT NULL,
|
|
129
|
+
role TEXT NOT NULL,
|
|
130
|
+
content TEXT NOT NULL,
|
|
131
|
+
sources JSONB,
|
|
132
|
+
ui_transformation JSONB,
|
|
133
|
+
trace JSONB,
|
|
134
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
135
|
+
)
|
|
136
|
+
`);
|
|
137
|
+
await client.query(`
|
|
138
|
+
CREATE TABLE IF NOT EXISTS ${this.feedbackTableName} (
|
|
139
|
+
id TEXT PRIMARY KEY,
|
|
140
|
+
message_id TEXT NOT NULL UNIQUE,
|
|
141
|
+
session_id TEXT NOT NULL,
|
|
142
|
+
rating TEXT NOT NULL,
|
|
143
|
+
comment TEXT,
|
|
144
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
145
|
+
)
|
|
146
|
+
`);
|
|
147
|
+
} finally {
|
|
148
|
+
client.release();
|
|
149
|
+
}
|
|
150
|
+
return this.pgPool;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private async getMongoClient() {
|
|
154
|
+
const globalKey = `__retrivora_mongo_client_${this.config.projectId}`;
|
|
155
|
+
const _g = global as any;
|
|
156
|
+
if (_g[globalKey]) {
|
|
157
|
+
this.mongoClient = _g[globalKey];
|
|
158
|
+
this.mongoDbName = (this.config.vectorDb.options as any).database;
|
|
159
|
+
return this.mongoClient;
|
|
160
|
+
}
|
|
161
|
+
const opts = this.config.vectorDb.options as Record<string, any>;
|
|
162
|
+
if (!opts.uri || !opts.database) {
|
|
163
|
+
throw new Error('[DatabaseStorage] mongo uri and database options are missing.');
|
|
164
|
+
}
|
|
165
|
+
const { MongoClient } = await import('mongodb');
|
|
166
|
+
this.mongoClient = new MongoClient(opts.uri);
|
|
167
|
+
await this.mongoClient.connect();
|
|
168
|
+
_g[globalKey] = this.mongoClient;
|
|
169
|
+
this.mongoDbName = opts.database;
|
|
170
|
+
return this.mongoClient;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private getMongoDb() {
|
|
174
|
+
return this.mongoClient.db(this.mongoDbName);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Helper for FS fallback reads
|
|
178
|
+
private readLocalFile(filePath: string): any[] {
|
|
179
|
+
if (!fs.existsSync(filePath)) return [];
|
|
180
|
+
try {
|
|
181
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
182
|
+
return JSON.parse(raw) || [];
|
|
183
|
+
} catch {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Helper for FS fallback writes
|
|
189
|
+
private writeLocalFile(filePath: string, data: any[]): void {
|
|
190
|
+
if (!fs.existsSync(this.fallbackDir)) {
|
|
191
|
+
fs.mkdirSync(this.fallbackDir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ─── Message History Operations ──────────────────────────────────────────
|
|
197
|
+
|
|
198
|
+
async saveMessage(
|
|
199
|
+
sessionId: string,
|
|
200
|
+
message: Omit<HistoryMessage, 'createdAt'>
|
|
201
|
+
): Promise<void> {
|
|
202
|
+
this.checkHistoryEnabled();
|
|
203
|
+
const createdAt = new Date().toISOString();
|
|
204
|
+
const msgId = message.id || `msg_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
205
|
+
|
|
206
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
207
|
+
const pool = await this.getPGPool();
|
|
208
|
+
await pool.query(
|
|
209
|
+
`INSERT INTO ${this.historyTableName} (id, session_id, role, content, sources, ui_transformation, trace, created_at)
|
|
210
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
211
|
+
ON CONFLICT (id) DO UPDATE
|
|
212
|
+
SET content = EXCLUDED.content, sources = EXCLUDED.sources, ui_transformation = EXCLUDED.ui_transformation, trace = EXCLUDED.trace`,
|
|
213
|
+
[
|
|
214
|
+
msgId,
|
|
215
|
+
sessionId,
|
|
216
|
+
message.role,
|
|
217
|
+
message.content,
|
|
218
|
+
message.sources ? JSON.stringify(message.sources) : null,
|
|
219
|
+
message.uiTransformation ? JSON.stringify(message.uiTransformation) : null,
|
|
220
|
+
message.trace ? JSON.stringify(message.trace) : null,
|
|
221
|
+
createdAt,
|
|
222
|
+
]
|
|
223
|
+
);
|
|
224
|
+
} else if (this.provider === 'mongodb') {
|
|
225
|
+
await this.getMongoClient();
|
|
226
|
+
const db = this.getMongoDb();
|
|
227
|
+
const col = db.collection(this.historyTableName);
|
|
228
|
+
await col.updateOne(
|
|
229
|
+
{ id: msgId },
|
|
230
|
+
{
|
|
231
|
+
$set: {
|
|
232
|
+
id: msgId,
|
|
233
|
+
session_id: sessionId,
|
|
234
|
+
role: message.role,
|
|
235
|
+
content: message.content,
|
|
236
|
+
sources: message.sources || null,
|
|
237
|
+
ui_transformation: message.uiTransformation || null,
|
|
238
|
+
trace: message.trace || null,
|
|
239
|
+
created_at: createdAt,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
{ upsert: true }
|
|
243
|
+
);
|
|
244
|
+
} else {
|
|
245
|
+
// Local FS fallback
|
|
246
|
+
const history = this.readLocalFile(this.historyFile);
|
|
247
|
+
const index = history.findIndex((h) => h.id === msgId);
|
|
248
|
+
const item = {
|
|
249
|
+
id: msgId,
|
|
250
|
+
session_id: sessionId,
|
|
251
|
+
role: message.role,
|
|
252
|
+
content: message.content,
|
|
253
|
+
sources: message.sources || null,
|
|
254
|
+
ui_transformation: message.uiTransformation || null,
|
|
255
|
+
trace: message.trace || null,
|
|
256
|
+
created_at: createdAt,
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
if (index !== -1) {
|
|
260
|
+
history[index] = item;
|
|
261
|
+
} else {
|
|
262
|
+
history.push(item);
|
|
263
|
+
}
|
|
264
|
+
this.writeLocalFile(this.historyFile, history);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async getHistory(sessionId: string): Promise<HistoryMessage[]> {
|
|
269
|
+
this.checkHistoryEnabled();
|
|
270
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
271
|
+
const pool = await this.getPGPool();
|
|
272
|
+
const res = await pool.query(
|
|
273
|
+
`SELECT id, role, content, sources, ui_transformation as "uiTransformation", trace, created_at as "createdAt"
|
|
274
|
+
FROM ${this.historyTableName}
|
|
275
|
+
WHERE session_id = $1
|
|
276
|
+
ORDER BY created_at ASC`,
|
|
277
|
+
[sessionId]
|
|
278
|
+
);
|
|
279
|
+
return res.rows;
|
|
280
|
+
} else if (this.provider === 'mongodb') {
|
|
281
|
+
await this.getMongoClient();
|
|
282
|
+
const db = this.getMongoDb();
|
|
283
|
+
const col = db.collection(this.historyTableName);
|
|
284
|
+
const items = await col
|
|
285
|
+
.find({ session_id: sessionId })
|
|
286
|
+
.sort({ created_at: 1 })
|
|
287
|
+
.toArray();
|
|
288
|
+
return items.map((item: any) => ({
|
|
289
|
+
id: item.id,
|
|
290
|
+
role: item.role,
|
|
291
|
+
content: item.content,
|
|
292
|
+
sources: item.sources,
|
|
293
|
+
uiTransformation: item.ui_transformation,
|
|
294
|
+
trace: item.trace,
|
|
295
|
+
createdAt: item.created_at,
|
|
296
|
+
}));
|
|
297
|
+
} else {
|
|
298
|
+
const history = this.readLocalFile(this.historyFile);
|
|
299
|
+
return history
|
|
300
|
+
.filter((h) => h.session_id === sessionId)
|
|
301
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime());
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async clearHistory(sessionId: string): Promise<void> {
|
|
306
|
+
this.checkHistoryEnabled();
|
|
307
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
308
|
+
const pool = await this.getPGPool();
|
|
309
|
+
await pool.query(`DELETE FROM ${this.historyTableName} WHERE session_id = $1`, [sessionId]);
|
|
310
|
+
await pool.query(`DELETE FROM ${this.feedbackTableName} WHERE session_id = $1`, [sessionId]);
|
|
311
|
+
} else if (this.provider === 'mongodb') {
|
|
312
|
+
await this.getMongoClient();
|
|
313
|
+
const db = this.getMongoDb();
|
|
314
|
+
await db.collection(this.historyTableName).deleteMany({ session_id: sessionId });
|
|
315
|
+
await db.collection(this.feedbackTableName).deleteMany({ session_id: sessionId });
|
|
316
|
+
} else {
|
|
317
|
+
const history = this.readLocalFile(this.historyFile);
|
|
318
|
+
const filteredHistory = history.filter((h) => h.session_id !== sessionId);
|
|
319
|
+
this.writeLocalFile(this.historyFile, filteredHistory);
|
|
320
|
+
|
|
321
|
+
const feedback = this.readLocalFile(this.feedbackFile);
|
|
322
|
+
const filteredFeedback = feedback.filter((f) => f.session_id !== sessionId);
|
|
323
|
+
this.writeLocalFile(this.feedbackFile, filteredFeedback);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async listSessions(): Promise<string[]> {
|
|
328
|
+
this.checkHistoryEnabled();
|
|
329
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
330
|
+
const pool = await this.getPGPool();
|
|
331
|
+
const res = await pool.query(`SELECT DISTINCT session_id FROM ${this.historyTableName}`);
|
|
332
|
+
return res.rows.map((r: any) => r.session_id);
|
|
333
|
+
} else if (this.provider === 'mongodb') {
|
|
334
|
+
await this.getMongoClient();
|
|
335
|
+
const db = this.getMongoDb();
|
|
336
|
+
return db.collection(this.historyTableName).distinct('session_id');
|
|
337
|
+
} else {
|
|
338
|
+
const history = this.readLocalFile(this.historyFile);
|
|
339
|
+
const sessions = new Set<string>(history.map((h) => h.session_id));
|
|
340
|
+
return Array.from(sessions);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ─── Feedback Operations ──────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
async saveFeedback(feedback: Omit<FeedbackItem, 'id' | 'createdAt'>): Promise<void> {
|
|
347
|
+
this.checkFeedbackEnabled();
|
|
348
|
+
const id = `fb_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
|
|
349
|
+
const createdAt = new Date().toISOString();
|
|
350
|
+
|
|
351
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
352
|
+
const pool = await this.getPGPool();
|
|
353
|
+
await pool.query(
|
|
354
|
+
`INSERT INTO ${this.feedbackTableName} (id, message_id, session_id, rating, comment, created_at)
|
|
355
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
356
|
+
ON CONFLICT (message_id) DO UPDATE
|
|
357
|
+
SET rating = EXCLUDED.rating, comment = EXCLUDED.comment`,
|
|
358
|
+
[id, feedback.messageId, feedback.sessionId, feedback.rating, feedback.comment || null, createdAt]
|
|
359
|
+
);
|
|
360
|
+
} else if (this.provider === 'mongodb') {
|
|
361
|
+
await this.getMongoClient();
|
|
362
|
+
const db = this.getMongoDb();
|
|
363
|
+
const col = db.collection(this.feedbackTableName);
|
|
364
|
+
await col.updateOne(
|
|
365
|
+
{ message_id: feedback.messageId },
|
|
366
|
+
{
|
|
367
|
+
$set: {
|
|
368
|
+
id,
|
|
369
|
+
message_id: feedback.messageId,
|
|
370
|
+
session_id: feedback.sessionId,
|
|
371
|
+
rating: feedback.rating,
|
|
372
|
+
comment: feedback.comment || null,
|
|
373
|
+
created_at: createdAt,
|
|
374
|
+
},
|
|
375
|
+
},
|
|
376
|
+
{ upsert: true }
|
|
377
|
+
);
|
|
378
|
+
} else {
|
|
379
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
380
|
+
const index = list.findIndex((f) => f.message_id === feedback.messageId);
|
|
381
|
+
const item = {
|
|
382
|
+
id,
|
|
383
|
+
message_id: feedback.messageId,
|
|
384
|
+
session_id: feedback.sessionId,
|
|
385
|
+
rating: feedback.rating,
|
|
386
|
+
comment: feedback.comment || null,
|
|
387
|
+
created_at: createdAt,
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
if (index !== -1) {
|
|
391
|
+
list[index] = item;
|
|
392
|
+
} else {
|
|
393
|
+
list.push(item);
|
|
394
|
+
}
|
|
395
|
+
this.writeLocalFile(this.feedbackFile, list);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async getFeedback(messageId: string): Promise<FeedbackItem | null> {
|
|
400
|
+
this.checkFeedbackEnabled();
|
|
401
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
402
|
+
const pool = await this.getPGPool();
|
|
403
|
+
const res = await pool.query(
|
|
404
|
+
`SELECT id, message_id as "messageId", session_id as "sessionId", rating, comment, created_at as "createdAt"
|
|
405
|
+
FROM ${this.feedbackTableName}
|
|
406
|
+
WHERE message_id = $1`,
|
|
407
|
+
[messageId]
|
|
408
|
+
);
|
|
409
|
+
return res.rows[0] || null;
|
|
410
|
+
} else if (this.provider === 'mongodb') {
|
|
411
|
+
await this.getMongoClient();
|
|
412
|
+
const db = this.getMongoDb();
|
|
413
|
+
const col = db.collection(this.feedbackTableName);
|
|
414
|
+
const item = await col.findOne({ message_id: messageId });
|
|
415
|
+
if (!item) return null;
|
|
416
|
+
return {
|
|
417
|
+
id: item.id,
|
|
418
|
+
messageId: item.message_id,
|
|
419
|
+
sessionId: item.session_id,
|
|
420
|
+
rating: item.rating,
|
|
421
|
+
comment: item.comment,
|
|
422
|
+
createdAt: item.created_at,
|
|
423
|
+
};
|
|
424
|
+
} else {
|
|
425
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
426
|
+
return list.find((f) => f.message_id === messageId) || null;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async listFeedback(): Promise<FeedbackItem[]> {
|
|
431
|
+
this.checkFeedbackEnabled();
|
|
432
|
+
if (this.provider === 'postgresql' || this.provider === 'pgvector') {
|
|
433
|
+
const pool = await this.getPGPool();
|
|
434
|
+
const res = await pool.query(
|
|
435
|
+
`SELECT id, message_id as "messageId", session_id as "sessionId", rating, comment, created_at as "createdAt"
|
|
436
|
+
FROM ${this.feedbackTableName}
|
|
437
|
+
ORDER BY created_at DESC`
|
|
438
|
+
);
|
|
439
|
+
return res.rows;
|
|
440
|
+
} else if (this.provider === 'mongodb') {
|
|
441
|
+
await this.getMongoClient();
|
|
442
|
+
const db = this.getMongoDb();
|
|
443
|
+
const col = db.collection(this.feedbackTableName);
|
|
444
|
+
const items = await col.find({}).sort({ created_at: -1 }).toArray();
|
|
445
|
+
return items.map((item: any) => ({
|
|
446
|
+
id: item.id,
|
|
447
|
+
messageId: item.message_id,
|
|
448
|
+
sessionId: item.session_id,
|
|
449
|
+
rating: item.rating,
|
|
450
|
+
comment: item.comment,
|
|
451
|
+
createdAt: item.created_at,
|
|
452
|
+
}));
|
|
453
|
+
} else {
|
|
454
|
+
const list = this.readLocalFile(this.feedbackFile);
|
|
455
|
+
return [...list].sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async disconnect() {
|
|
460
|
+
if (this.pgPool) {
|
|
461
|
+
await this.pgPool.end();
|
|
462
|
+
this.pgPool = null;
|
|
463
|
+
}
|
|
464
|
+
if (this.mongoClient) {
|
|
465
|
+
await this.mongoClient.close();
|
|
466
|
+
this.mongoClient = null;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|