@yesvara/svara 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @module cli/commands/db
3
+ * SvaraJS — Database inspection commands
4
+ *
5
+ * Usage:
6
+ * svara db:list-chunks [--agent <name>]
7
+ * svara db:search <query> [--agent <name>] [--limit 5]
8
+ * svara db:stats [--agent <name>]
9
+ * svara db:users
10
+ * svara db:sessions [--user <email>]
11
+ */
12
+
13
+ import { SvaraDB } from '../../database/sqlite.js';
14
+ import type { DocumentChunk } from '../../core/types.js';
15
+
16
+ interface DBCommandOptions {
17
+ agent?: string;
18
+ limit?: number;
19
+ user?: string;
20
+ }
21
+
22
+ const db = new SvaraDB('./data/svara.db');
23
+
24
+ // ── List Chunks ───────────────────────────────────────────────────────────────
25
+
26
+ export async function listChunks(options: DBCommandOptions): Promise<void> {
27
+ try {
28
+ let query = 'SELECT id, agent_name, document_id, content, source FROM svara_chunks';
29
+ const params: (string | number)[] = [];
30
+
31
+ if (options.agent) {
32
+ query += ' WHERE agent_name = ?';
33
+ params.push(options.agent);
34
+ }
35
+
36
+ query += ' LIMIT 20';
37
+
38
+ const chunks = db.query(query, params) as Array<{
39
+ id: string;
40
+ agent_name: string;
41
+ document_id: string;
42
+ content: string;
43
+ source: string;
44
+ }>;
45
+
46
+ if (!chunks.length) {
47
+ console.log('ℹ️ No chunks found.');
48
+ return;
49
+ }
50
+
51
+ console.log(`\n📚 Chunks (showing ${chunks.length}):\n`);
52
+ chunks.forEach((chunk, i) => {
53
+ const preview = chunk.content.substring(0, 80).replace(/\n/g, ' ');
54
+ console.log(`${i + 1}. [${chunk.agent_name}] ${chunk.source}`);
55
+ console.log(` ID: ${chunk.id}`);
56
+ console.log(` Preview: ${preview}...`);
57
+ console.log('');
58
+ });
59
+ } catch (error) {
60
+ console.error('❌ Error listing chunks:', error instanceof Error ? error.message : error);
61
+ process.exit(1);
62
+ }
63
+ }
64
+
65
+ // ── Search Chunks ─────────────────────────────────────────────────────────────
66
+
67
+ export async function searchChunks(query: string, options: DBCommandOptions): Promise<void> {
68
+ try {
69
+ const limit = options.limit || 5;
70
+ let sql = `SELECT id, agent_name, source, content FROM svara_chunks
71
+ WHERE content LIKE ?`;
72
+ const params: (string | number)[] = [`%${query}%`];
73
+
74
+ if (options.agent) {
75
+ sql += ' AND agent_name = ?';
76
+ params.push(options.agent);
77
+ }
78
+
79
+ sql += ` LIMIT ${limit}`;
80
+
81
+ const results = db.query(sql, params) as Array<{
82
+ id: string;
83
+ agent_name: string;
84
+ source: string;
85
+ content: string;
86
+ }>;
87
+
88
+ if (!results.length) {
89
+ console.log(`ℹ️ No chunks found matching "${query}".`);
90
+ return;
91
+ }
92
+
93
+ console.log(`\n🔍 Search Results for "${query}" (${results.length} found):\n`);
94
+ results.forEach((result, i) => {
95
+ const preview = result.content.substring(0, 100).replace(/\n/g, ' ');
96
+ console.log(`${i + 1}. [${result.agent_name}] ${result.source}`);
97
+ console.log(` ${preview}...`);
98
+ console.log('');
99
+ });
100
+ } catch (error) {
101
+ console.error('❌ Error searching chunks:', error instanceof Error ? error.message : error);
102
+ process.exit(1);
103
+ }
104
+ }
105
+
106
+ // ── Database Stats ────────────────────────────────────────────────────────────
107
+
108
+ export async function dbStats(options: DBCommandOptions): Promise<void> {
109
+ try {
110
+ const totalChunks = db.query('SELECT COUNT(*) as count FROM svara_chunks') as Array<{
111
+ count: number;
112
+ }>;
113
+ const totalUsers = db.query('SELECT COUNT(*) as count FROM svara_users') as Array<{
114
+ count: number;
115
+ }>;
116
+ const totalSessions = db.query('SELECT COUNT(*) as count FROM svara_sessions') as Array<{
117
+ count: number;
118
+ }>;
119
+ const totalMessages = db.query('SELECT COUNT(*) as count FROM svara_messages') as Array<{
120
+ count: number;
121
+ }>;
122
+
123
+ const agents = db.query(
124
+ 'SELECT DISTINCT agent_name, COUNT(*) as chunk_count FROM svara_chunks GROUP BY agent_name'
125
+ ) as Array<{
126
+ agent_name: string;
127
+ chunk_count: number;
128
+ }>;
129
+
130
+ console.log('\n📊 Database Statistics:\n');
131
+ console.log(` Total Chunks: ${totalChunks[0]?.count ?? 0}`);
132
+ console.log(` Total Users: ${totalUsers[0]?.count ?? 0}`);
133
+ console.log(` Total Sessions: ${totalSessions[0]?.count ?? 0}`);
134
+ console.log(` Total Messages: ${totalMessages[0]?.count ?? 0}`);
135
+ console.log('\n Chunks by Agent:');
136
+
137
+ if (agents.length === 0) {
138
+ console.log(' (none)');
139
+ } else {
140
+ agents.forEach((agent) => {
141
+ console.log(` - ${agent.agent_name}: ${agent.chunk_count} chunks`);
142
+ });
143
+ }
144
+
145
+ console.log('');
146
+ } catch (error) {
147
+ console.error('❌ Error getting stats:', error instanceof Error ? error.message : error);
148
+ process.exit(1);
149
+ }
150
+ }
151
+
152
+ // ── List Users ────────────────────────────────────────────────────────────────
153
+
154
+ export async function listUsers(): Promise<void> {
155
+ try {
156
+ const users = db.query(
157
+ 'SELECT id, email, display_name, first_seen, last_seen FROM svara_users LIMIT 50'
158
+ ) as Array<{
159
+ id: string;
160
+ email: string;
161
+ display_name: string;
162
+ first_seen: string;
163
+ last_seen: string;
164
+ }>;
165
+
166
+ if (!users.length) {
167
+ console.log('ℹ️ No users found.');
168
+ return;
169
+ }
170
+
171
+ console.log(`\n👥 Users (${users.length}):\n`);
172
+ users.forEach((user) => {
173
+ console.log(` ${user.email}`);
174
+ console.log(` Name: ${user.display_name || '(none)'}`);
175
+ console.log(` First seen: ${user.first_seen}`);
176
+ console.log(` Last seen: ${user.last_seen}`);
177
+ console.log('');
178
+ });
179
+ } catch (error) {
180
+ console.error('❌ Error listing users:', error instanceof Error ? error.message : error);
181
+ process.exit(1);
182
+ }
183
+ }
184
+
185
+ // ── List Sessions ─────────────────────────────────────────────────────────────
186
+
187
+ export async function listSessions(options: DBCommandOptions): Promise<void> {
188
+ try {
189
+ let query = `SELECT s.id, s.user_id, s.agent_name, s.created_at, COUNT(m.id) as message_count
190
+ FROM svara_sessions s
191
+ LEFT JOIN svara_messages m ON s.id = m.session_id`;
192
+ const params: string[] = [];
193
+
194
+ if (options.user) {
195
+ query += ` WHERE s.user_id = (SELECT id FROM svara_users WHERE email = ?)`;
196
+ params.push(options.user);
197
+ }
198
+
199
+ query += ' GROUP BY s.id LIMIT 50';
200
+
201
+ const sessions = db.query(query, params) as Array<{
202
+ id: string;
203
+ user_id: string;
204
+ agent_name: string;
205
+ created_at: string;
206
+ message_count: number;
207
+ }>;
208
+
209
+ if (!sessions.length) {
210
+ console.log('ℹ️ No sessions found.');
211
+ return;
212
+ }
213
+
214
+ console.log(`\n💬 Sessions (${sessions.length}):\n`);
215
+ sessions.forEach((session) => {
216
+ console.log(` ID: ${session.id}`);
217
+ console.log(` Agent: ${session.agent_name}`);
218
+ console.log(` Messages: ${session.message_count}`);
219
+ console.log(` Created: ${session.created_at}`);
220
+ console.log('');
221
+ });
222
+ } catch (error) {
223
+ console.error('❌ Error listing sessions:', error instanceof Error ? error.message : error);
224
+ process.exit(1);
225
+ }
226
+ }
227
+
228
+ // ── Clear Chunks ──────────────────────────────────────────────────────────────
229
+
230
+ export async function clearChunks(agentName: string): Promise<void> {
231
+ try {
232
+ if (!agentName) {
233
+ console.error('❌ Agent name required. Usage: svara db:clear-chunks <agent-name>');
234
+ process.exit(1);
235
+ }
236
+
237
+ const result = db.query('SELECT COUNT(*) as count FROM svara_chunks WHERE agent_name = ?', [
238
+ agentName,
239
+ ]) as Array<{
240
+ count: number;
241
+ }>;
242
+ const count = result[0]?.count ?? 0;
243
+
244
+ if (count === 0) {
245
+ console.log(`ℹ️ No chunks found for agent "${agentName}".`);
246
+ return;
247
+ }
248
+
249
+ console.log(`\n⚠️ About to delete ${count} chunks for agent "${agentName}".`);
250
+ console.log('Use --yes flag to confirm: svara db:clear-chunks <agent> --yes\n');
251
+
252
+ process.exit(0);
253
+ } catch (error) {
254
+ console.error('❌ Error clearing chunks:', error instanceof Error ? error.message : error);
255
+ process.exit(1);
256
+ }
257
+ }
258
+
259
+ export async function clearChunksConfirmed(agentName: string): Promise<void> {
260
+ try {
261
+ db.run('DELETE FROM svara_chunks WHERE agent_name = ?', [agentName]);
262
+ console.log(`✅ Deleted all chunks for agent "${agentName}".`);
263
+ } catch (error) {
264
+ console.error('❌ Error deleting chunks:', error instanceof Error ? error.message : error);
265
+ process.exit(1);
266
+ }
267
+ }
package/src/cli/index.ts CHANGED
@@ -11,11 +11,12 @@
11
11
  */
