@yesvara/svara 0.1.0 → 0.1.1

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/dist/index.js CHANGED
@@ -30,13 +30,274 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
32
 
33
+ // src/database/schema.ts
34
+ var SCHEMA_VERSION, CREATE_TABLES_SQL, INSERT_META_SQL;
35
+ var init_schema = __esm({
36
+ "src/database/schema.ts"() {
37
+ "use strict";
38
+ SCHEMA_VERSION = 1;
39
+ CREATE_TABLES_SQL = `
40
+ -- Schema version tracking
41
+ CREATE TABLE IF NOT EXISTS svara_meta (
42
+ key TEXT PRIMARY KEY,
43
+ value TEXT NOT NULL
44
+ );
45
+
46
+ -- Conversation history persistence
47
+ CREATE TABLE IF NOT EXISTS svara_messages (
48
+ id TEXT PRIMARY KEY,
49
+ session_id TEXT NOT NULL,
50
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
51
+ content TEXT NOT NULL,
52
+ tool_call_id TEXT,
53
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_messages_session
57
+ ON svara_messages (session_id, created_at);
58
+
59
+ -- User registry
60
+ CREATE TABLE IF NOT EXISTS svara_users (
61
+ id TEXT PRIMARY KEY,
62
+ email TEXT,
63
+ display_name TEXT,
64
+ first_seen INTEGER NOT NULL DEFAULT (unixepoch()),
65
+ last_seen INTEGER NOT NULL DEFAULT (unixepoch()),
66
+ metadata TEXT DEFAULT '{}'
67
+ );
68
+
69
+ CREATE INDEX IF NOT EXISTS idx_users_email
70
+ ON svara_users (email);
71
+
72
+ -- Session metadata
73
+ CREATE TABLE IF NOT EXISTS svara_sessions (
74
+ id TEXT PRIMARY KEY,
75
+ user_id TEXT NOT NULL,
76
+ channel TEXT NOT NULL,
77
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
78
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
79
+ metadata TEXT DEFAULT '{}',
80
+ FOREIGN KEY (user_id) REFERENCES svara_users(id)
81
+ );
82
+
83
+ CREATE INDEX IF NOT EXISTS idx_sessions_user
84
+ ON svara_sessions (user_id);
85
+
86
+ -- Vector store chunks for RAG (per agent)
87
+ CREATE TABLE IF NOT EXISTS svara_chunks (
88
+ id TEXT PRIMARY KEY,
89
+ agent_name TEXT NOT NULL, -- Separate RAG per agent
90
+ document_id TEXT NOT NULL,
91
+ content TEXT NOT NULL,
92
+ content_hash TEXT NOT NULL, -- MD5 hash of content for deduplication
93
+ chunk_index INTEGER NOT NULL,
94
+ embedding TEXT, -- stored as JSON string of float array
95
+ source TEXT NOT NULL,
96
+ metadata TEXT DEFAULT '{}',
97
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
98
+ );
99
+
100
+ CREATE INDEX IF NOT EXISTS idx_chunks_agent
101
+ ON svara_chunks (agent_name);
102
+
103
+ CREATE INDEX IF NOT EXISTS idx_chunks_agent_document
104
+ ON svara_chunks (agent_name, document_id);
105
+
106
+ CREATE INDEX IF NOT EXISTS idx_chunks_content_hash
107
+ ON svara_chunks (content_hash);
108
+
109
+ -- Document registry
110
+ CREATE TABLE IF NOT EXISTS svara_documents (
111
+ id TEXT PRIMARY KEY,
112
+ source TEXT NOT NULL UNIQUE,
113
+ type TEXT NOT NULL,
114
+ size INTEGER,
115
+ hash TEXT,
116
+ indexed_at INTEGER NOT NULL DEFAULT (unixepoch()),
117
+ metadata TEXT DEFAULT '{}'
118
+ );
119
+
120
+ -- Key-value store for arbitrary agent state
121
+ CREATE TABLE IF NOT EXISTS svara_kv (
122
+ key TEXT PRIMARY KEY,
123
+ value TEXT NOT NULL,
124
+ expires_at INTEGER, -- unix timestamp, NULL = no expiry
125
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
126
+ );
127
+ `;
128
+ INSERT_META_SQL = `
129
+ INSERT OR REPLACE INTO svara_meta (key, value)
130
+ VALUES ('schema_version', ?), ('created_at', ?);
131
+ `;
132
+ }
133
+ });
134
+
135
+ // src/database/sqlite.ts
136
+ var import_path, import_fs, KVStore, SvaraDB;
137
+ var init_sqlite = __esm({
138
+ "src/database/sqlite.ts"() {
139
+ "use strict";
140
+ import_path = __toESM(require("path"));
141
+ import_fs = __toESM(require("fs"));
142
+ init_schema();
143
+ KVStore = class {
144
+ constructor(db) {
145
+ this.db = db;
146
+ }
147
+ db;
148
+ /** Set a key-value pair, with optional TTL in seconds. */
149
+ set(key, value, ttlSeconds) {
150
+ const expiresAt = ttlSeconds ? Math.floor(Date.now() / 1e3) + ttlSeconds : null;
151
+ this.db.prepare(`
152
+ INSERT OR REPLACE INTO svara_kv (key, value, expires_at, updated_at)
153
+ VALUES (?, ?, ?, unixepoch())
154
+ `).run(key, JSON.stringify(value), expiresAt);
155
+ }
156
+ /** Get a value by key. Returns undefined if not found or expired. */
157
+ get(key) {
158
+ const row = this.db.prepare(`
159
+ SELECT value, expires_at FROM svara_kv
160
+ WHERE key = ? AND (expires_at IS NULL OR expires_at > unixepoch())
161
+ `).get(key);
162
+ if (!row) return void 0;
163
+ return JSON.parse(row.value);
164
+ }
165
+ /** Delete a key. */
166
+ delete(key) {
167
+ this.db.prepare("DELETE FROM svara_kv WHERE key = ?").run(key);
168
+ }
169
+ /** Check if a key exists and is not expired. */
170
+ has(key) {
171
+ return this.get(key) !== void 0;
172
+ }
173
+ /** Get all keys matching a prefix. */
174
+ keys(prefix = "") {
175
+ const rows = this.db.prepare(`
176
+ SELECT key FROM svara_kv
177
+ WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > unixepoch())
178
+ `).all(`${prefix}%`);
179
+ return rows.map((r) => r.key);
180
+ }
181
+ };
182
+ SvaraDB = class {
183
+ db;
184
+ kv;
185
+ constructor(dbPath = ":memory:") {
186
+ if (dbPath !== ":memory:") {
187
+ import_fs.default.mkdirSync(import_path.default.dirname(import_path.default.resolve(dbPath)), { recursive: true });
188
+ }
189
+ this.db = this.openDatabase(dbPath);
190
+ this.configure();
191
+ this.migrate();
192
+ this.kv = new KVStore(this.db);
193
+ }
194
+ // ─── Query Helpers ────────────────────────────────────────────────────────
195
+ /**
196
+ * Run a SELECT and return all matching rows.
197
+ */
198
+ query(sql, params = []) {
199
+ return this.db.prepare(sql).all(...params);
200
+ }
201
+ /**
202
+ * Run a SELECT and return the first matching row.
203
+ */
204
+ queryOne(sql, params = []) {
205
+ return this.db.prepare(sql).get(...params);
206
+ }
207
+ /**
208
+ * Run an INSERT/UPDATE/DELETE. Returns affected row count.
209
+ */
210
+ run(sql, params = []) {
211
+ return this.db.prepare(sql).run(...params).changes;
212
+ }
213
+ /**
214
+ * Execute raw SQL (for DDL, migrations, etc.).
215
+ */
216
+ exec(sql) {
217
+ this.db.exec(sql);
218
+ }
219
+ /**
220
+ * Run multiple operations in a single transaction.
221
+ *
222
+ * @example
223
+ * db.transaction(() => {
224
+ * db.run('INSERT INTO orders ...', [...]);
225
+ * db.run('UPDATE inventory ...', [...]);
226
+ * });
227
+ */
228
+ transaction(fn) {
229
+ return this.db.transaction(fn)();
230
+ }
231
+ /**
232
+ * Close the database connection.
233
+ */
234
+ close() {
235
+ this.db.close();
236
+ }
237
+ // ─── Internal Message Storage ─────────────────────────────────────────────
238
+ saveMessage(params) {
239
+ this.db.prepare(`
240
+ INSERT OR REPLACE INTO svara_messages (id, session_id, role, content, tool_call_id)
241
+ VALUES (?, ?, ?, ?, ?)
242
+ `).run(
243
+ params.id,
244
+ params.sessionId,
245
+ params.role,
246
+ params.content,
247
+ params.toolCallId ?? null
248
+ );
249
+ }
250
+ getMessages(sessionId, limit = 50) {
251
+ return this.db.prepare(`
252
+ SELECT id, role, content, tool_call_id, created_at
253
+ FROM svara_messages
254
+ WHERE session_id = ?
255
+ ORDER BY created_at ASC
256
+ LIMIT ?
257
+ `).all(sessionId, limit);
258
+ }
259
+ clearSession(sessionId) {
260
+ this.db.prepare("DELETE FROM svara_messages WHERE session_id = ?").run(sessionId);
261
+ }
262
+ // ─── Private Setup ────────────────────────────────────────────────────────
263
+ openDatabase(dbPath) {
264
+ try {
265
+ const Database = require("better-sqlite3");
266
+ return new Database(dbPath);
267
+ } catch {
268
+ throw new Error(
269
+ '[SvaraJS] Database requires the "better-sqlite3" package.\nRun: npm install better-sqlite3'
270
+ );
271
+ }
272
+ }
273
+ configure() {
274
+ this.db.pragma("journal_mode = WAL");
275
+ this.db.pragma("synchronous = NORMAL");
276
+ this.db.pragma("foreign_keys = ON");
277
+ }
278
+ migrate() {
279
+ this.db.exec(CREATE_TABLES_SQL);
280
+ const meta = this.db.prepare(
281
+ "SELECT value FROM svara_meta WHERE key = 'schema_version'"
282
+ ).get();
283
+ if (!meta) {
284
+ this.db.prepare(INSERT_META_SQL).run(
285
+ String(SCHEMA_VERSION),
286
+ (/* @__PURE__ */ new Date()).toISOString()
287
+ );
288
+ }
289
+ }
290
+ };
291
+ }
292
+ });
293
+
33
294
  // src/rag/loader.ts
