@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.
Files changed (60) hide show
  1. package/README.md +136 -136
  2. package/dist/{ILLMProvider-Bhk6zJOK.d.mts → ILLMProvider-DMxLyTdq.d.mts} +59 -4
  3. package/dist/{ILLMProvider-Bhk6zJOK.d.ts → ILLMProvider-DMxLyTdq.d.ts} +59 -4
  4. package/dist/handlers/index.d.mts +2 -2
  5. package/dist/handlers/index.d.ts +2 -2
  6. package/dist/handlers/index.js +2327 -474
  7. package/dist/handlers/index.mjs +2329 -473
  8. package/dist/{index-B9J_XEh0.d.ts → index-CfkqZd2Y.d.ts} +12 -2
  9. package/dist/{index-C3SVtPYg.d.mts → index-DXd29KMq.d.mts} +27 -52
  10. package/dist/{index-Bu7T6xgr.d.ts → index-D_bOdJML.d.ts} +27 -52
  11. package/dist/{index-BJ4cd-t5.d.mts → index-xygonxpW.d.mts} +12 -2
  12. package/dist/index.css +783 -550
  13. package/dist/index.d.mts +35 -7
  14. package/dist/index.d.ts +35 -7
  15. package/dist/index.js +419 -282
  16. package/dist/index.mjs +426 -292
  17. package/dist/server.d.mts +65 -6
  18. package/dist/server.d.ts +65 -6
  19. package/dist/server.js +2185 -317
  20. package/dist/server.mjs +2185 -317
  21. package/package.json +13 -8
  22. package/src/app/constants.tsx +37 -7
  23. package/src/app/page.tsx +2 -0
  24. package/src/components/ChatWidget.tsx +2 -1
  25. package/src/components/ChatWindow.tsx +4 -1
  26. package/src/components/CodeViewer.tsx +19 -14
  27. package/src/components/MarkdownComponents.tsx +44 -1
  28. package/src/components/MessageBubble.tsx +162 -50
  29. package/src/components/constants.tsx +228 -0
  30. package/src/config/RagConfig.ts +48 -2
  31. package/src/config/constants.ts +4 -0
  32. package/src/config/serverConfig.ts +15 -0
  33. package/src/core/ConfigResolver.ts +38 -6
  34. package/src/core/DatabaseStorage.ts +469 -0
  35. package/src/core/LicenseVerifier.ts +260 -0
  36. package/src/core/MultiAgentCoordinator.ts +239 -0
  37. package/src/core/Pipeline.ts +151 -18
  38. package/src/core/Retrivora.ts +7 -0
  39. package/src/core/VectorPlugin.ts +12 -3
  40. package/src/core/mcp.ts +261 -0
  41. package/src/handlers/index.ts +449 -63
  42. package/src/hooks/useRagChat.ts +96 -42
  43. package/src/hooks/useStoredMessages.ts +15 -4
  44. package/src/index.ts +3 -0
  45. package/src/llm/LLMFactory.ts +9 -1
  46. package/src/llm/providers/GroqProvider.ts +176 -0
  47. package/src/llm/providers/QwenProvider.ts +191 -0
  48. package/src/server.ts +7 -0
  49. package/src/types/chat.ts +14 -0
  50. package/src/types/props.ts +12 -0
  51. package/.env.example +0 -80
  52. package/LICENSE.txt +0 -21
  53. package/src/components/AmbientBackground.tsx +0 -29
  54. package/src/components/ArchitectureCard.tsx +0 -17
  55. package/src/components/ArchitectureCardsSection.tsx +0 -15
  56. package/src/components/DocViewer.tsx +0 -103
  57. package/src/components/Documentation.tsx +0 -121
  58. package/src/components/Hero.tsx +0 -59
  59. package/src/components/Lifecycle.tsx +0 -37
  60. package/src/components/Navbar.tsx +0 -55
@@ -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
  /**
@@ -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 = 'agentic';
115
- if (retrieval.strategy === 'contextual-compression') normalized.useReranking = true;
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 === 'agentic' || type === 'agentic-rag') normalized.architecture = 'agentic';
121
- else if (type === 'hybrid-rag') normalized.architecture = 'hybrid';
122
- else if (type === 'graph-rag') {
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 ?? 'simple';
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
+ }