12
12
 
13
13
  import { Command } from 'commander';
14
- import { createRequire } from 'module';
14
+ import path from 'path';
15
+ import fs from 'fs';
15
16
 
16
- const require = createRequire(import.meta.url);
17
- // eslint-disable-next-line @typescript-eslint/no-require-imports
18
- const pkg = require('../../package.json') as { version: string; description: string };
17
+ // Load package.json (works in both CommonJS and ESM)
18
+ const pkgPath = path.resolve(path.dirname(__filename), '../../package.json');
19
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { version: string; description: string };
19
20
 
20
21
  const program = new Command();
21
22
 
@@ -70,6 +71,75 @@ program
70
71
  }
71
72
  });
72
73
 
74
+ // ── svara db:* commands ────────────────────────────────────────────────────────
75
+
76
+ // svara db:list-chunks
77
+ program
78
+ .command('db:list-chunks')
79
+ .description('List all chunks in vector store')
80
+ .option('--agent <name>', 'Filter by agent name')
81
+ .action(async (opts: { agent?: string }) => {
82
+ const { listChunks } = await import('./commands/db.js');
83
+ await listChunks(opts);
84
+ });
85
+
86
+ // svara db:search
87
+ program
88
+ .command('db:search <query>')
89
+ .description('Search chunks by content')
90
+ .option('--agent <name>', 'Filter by agent name')
91
+ .option('--limit <num>', 'Number of results', '5')
92
+ .action(async (query: string, opts: { agent?: string; limit: string }) => {
93
+ const { searchChunks } = await import('./commands/db.js');
94
+ await searchChunks(query, {
95
+ agent: opts.agent,
96
+ limit: parseInt(opts.limit, 10),
97
+ });
98
+ });
99
+
100
+ // svara db:stats
101
+ program
102
+ .command('db:stats')
103
+ .description('Show database statistics')
104
+ .option('--agent <name>', 'Filter by agent name')
105
+ .action(async (opts: { agent?: string }) => {
106
+ const { dbStats } = await import('./commands/db.js');
107
+ await dbStats(opts);
108
+ });
109
+
110
+ // svara db:users
111
+ program
112
+ .command('db:users')
113
+ .description('List all users in the database')
114
+ .action(async () => {
115
+ const { listUsers } = await import('./commands/db.js');
116
+ await listUsers();
117
+ });
118
+
119
+ // svara db:sessions
120
+ program
121
+ .command('db:sessions')
122
+ .description('List all sessions in the database')
123
+ .option('--user <email>', 'Filter by user email')
124
+ .action(async (opts: { user?: string }) => {
125
+ const { listSessions } = await import('./commands/db.js');
126
+ await listSessions(opts);
127
+ });
128
+
129
+ // svara db:clear-chunks
130
+ program
131
+ .command('db:clear-chunks <agent>')
132
+ .description('Delete all chunks for an agent (requires --yes confirmation)')
133
+ .option('--yes', 'Confirm deletion')
134
+ .action(async (agent: string, opts: { yes?: boolean }) => {
135
+ const { clearChunksConfirmed, clearChunks } = await import('./commands/db.js');
136
+ if (opts.yes) {
137
+ await clearChunksConfirmed(agent);
138
+ } else {
139
+ await clearChunks(agent);
140
+ }
141
+ });
142
+
73
143
  program.parse(process.argv);