34
- var import_promises, import_path, import_crypto, TextFileLoader, JsonFileLoader, HtmlFileLoader, PdfFileLoader, DocxFileLoader, DocumentLoader;
295
+ var import_promises, import_path2, import_crypto, TextFileLoader, JsonFileLoader, HtmlFileLoader, PdfFileLoader, DocxFileLoader, DocumentLoader;
35
296
  var init_loader = __esm({
36
297
  "src/rag/loader.ts"() {
37
298
  "use strict";
38
299
  import_promises = __toESM(require("fs/promises"));
39
- import_path = __toESM(require("path"));
300
+ import_path2 = __toESM(require("path"));
40
301
  import_crypto = __toESM(require("crypto"));
41
302
  TextFileLoader = class {
42
303
  extensions = [".txt", ".md", ".mdx", ".rst", ".csv", ".log"];
@@ -48,7 +309,7 @@ var init_loader = __esm({
48
309
  extensions = [".json", ".jsonl"];
49
310
  async load(filePath) {
50
311
  const raw = await import_promises.default.readFile(filePath, "utf-8");
51
- if (import_path.default.extname(filePath) === ".jsonl") {
312
+ if (import_path2.default.extname(filePath) === ".jsonl") {
52
313
  return raw.split("\n").filter(Boolean).map((line) => {
53
314
  const obj = JSON.parse(line);
54
315
  return Object.values(obj).join(" ");
@@ -116,7 +377,7 @@ var init_loader = __esm({
116
377
  * Load a single file into a Document.
117
378
  */
118
379
  async load(filePath) {
119
- const ext = import_path.default.extname(filePath).toLowerCase();
380
+ const ext = import_path2.default.extname(filePath).toLowerCase();
120
381
  const loader = this.extensionMap.get(ext);
121
382
  if (!loader) {
122
383
  throw new Error(
@@ -131,7 +392,7 @@ var init_loader = __esm({
131
392
  type: this.detectType(ext),
132
393
  source: filePath,
133
394
  metadata: {
134
- filename: import_path.default.basename(filePath),
395
+ filename: import_path2.default.basename(filePath),
135
396
  extension: ext,
136
397
  size: stats.size,
137
398
  lastModified: stats.mtime.toISOString()
@@ -155,7 +416,7 @@ var init_loader = __esm({
155
416
  }
156
417
  /** Check if this loader supports a given file extension. */
157
418
  supports(filePath) {
158
- const ext = import_path.default.extname(filePath).toLowerCase();
419
+ const ext = import_path2.default.extname(filePath).toLowerCase();
159
420
  return this.extensionMap.has(ext);
160
421
  }
161
422
  detectType(ext) {
@@ -218,6 +479,7 @@ var init_chunker = __esm({
218
479
  id: this.chunkId(document.id, index),
219
480
  documentId: document.id,
220
481
  content: content.trim(),
482
+ source: document.source,
221
483
  index,
222
484
  metadata: {
223
485
  ...document.metadata,
@@ -313,12 +575,14 @@ function cosineSimilarity(a, b) {
313
575
  const denominator = Math.sqrt(normA) * Math.sqrt(normB);
314
576
  return denominator === 0 ? 0 : dot / denominator;
315
577
  }
316
- var OpenAIEmbeddings, OllamaEmbeddings, InMemoryVectorStore, VectorRetriever;
578
+ var import_crypto3, OpenAIEmbeddings, OllamaEmbeddings, VectorStore, PersistentVectorStore, VectorRetriever;
317
579
  var init_retriever = __esm({
318
580
  "src/rag/retriever.ts"() {
319
581
  "use strict";
320
582
  init_loader();
321
583
  init_chunker();
584
+ init_sqlite();
585
+ import_crypto3 = __toESM(require("crypto"));
322
586
  OpenAIEmbeddings = class {
323
587
  client;
324
588
  model;
@@ -373,25 +637,95 @@ var init_retriever = __esm({
373
637
  return data.embedding;
374
638
  }
375
639
  };
376
- InMemoryVectorStore = class {
377
- entries = [];
378
- add(chunk, embedding) {
379
- const existing = this.entries.findIndex((e) => e.chunk.id === chunk.id);
380
- if (existing >= 0) {
381
- this.entries[existing] = { chunk, embedding };
382
- } else {
383
- this.entries.push({ chunk, embedding });
384
- }
640
+ VectorStore = class {
641
+ contentHash(content) {
642
+ return import_crypto3.default.createHash("md5").update(content).digest("hex");
385
643
  }
386
- search(queryEmbedding, topK, threshold = 0) {
387
- const scored = this.entries.map((entry) => ({
388
- chunk: entry.chunk,
389
- score: cosineSimilarity(queryEmbedding, entry.embedding)
390
- }));
391
- return scored.filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).slice(0, topK).map((s) => s.chunk);
392
- }
393
- get size() {
394
- return this.entries.length;
644
+ };
645
+ PersistentVectorStore = class extends VectorStore {
646
+ constructor(db, agentName) {
647
+ super();
648
+ this.db = db;
649
+ this.agentName = agentName;
650
+ }
651
+ db;
652
+ agentName;
653
+ async add(chunk, embedding) {
654
+ const contentHash = this.contentHash(chunk.content);
655
+ const existing = this.db.query(
656
+ "SELECT id FROM svara_chunks WHERE agent_name = ? AND content_hash = ?",
657
+ [this.agentName, contentHash]
658
+ );
659
+ if (existing.length > 0) {
660
+ console.log(`[SvaraJS:RAG] Duplicate content detected for ${this.agentName}, skipping chunk ${chunk.id}`);
661
+ return;
662
+ }
663
+ const embeddingJson = JSON.stringify(embedding);
664
+ this.db.run(
665
+ `INSERT OR REPLACE INTO svara_chunks
666
+ (id, agent_name, document_id, content, content_hash, chunk_index, embedding, source, metadata)
667
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
668
+ [
669
+ chunk.id,
670
+ this.agentName,
671
+ chunk.documentId,
672
+ chunk.content,
673
+ contentHash,
674
+ chunk.index,
675
+ embeddingJson,
676
+ chunk.source,
677
+ JSON.stringify(chunk.metadata)
678
+ ]
679
+ );
680
+ }
681
+ async search(queryEmbedding, topK, threshold = 0) {
682
+ const rows = this.db.query(
683
+ "SELECT id, document_id, content, chunk_index, embedding, source, metadata FROM svara_chunks WHERE agent_name = ? ORDER BY id DESC",
684
+ [this.agentName]
685
+ );
686
+ const scored = rows.map((row) => {
687
+ const embedding = JSON.parse(row.embedding);
688
+ return {
689
+ chunk: {
690
+ id: row.id,
691
+ documentId: row.document_id,
692
+ content: row.content,
693
+ index: row.chunk_index,
694
+ source: row.source,
695
+ metadata: JSON.parse(row.metadata)
696
+ },
697
+ score: cosineSimilarity(queryEmbedding, embedding)
698
+ };
699
+ }).filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).slice(0, topK);
700
+ return scored.map((s) => s.chunk);
701
+ }
702
+ async searchWithScores(queryEmbedding, topK, threshold = 0) {
703
+ const rows = this.db.query(
704
+ "SELECT id, document_id, content, chunk_index, embedding, source, metadata FROM svara_chunks WHERE agent_name = ? ORDER BY id DESC",
705
+ [this.agentName]
706
+ );
707
+ const scored = rows.map((row) => {
708
+ const embedding = JSON.parse(row.embedding);
709
+ return {
710
+ chunk: {
711
+ id: row.id,
712
+ documentId: row.document_id,
713
+ content: row.content,
714
+ index: row.chunk_index,
715
+ source: row.source,
716
+ metadata: JSON.parse(row.metadata)
717
+ },
718
+ score: cosineSimilarity(queryEmbedding, embedding)
719
+ };
720
+ }).filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).slice(0, topK);
721
+ return scored;
722
+ }
723
+ async size() {
724
+ const result = this.db.query(
725
+ "SELECT COUNT(*) as count FROM svara_chunks WHERE agent_name = ?",
726
+ [this.agentName]
727
+ );
728
+ return result[0]?.count ?? 0;
395
729
  }
396
730
  };
397
731
  VectorRetriever = class {
@@ -400,13 +734,17 @@ var init_retriever = __esm({
400
734
  loader;
401
735
  chunker;
402
736
  config;
403
- constructor() {
404
- this.store = new InMemoryVectorStore();
737
+ db;
738
+ agentName;
739
+ constructor(agentName, db) {
740
+ this.agentName = agentName;
405
741
  this.loader = new DocumentLoader();
406
742
  this.chunker = new Chunker();
743
+ this.db = db || new SvaraDB("./data/svara.db");
407
744
  }
408
745
  async init(config) {
409
746
  this.config = config;
747
+ this.store = new PersistentVectorStore(this.db, this.agentName);
410
748
  if (config.chunking) {
411
749
  this.chunker = new Chunker({
412
750
  strategy: config.chunking.strategy ?? "sentence",
@@ -435,15 +773,17 @@ var init_retriever = __esm({
435
773
  console.log(`[SvaraJS:RAG] Embedding ${chunks.length} chunk(s)...`);
436
774
  const embeddings = await this.embedder.embed(chunks.map((c) => c.content));
437
775
  for (let i = 0; i < chunks.length; i++) {
438
- this.store.add(chunks[i], embeddings[i]);
776
+ await this.store.add(chunks[i], embeddings[i]);
439
777
  }
440
- console.log(`[SvaraJS:RAG] Vector store now has ${this.store.size} chunk(s).`);
778
+ const size = await this.store.size();
779
+ console.log(`[SvaraJS:RAG] Vector store now has ${size} chunk(s).`);
441
780
  }
442
781
  async retrieve(query, topK = 5) {
443
- if (this.store.size === 0) return "";
782
+ const size = await this.store.size();
783
+ if (size === 0) return "";
444
784
  const queryEmbedding = await this.embedder.embedOne(query);
445
785
  const threshold = this.config.retrieval?.threshold ?? 0.3;
446
- const chunks = this.store.search(queryEmbedding, topK, threshold);
786
+ const chunks = await this.store.search(queryEmbedding, topK, threshold);
447
787
  if (!chunks.length) return "";
448
788
  return chunks.map((chunk, i) => `[Context ${i + 1}]
449
789
  Source: ${String(chunk.metadata.filename ?? chunk.documentId)}
@@ -452,11 +792,11 @@ ${chunk.content}`).join("\n\n---\n\n");
452
792
  async retrieveChunks(query, topK = 5) {
453
793
  const queryEmbedding = await this.embedder.embedOne(query);
454
794
  const threshold = this.config.retrieval?.threshold ?? 0.3;
455
- const chunks = this.store.search(queryEmbedding, topK, threshold);
795
+ const chunksWithScores = await this.store.searchWithScores(queryEmbedding, topK, threshold);
456
796
  return {
457
- chunks,
797
+ chunks: chunksWithScores,
458
798
  query,
459
- totalFound: chunks.length
799
+ totalFound: chunksWithScores.length
460
800
  };
461
801
  }
462
802
  };
@@ -1294,6 +1634,9 @@ var ToolRegistry = class {
1294
1634
  }
1295
1635
  };
1296
1636
 
1637
+ // src/core/agent.ts
1638
+ init_sqlite();
1639
+
1297
1640
  // src/tools/executor.ts
1298
1641
  var ToolExecutor = class {
1299
1642
  constructor(registry) {
@@ -1354,13 +1697,17 @@ var SvaraAgent = class extends import_events.default {
1354
1697
  verbose;
1355
1698
  channels = /* @__PURE__ */ new Map();
1356
1699
  knowledgeBase = null;
1700
+ retriever = null;
1701
+ // Store VectorRetriever for retrieveChunks access
1357
1702
  knowledgePaths = [];
1358
1703
  isStarted = false;
1704
+ db;
1359
1705
  constructor(config) {
1360
1706
  super();
1361
1707
  this.name = config.name;
1362
1708
  this.maxIterations = config.maxIterations ?? 10;
1363
1709
  this.verbose = config.verbose ?? false;
1710
+ this.db = new SvaraDB("./data/svara.db");
1364
1711
  this.systemPrompt = config.systemPrompt ?? `You are ${config.name}, a helpful and friendly AI assistant. Be concise and accurate.`;
1365
1712
  this.llmConfig = resolveConfig(config.model, {
1366
1713
  temperature: config.temperature,
@@ -1479,7 +1826,8 @@ var SvaraAgent = class extends import_events.default {
1479
1826
  response: result.response,
1480
1827
  sessionId: result.sessionId,
1481
1828
  usage: result.usage,
1482
- toolsUsed: result.toolsUsed
1829
+ toolsUsed: result.toolsUsed,
1830
+ retrievedDocuments: result.retrievedDocuments || []
1483
1831
  });
1484
1832
  } catch (err) {
1485
1833
  const error = err;
@@ -1550,6 +1898,46 @@ var SvaraAgent = class extends import_events.default {
1550
1898
  await this.knowledgeBase.load(arr);
1551
1899
  }
1552
1900
  }
1901
+ // ─── Internal: User & Session Tracking ───────────────────────────────────────
1902
+ async trackUserAndSession(userId, sessionId, channel = "api") {
1903
+ try {
1904
+ const existingUser = this.db.query(
1905
+ "SELECT id FROM svara_users WHERE id = ?",
1906
+ [userId]
1907
+ );
1908
+ if (existingUser.length === 0) {
1909
+ this.db.run(
1910
+ `INSERT INTO svara_users (id, display_name, first_seen, last_seen)
1911
+ VALUES (?, ?, unixepoch(), unixepoch())`,
1912
+ [userId, userId]
1913
+ );
1914
+ } else {
1915
+ this.db.run(
1916
+ "UPDATE svara_users SET last_seen = unixepoch() WHERE id = ?",
1917
+ [userId]
1918
+ );
1919
+ }
1920
+ const existingSession = this.db.query(
1921
+ "SELECT id FROM svara_sessions WHERE id = ?",
1922
+ [sessionId]
1923
+ );
1924
+ if (existingSession.length === 0) {
1925
+ this.db.run(
1926
+ `INSERT INTO svara_sessions (id, user_id, channel, created_at, updated_at)
1927
+ VALUES (?, ?, ?, unixepoch(), unixepoch())`,
1928
+ [sessionId, userId, channel]
1929
+ );
1930
+ } else {
1931
+ this.db.run(
1932
+ "UPDATE svara_sessions SET updated_at = unixepoch() WHERE id = ?",
1933
+ [sessionId]
1934
+ );
1935
+ }
1936
+ this.log("debug", `Tracked user ${userId} with session ${sessionId}`);
1937
+ } catch (error) {
1938
+ this.log("error", `Failed to track user: ${error.message}`);
1939
+ }
1940
+ }
1553
1941
  // ─── Internal: Agentic Loop ───────────────────────────────────────────────
1554
1942
  /**
1555
1943
  * Receives a raw incoming message from a channel and processes it.
@@ -1562,13 +1950,33 @@ var SvaraAgent = class extends import_events.default {
1562
1950
  });
1563
1951
  }
1564
1952
  async run(message, options) {
1953
+ console.log(`
1954
+ [RUN START] kb=${!!this.knowledgeBase} ret=${!!this.retriever}`);
1565
1955
  const startTime = Date.now();
1566
1956
  const sessionId = options.sessionId ?? crypto.randomUUID();
1567
- this.emit("message:received", { message, sessionId, userId: options.userId });
1957
+ const userId = options.userId ?? "unknown";
1958
+ await this.trackUserAndSession(userId, sessionId);
1959
+ this.emit("message:received", { message, sessionId, userId });
1568
1960
  const history = await this.memory.getHistory(sessionId);
1569
1961
  let ragContext = "";
1570
- if (this.knowledgeBase) {
1962
+ let retrievedDocuments = [];
1963
+ if (this.knowledgeBase && this.retriever) {
1571
1964
  ragContext = await this.knowledgeBase.retrieve(message);
1965
+ try {
1966
+ console.log(`[DEBUG] Calling retrieveChunks for query: "${message}"`);
1967
+ const context = await this.retriever.retrieveChunks(message, 3);
1968
+ console.log(`[DEBUG] Retrieved ${context.chunks.length} chunks`);
1969
+ retrievedDocuments = context.chunks.map((item) => ({
1970
+ source: item.chunk?.source || "unknown",
1971
+ score: Math.round(item.score * 100) / 100,
1972
+ excerpt: item.chunk?.content?.substring(0, 150) || ""
1973
+ }));
1974
+ console.log(`[DEBUG] Mapped ${retrievedDocuments.length} documents`);
1975
+ } catch (e) {
1976
+ console.error(`[ERROR] RAG retrieval failed:`, e);
1977
+ }
1978
+ } else {
1979
+ console.log(`[DEBUG] No knowledgeBase (${!!this.knowledgeBase}) or retriever (${!!this.retriever})`);
1572
1980
  }
1573
1981
  const messages = this.context.buildMessages(
1574
1982
  this.systemPrompt,
@@ -1578,7 +1986,7 @@ var SvaraAgent = class extends import_events.default {
1578
1986
  );
1579
1987
  const internalCtx = {
1580
1988
  sessionId,
1581
- userId: options.userId ?? "unknown",
1989
+ userId,
1582
1990
  agentName: this.name,
1583
1991
  history,
1584
1992
  metadata: options.metadata ?? {}
@@ -1635,7 +2043,8 @@ var SvaraAgent = class extends import_events.default {
1635
2043
  toolsUsed: [...new Set(toolsUsed)],
1636
2044
  iterations,
1637
2045
  usage: totalUsage,
1638
- duration: Date.now() - startTime
2046
+ duration: Date.now() - startTime,
2047
+ retrievedDocuments: retrievedDocuments.length > 0 ? retrievedDocuments : void 0
1639
2048
  };
1640
2049
  this.emit("message:sent", { response: finalResponse, sessionId });
1641
2050
  return result;
@@ -1645,8 +2054,8 @@ var SvaraAgent = class extends import_events.default {
1645
2054
  try {
1646
2055
  const { glob } = await import("glob");
1647
2056
  const { VectorRetriever: VectorRetriever2 } = await Promise.resolve().then(() => (init_retriever(), retriever_exports));
1648
- const retriever = new VectorRetriever2();
1649
- await retriever.init({ embeddings: { provider: "openai" } });
2057
+ this.retriever = new VectorRetriever2(this.name, this.db);
2058
+ await this.retriever.init({ embeddings: { provider: "openai" } });
1650
2059
  const files = [];
1651
2060
  for (const pattern of paths) {
1652
2061
  const matches = await glob(pattern);
@@ -1656,16 +2065,16 @@ var SvaraAgent = class extends import_events.default {
1656
2065
  console.warn(`[@yesvara/svara] No files found matching: ${paths.join(", ")}`);
1657
2066
  return;
1658
2067
  }
1659
- await retriever.addDocuments(files);
2068
+ await this.retriever.addDocuments(files);
1660
2069
  this.knowledgeBase = {
1661
2070
  load: async (p) => {
1662
2071
  const newFiles = [];
1663
2072
  for (const pattern of Array.isArray(p) ? p : [p]) {
1664
2073
  newFiles.push(...await glob(pattern));
1665
2074
  }
1666
- await retriever.addDocuments(newFiles);
2075
+ await this.retriever.addDocuments(newFiles);
1667
2076
  },
1668
- retrieve: (query, topK) => retriever.retrieve(query, topK)
2077
+ retrieve: (query, topK) => this.retriever.retrieve(query, topK)
1669
2078
  };
1670
2079
  this.log("info", `Knowledge base loaded: ${files.length} file(s).`);
1671
2080
  } catch (err) {
@@ -1727,232 +2136,8 @@ function createTool(definition) {
1727
2136
  };
1728
2137
  }
1729
2138
 
1730
- // src/database/sqlite.ts
1731
- var import_path2 = __toESM(require("path"));
1732
- var import_fs = __toESM(require("fs"));
1733
-
1734
- // src/database/schema.ts
1735
- var SCHEMA_VERSION = 1;
1736
- var CREATE_TABLES_SQL = `
1737
- -- Schema version tracking
1738
- CREATE TABLE IF NOT EXISTS svara_meta (
1739
- key TEXT PRIMARY KEY,
1740
- value TEXT NOT NULL
1741
- );
1742
-
1743
- -- Conversation history persistence
1744
- CREATE TABLE IF NOT EXISTS svara_messages (
1745
- id TEXT PRIMARY KEY,
1746
- session_id TEXT NOT NULL,
1747
- role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
1748
- content TEXT NOT NULL,
1749
- tool_call_id TEXT,
1750
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
1751
- );
1752
-
1753
- CREATE INDEX IF NOT EXISTS idx_messages_session
1754
- ON svara_messages (session_id, created_at);
1755
-
1756
- -- Session metadata
1757
- CREATE TABLE IF NOT EXISTS svara_sessions (
1758
- id TEXT PRIMARY KEY,
1759
- user_id TEXT,
1760
- channel TEXT NOT NULL,
1761
- created_at INTEGER NOT NULL DEFAULT (unixepoch()),
1762
- updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
1763
- metadata TEXT DEFAULT '{}'
1764
- );
1765
-
1766
- -- Vector store chunks for RAG
1767
- CREATE TABLE IF NOT EXISTS svara_chunks (
1768
- id TEXT PRIMARY KEY,
1769
- document_id TEXT NOT NULL,
1770
- content TEXT NOT NULL,
1771
- chunk_index INTEGER NOT NULL,
1772
- embedding BLOB, -- stored as binary float32 array
1773
- source TEXT NOT NULL,
1774
- metadata TEXT DEFAULT '{}',
1775
- created_at INTEGER NOT NULL DEFAULT (unixepoch())
1776
- );
1777
-
1778
- CREATE INDEX IF NOT EXISTS idx_chunks_document
1779
- ON svara_chunks (document_id);
1780
-
1781
- -- Document registry
1782
- CREATE TABLE IF NOT EXISTS svara_documents (
1783
- id TEXT PRIMARY KEY,
1784
- source TEXT NOT NULL UNIQUE,
1785
- type TEXT NOT NULL,
1786
- size INTEGER,
1787
- hash TEXT,
1788
- indexed_at INTEGER NOT NULL DEFAULT (unixepoch()),
1789
- metadata TEXT DEFAULT '{}'
1790
- );
1791
-
1792
- -- Key-value store for arbitrary agent state
1793
- CREATE TABLE IF NOT EXISTS svara_kv (
1794
- key TEXT PRIMARY KEY,
1795
- value TEXT NOT NULL,
1796
- expires_at INTEGER, -- unix timestamp, NULL = no expiry
1797
- updated_at INTEGER NOT NULL DEFAULT (unixepoch())
1798
- );
1799
- `;
1800
- var INSERT_META_SQL = `
1801
- INSERT OR REPLACE INTO svara_meta (key, value)
1802
- VALUES ('schema_version', ?), ('created_at', ?);
1803
- `;
1804
-
1805
- // src/database/sqlite.ts
1806
- var KVStore = class {
1807
- constructor(db) {
1808
- this.db = db;
1809
- }
1810
- db;
1811
- /** Set a key-value pair, with optional TTL in seconds. */
1812
- set(key, value, ttlSeconds) {
1813
- const expiresAt = ttlSeconds ? Math.floor(Date.now() / 1e3) + ttlSeconds : null;
1814
- this.db.prepare(`
1815
- INSERT OR REPLACE INTO svara_kv (key, value, expires_at, updated_at)
1816
- VALUES (?, ?, ?, unixepoch())
1817
- `).run(key, JSON.stringify(value), expiresAt);
1818
- }
1819
- /** Get a value by key. Returns undefined if not found or expired. */
1820
- get(key) {
1821
- const row = this.db.prepare(`
1822
- SELECT value, expires_at FROM svara_kv
1823
- WHERE key = ? AND (expires_at IS NULL OR expires_at > unixepoch())
1824
- `).get(key);
1825
- if (!row) return void 0;
1826
- return JSON.parse(row.value);
1827
- }
1828
- /** Delete a key. */
1829
- delete(key) {
1830
- this.db.prepare("DELETE FROM svara_kv WHERE key = ?").run(key);
1831
- }
1832
- /** Check if a key exists and is not expired. */
1833
- has(key) {
1834
- return this.get(key) !== void 0;
1835
- }
1836
- /** Get all keys matching a prefix. */
1837
- keys(prefix = "") {
1838
- const rows = this.db.prepare(`
1839
- SELECT key FROM svara_kv
1840
- WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > unixepoch())
1841
- `).all(`${prefix}%`);
1842
- return rows.map((r) => r.key);
1843
- }
1844
- };
1845
- var SvaraDB = class {
1846
- db;
1847
- kv;
1848
- constructor(dbPath = ":memory:") {
1849
- if (dbPath !== ":memory:") {
1850
- import_fs.default.mkdirSync(import_path2.default.dirname(import_path2.default.resolve(dbPath)), { recursive: true });
1851
- }
1852
- this.db = this.openDatabase(dbPath);
1853
- this.configure();
1854
- this.migrate();
1855
- this.kv = new KVStore(this.db);
1856
- }
1857
- // ─── Query Helpers ────────────────────────────────────────────────────────
1858
- /**
1859
- * Run a SELECT and return all matching rows.
1860
- */
1861
- query(sql, params = []) {
1862
- return this.db.prepare(sql).all(...params);
1863
- }
1864
- /**
1865
- * Run a SELECT and return the first matching row.
1866
- */
1867
- queryOne(sql, params = []) {
1868
- return this.db.prepare(sql).get(...params);
1869
- }
1870
- /**
1871
- * Run an INSERT/UPDATE/DELETE. Returns affected row count.
1872
- */
1873
- run(sql, params = []) {
1874
- return this.db.prepare(sql).run(...params).changes;
1875
- }
1876
- /**
1877
- * Execute raw SQL (for DDL, migrations, etc.).
1878
- */
1879
- exec(sql) {
1880
- this.db.exec(sql);
1881
- }
1882
- /**
1883
- * Run multiple operations in a single transaction.
1884
- *
1885
- * @example
1886
- * db.transaction(() => {
1887
- * db.run('INSERT INTO orders ...', [...]);
1888
- * db.run('UPDATE inventory ...', [...]);
1889
- * });
1890
- */
1891
- transaction(fn) {
1892
- return this.db.transaction(fn)();
1893
- }
1894
- /**
1895
- * Close the database connection.
1896
- */
1897
- close() {
1898
- this.db.close();
1899
- }
1900
- // ─── Internal Message Storage ─────────────────────────────────────────────
1901
- saveMessage(params) {
1902
- this.db.prepare(`
1903
- INSERT OR REPLACE INTO svara_messages (id, session_id, role, content, tool_call_id)
1904
- VALUES (?, ?, ?, ?, ?)
1905
- `).run(
1906
- params.id,
1907
- params.sessionId,
1908
- params.role,
1909
- params.content,
1910
- params.toolCallId ?? null
1911
- );
1912
- }
1913
- getMessages(sessionId, limit = 50) {
1914
- return this.db.prepare(`
1915
- SELECT id, role, content, tool_call_id, created_at
1916
- FROM svara_messages
1917
- WHERE session_id = ?
1918
- ORDER BY created_at ASC
1919
- LIMIT ?
1920
- `).all(sessionId, limit);
1921
- }
1922
- clearSession(sessionId) {
1923
- this.db.prepare("DELETE FROM svara_messages WHERE session_id = ?").run(sessionId);
1924
- }
1925
- // ─── Private Setup ────────────────────────────────────────────────────────
1926
- openDatabase(dbPath) {
1927
- try {
1928
- const Database = require("better-sqlite3");
1929
- return new Database(dbPath);
1930
- } catch {
1931
- throw new Error(
1932
- '[SvaraJS] Database requires the "better-sqlite3" package.\nRun: npm install better-sqlite3'
1933
- );
1934
- }
1935
- }
1936
- configure() {
1937
- this.db.pragma("journal_mode = WAL");
1938
- this.db.pragma("synchronous = NORMAL");
1939
- this.db.pragma("foreign_keys = ON");
1940
- }
1941
- migrate() {
1942
- this.db.exec(CREATE_TABLES_SQL);
1943
- const meta = this.db.prepare(
1944
- "SELECT value FROM svara_meta WHERE key = 'schema_version'"
1945
- ).get();
1946
- if (!meta) {
1947
- this.db.prepare(INSERT_META_SQL).run(
1948
- String(SCHEMA_VERSION),
1949
- (/* @__PURE__ */ new Date()).toISOString()
1950
- );
1951
- }
1952
- }
1953
- };
1954
-
1955
2139
  // src/index.ts
2140
+ init_sqlite();
1956
2141
  init_web();
1957
2142
  init_telegram();
1958
2143
  init_whatsapp();