@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.
- package/CONTRIBUTING.md +233 -0
- package/README.md +211 -23
- package/SvaraJS.png +0 -0
- package/dist/{chunk-FEA5KIJN.mjs → chunk-CCNWHBEI.mjs} +108 -27
- package/dist/chunk-GA7LHPOF.mjs +257 -0
- package/dist/cli/index.js +490 -4
- package/dist/cli/index.mjs +35 -3
- package/dist/db-PEMUBXAR.mjs +190 -0
- package/dist/index.d.mts +18 -2
- package/dist/index.d.ts +18 -2
- package/dist/index.js +454 -269
- package/dist/index.mjs +83 -239
- package/dist/{retriever-4QY667XF.mjs → retriever-JYOGHA4F.mjs} +2 -1
- package/package.json +27 -15
- package/src/cli/commands/db.ts +267 -0
- package/src/cli/index.ts +74 -4
- package/src/core/agent.ts +89 -8
- package/src/core/types.ts +14 -1
- package/src/database/schema.ts +31 -6
- package/src/rag/chunker.ts +1 -0
- package/src/rag/retriever.ts +146 -33
- package/svara@1.0.0 +0 -0
- package/test-rag.ts +20 -0
- package/tsx +0 -0
|
@@ -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
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import fs from 'fs';
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const pkg =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
538
|
-
|
|
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:
|
|
172
|
+
chunks: ChunkWithScore[];
|
|
160
173
|
query: string;
|
|
161
174
|
totalFound: number;
|
|
162
175
|
}
|
package/src/database/schema.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
54
|
-
ON svara_chunks (
|
|
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 (
|