74
144
 
75
145
  // Show help if no command given
package/src/core/agent.ts CHANGED
@@ -53,6 +53,7 @@ import type {
53
53
  import { ConversationMemory } from '../memory/conversation.js';
54
54
  import { ContextBuilder } from '../memory/context.js';
55
55
  import { ToolRegistry } from '../tools/registry.js';
56
+ import { SvaraDB } from '../database/sqlite.js';
56
57
  import { ToolExecutor } from '../tools/executor.js';
57
58
  import type { Tool } from '../types.js';
58
59
 
@@ -169,8 +170,10 @@ export class SvaraAgent extends EventEmitter {
169
170
 
170
171
  private channels: Map<ChannelName, SvaraChannel> = new Map();
171
172
  private knowledgeBase: KnowledgeBase | null = null;
173
+ private retriever: any = null; // Store VectorRetriever for retrieveChunks access
172
174
  private knowledgePaths: string[] = [];
173
175
  private isStarted = false;
176
+ private db: SvaraDB;
174
177
 
175
178
  constructor(config: AgentConfig) {
176
179
  super();
@@ -178,6 +181,7 @@ export class SvaraAgent extends EventEmitter {
178
181
  this.name = config.name;
179
182
  this.maxIterations = config.maxIterations ?? 10;
180
183
  this.verbose = config.verbose ?? false;
184
+ this.db = new SvaraDB('./data/svara.db');
181
185
 
182
186
  this.systemPrompt = config.systemPrompt
183
187
  ?? `You are ${config.name}, a helpful and friendly AI assistant. Be concise and accurate.`;
@@ -324,6 +328,7 @@ export class SvaraAgent extends EventEmitter {
324
328
  sessionId: result.sessionId,
325
329
  usage: result.usage,
326
330
  toolsUsed: result.toolsUsed,
331
+ retrievedDocuments: result.retrievedDocuments || [],
327
332
  });
328
333
  } catch (err) {
329
334
  const error = err as Error;
@@ -405,6 +410,58 @@ export class SvaraAgent extends EventEmitter {
405
410
  }
406
411
  }
407
412
 
413
+ // ─── Internal: User & Session Tracking ───────────────────────────────────────
414
+
415
+ private async trackUserAndSession(userId: string, sessionId: string, channel = 'api'): Promise<void> {
416
+ try {
417
+ // Track user
418
+ const existingUser = this.db.query(
419
+ 'SELECT id FROM svara_users WHERE id = ?',
420
+ [userId]
421
+ ) as Array<{ id: string }>;
422
+
423
+ if (existingUser.length === 0) {
424
+ // New user
425
+ this.db.run(
426
+ `INSERT INTO svara_users (id, display_name, first_seen, last_seen)
427
+ VALUES (?, ?, unixepoch(), unixepoch())`,
428
+ [userId, userId]
429
+ );
430
+ } else {
431
+ // Update last_seen
432
+ this.db.run(
433
+ 'UPDATE svara_users SET last_seen = unixepoch() WHERE id = ?',
434
+ [userId]
435
+ );
436
+ }
437
+
438
+ // Track session
439
+ const existingSession = this.db.query(
440
+ 'SELECT id FROM svara_sessions WHERE id = ?',
441
+ [sessionId]
442
+ ) as Array<{ id: string }>;
443
+
444
+ if (existingSession.length === 0) {
445
+ // New session
446
+ this.db.run(
447
+ `INSERT INTO svara_sessions (id, user_id, channel, created_at, updated_at)
448
+ VALUES (?, ?, ?, unixepoch(), unixepoch())`,
449
+ [sessionId, userId, channel]
450
+ );
451
+ } else {
452
+ // Update updated_at
453
+ this.db.run(
454
+ 'UPDATE svara_sessions SET updated_at = unixepoch() WHERE id = ?',
455
+ [sessionId]
456
+ );
457
+ }
458
+
459
+ this.log('debug', `Tracked user ${userId} with session ${sessionId}`);
460
+ } catch (error) {
461
+ this.log('error', `Failed to track user: ${(error as Error).message}`);
462
+ }
463
+ }
464
+
408
465
  // ─── Internal: Agentic Loop ───────────────────────────────────────────────
409
466
 
410
467
  /**
@@ -419,18 +476,40 @@ export class SvaraAgent extends EventEmitter {
419
476
  }
420
477
 
421
478
  private async run(message: string, options: AgentRunOptions): Promise<AgentRunResult> {
479
+ console.log(`\n[RUN START] kb=${!!this.knowledgeBase} ret=${!!this.retriever}`);
422
480
  const startTime = Date.now();
423
481
  const sessionId = options.sessionId ?? crypto.randomUUID();
482
+ const userId = options.userId ?? 'unknown';
483
+
484
+ // Track user and session
485
+ await this.trackUserAndSession(userId, sessionId);
424
486
 
425
- this.emit('message:received', { message, sessionId, userId: options.userId });
487
+ this.emit('message:received', { message, sessionId, userId });
426
488
 
427
489
  // Build LLM message history
428
490
  const history = await this.memory.getHistory(sessionId);
429
491
 
430
492
  // RAG retrieval
431
493
  let ragContext = '';
432
- if (this.knowledgeBase) {
494
+ let retrievedDocuments: Array<{ source: string; score: number; excerpt: string }> = [];
495
+ if (this.knowledgeBase && this.retriever) {
433
496
  ragContext = await this.knowledgeBase.retrieve(message);
497
+ // Also retrieve chunks to get document metadata and scores
498
+ try {
499
+ console.log(`[DEBUG] Calling retrieveChunks for query: "${message}"`);
500
+ const context = await this.retriever.retrieveChunks(message, 3);
501
+ console.log(`[DEBUG] Retrieved ${context.chunks.length} chunks`);
502
+ retrievedDocuments = context.chunks.map((item: any) => ({
503
+ source: item.chunk?.source || 'unknown',
504
+ score: Math.round(item.score * 100) / 100,
505
+ excerpt: item.chunk?.content?.substring(0, 150) || '',
506
+ }));
507
+ console.log(`[DEBUG] Mapped ${retrievedDocuments.length} documents`);
508
+ } catch (e) {
509
+ console.error(`[ERROR] RAG retrieval failed:`, e);
510
+ }
511
+ } else {
512
+ console.log(`[DEBUG] No knowledgeBase (${!!this.knowledgeBase}) or retriever (${!!this.retriever})`);
434
513
  }
435
514
 
436
515
  const messages = this.context.buildMessages(
@@ -442,7 +521,7 @@ export class SvaraAgent extends EventEmitter {
442
521
 
443
522
  const internalCtx: InternalAgentContext = {
444
523
  sessionId,
445
- userId: options.userId ?? 'unknown',
524
+ userId,
446
525
  agentName: this.name,
447
526
  history,
448
527
  metadata: options.metadata ?? {},
@@ -521,6 +600,7 @@ export class SvaraAgent extends EventEmitter {
521
600
  iterations,
522
601
  usage: totalUsage,
523
602
  duration: Date.now() - startTime,
603
+ retrievedDocuments: retrievedDocuments.length > 0 ? retrievedDocuments : undefined,
524
604
  };
525
605
 
526
606
  this.emit('message:sent', { response: finalResponse, sessionId });
@@ -534,8 +614,9 @@ export class SvaraAgent extends EventEmitter {
534
614
  const { glob } = await import('glob');
535
615
  const { VectorRetriever } = await import('../rag/retriever.js');
536
616
 
537
- const retriever = new VectorRetriever();
538
- await retriever.init({ embeddings: { provider: 'openai' } });
617
+ // Create retriever with agent name for isolated RAG per agent
618
+ this.retriever = new VectorRetriever(this.name, this.db);
619
+ await this.retriever.init({ embeddings: { provider: 'openai' } });
539
620
 
540
621
  const files: string[] = [];
541
622
  for (const pattern of paths) {
@@ -548,16 +629,16 @@ export class SvaraAgent extends EventEmitter {
548
629
  return;
549
630
  }
550
631
 
551
- await retriever.addDocuments(files);
632
+ await this.retriever.addDocuments(files);
552
633
  this.knowledgeBase = {
553
634
  load: async (p) => {
554
635
  const newFiles: string[] = [];
555
636
  for (const pattern of (Array.isArray(p) ? p : [p])) {
556
637
  newFiles.push(...await glob(pattern));
557
638
  }
558
- await retriever.addDocuments(newFiles);
639
+ await this.retriever.addDocuments(newFiles);
559
640
  },
560
- retrieve: (query, topK) => retriever.retrieve(query, topK),
641
+ retrieve: (query, topK) => this.retriever.retrieve(query, topK),
561
642
  };
562
643
 
563
644
  this.log('info', `Knowledge base loaded: ${files.length} file(s).`);
package/src/core/types.ts CHANGED
@@ -89,6 +89,12 @@ export interface AgentRunOptions {
89
89
  metadata?: Record<string, unknown>;
90
90
  }
91
91
 
92
+ export interface RetrievedDocument {
93
+ source: string;
94
+ score: number;
95
+ excerpt: string;
96
+ }
97
+
92
98
  export interface AgentRunResult {
93
99
  response: string;
94
100
  sessionId: string;
@@ -96,6 +102,7 @@ export interface AgentRunResult {
96
102
  iterations: number;
97
103
  usage: TokenUsage;
98
104
  duration: number;
105
+ retrievedDocuments?: RetrievedDocument[];
99
106
  }
100
107
 
101
108
  // ─── Memory Internals ────────────────────────────────────────────────────────
@@ -127,6 +134,7 @@ export interface DocumentChunk {
127
134
  id: string;
128
135
  documentId: string;
129
136
  content: string;
137
+ source: string;
130
138
  index: number;
131
139
  metadata: {
132
140
  filename: string;
@@ -155,8 +163,13 @@ export interface RAGConfig {
155
163
  };
156
164
  }
157
165
 
166
+ export interface ChunkWithScore {
167
+ chunk: DocumentChunk;
168
+ score: number;
169
+ }
170
+
158
171
  export interface RetrievedContext {
159
- chunks: DocumentChunk[];
172
+ chunks: ChunkWithScore[];
160
173
  query: string;
161
174
  totalFound: number;
162
175
  }
@@ -28,30 +28,55 @@ CREATE TABLE IF NOT EXISTS svara_messages (
28
28
  CREATE INDEX IF NOT EXISTS idx_messages_session
29
29
  ON svara_messages (session_id, created_at);
30
30
 
31
+ -- User registry
32
+ CREATE TABLE IF NOT EXISTS svara_users (
33
+ id TEXT PRIMARY KEY,
34
+ email TEXT,
35
+ display_name TEXT,
36
+ first_seen INTEGER NOT NULL DEFAULT (unixepoch()),
37
+ last_seen INTEGER NOT NULL DEFAULT (unixepoch()),
38
+ metadata TEXT DEFAULT '{}'
39
+ );
40
+
41
+ CREATE INDEX IF NOT EXISTS idx_users_email
42
+ ON svara_users (email);
43
+
31
44
  -- Session metadata
32
45
  CREATE TABLE IF NOT EXISTS svara_sessions (
33
46
  id TEXT PRIMARY KEY,
34
- user_id TEXT,
47
+ user_id TEXT NOT NULL,
35
48
  channel TEXT NOT NULL,
36
49
  created_at INTEGER NOT NULL DEFAULT (unixepoch()),
37
50
  updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
38
- metadata TEXT DEFAULT '{}'
51
+ metadata TEXT DEFAULT '{}',
52
+ FOREIGN KEY (user_id) REFERENCES svara_users(id)
39
53
  );
40
54
 
41
- -- Vector store chunks for RAG
55
+ CREATE INDEX IF NOT EXISTS idx_sessions_user
56
+ ON svara_sessions (user_id);
57
+
58
+ -- Vector store chunks for RAG (per agent)
42
59
  CREATE TABLE IF NOT EXISTS svara_chunks (
43
60
  id TEXT PRIMARY KEY,
61
+ agent_name TEXT NOT NULL, -- Separate RAG per agent
44
62
  document_id TEXT NOT NULL,
45
63
  content TEXT NOT NULL,
64
+ content_hash TEXT NOT NULL, -- MD5 hash of content for deduplication
46
65
  chunk_index INTEGER NOT NULL,
47
- embedding BLOB, -- stored as binary float32 array
66
+ embedding TEXT, -- stored as JSON string of float array
48
67
  source TEXT NOT NULL,
49
68
  metadata TEXT DEFAULT '{}',
50
69
  created_at INTEGER NOT NULL DEFAULT (unixepoch())
51
70
  );
52
71
 
53
- CREATE INDEX IF NOT EXISTS idx_chunks_document
54
- ON svara_chunks (document_id);
72
+ CREATE INDEX IF NOT EXISTS idx_chunks_agent
73
+ ON svara_chunks (agent_name);
74
+
75
+ CREATE INDEX IF NOT EXISTS idx_chunks_agent_document
76
+ ON svara_chunks (agent_name, document_id);
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_chunks_content_hash
79
+ ON svara_chunks (content_hash);
55
80
 
56
81
  -- Document registry
57
82
  CREATE TABLE IF NOT EXISTS svara_documents (
@@ -61,6 +61,7 @@ export class Chunker {
61
61
  id: this.chunkId(document.id, index),
62
62
  documentId: document.id,
63
63
  content: content.trim(),
64
+ source: document.source,
64
65
  index,
65
66
  metadata: {
66
67
  ...document.metadata,