@yesvara/svara 0.1.0

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 (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/chunk-CIESM3BP.mjs +33 -0
  4. package/dist/chunk-FEA5KIJN.mjs +418 -0
  5. package/dist/cli/index.d.mts +1 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/index.js +328 -0
  8. package/dist/cli/index.mjs +39 -0
  9. package/dist/dev-OYGXXK2B.mjs +69 -0
  10. package/dist/index.d.mts +967 -0
  11. package/dist/index.d.ts +967 -0
  12. package/dist/index.js +1976 -0
  13. package/dist/index.mjs +1502 -0
  14. package/dist/new-7K4NIDZO.mjs +177 -0
  15. package/dist/retriever-4QY667XF.mjs +7 -0
  16. package/examples/01-basic/index.ts +26 -0
  17. package/examples/02-with-tools/index.ts +73 -0
  18. package/examples/03-rag-knowledge/index.ts +41 -0
  19. package/examples/04-multi-channel/index.ts +91 -0
  20. package/package.json +74 -0
  21. package/src/app/index.ts +176 -0
  22. package/src/channels/telegram.ts +122 -0
  23. package/src/channels/web.ts +118 -0
  24. package/src/channels/whatsapp.ts +161 -0
  25. package/src/cli/commands/dev.ts +87 -0
  26. package/src/cli/commands/new.ts +213 -0
  27. package/src/cli/index.ts +78 -0
  28. package/src/core/agent.ts +607 -0
  29. package/src/core/llm.ts +406 -0
  30. package/src/core/types.ts +183 -0
  31. package/src/database/schema.ts +79 -0
  32. package/src/database/sqlite.ts +239 -0
  33. package/src/index.ts +94 -0
  34. package/src/memory/context.ts +49 -0
  35. package/src/memory/conversation.ts +51 -0
  36. package/src/rag/chunker.ts +165 -0
  37. package/src/rag/loader.ts +216 -0
  38. package/src/rag/retriever.ts +248 -0
  39. package/src/tools/executor.ts +54 -0
  40. package/src/tools/index.ts +89 -0
  41. package/src/tools/registry.ts +44 -0
  42. package/src/types.ts +131 -0
  43. package/tsconfig.json +26 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,1502 @@
1
+ import {
2
+ Chunker,
3
+ DocumentLoader,
4
+ VectorRetriever
5
+ } from "./chunk-FEA5KIJN.mjs";
6
+ import {
7
+ __esm,
8
+ __export,
9
+ __require,
10
+ __toCommonJS
11
+ } from "./chunk-CIESM3BP.mjs";
12
+
13
+ // src/channels/web.ts
14
+ var web_exports = {};
15
+ __export(web_exports, {
16
+ WebChannel: () => WebChannel
17
+ });
18
+ import express2 from "express";
19
+ var WebChannel;
20
+ var init_web = __esm({
21
+ "src/channels/web.ts"() {
22
+ "use strict";
23
+ WebChannel = class {
24
+ constructor(config = {}) {
25
+ this.config = config;
26
+ this.app = this.buildApp();
27
+ }
28
+ config;
29
+ name = "web";
30
+ app;
31
+ server = null;
32
+ agent;
33
+ async mount(agent) {
34
+ this.agent = agent;
35
+ this.attachRoutes();
36
+ const port = this.config.port ?? 3e3;
37
+ return new Promise((resolve, reject) => {
38
+ this.server = this.app.listen(port, () => {
39
+ console.log(`[@yesvara/svara] Web channel running at http://localhost:${port}`);
40
+ resolve();
41
+ });
42
+ this.server.on("error", reject);
43
+ });
44
+ }
45
+ async send(_sessionId, _text) {
46
+ }
47
+ async stop() {
48
+ return new Promise((resolve) => {
49
+ this.server?.close(() => resolve()) ?? resolve();
50
+ });
51
+ }
52
+ // ─── Private ───────────────────────────────────────────────────────────────
53
+ buildApp() {
54
+ const app = express2();
55
+ app.use(express2.json({ limit: "10mb" }));
56
+ if (this.config.cors) {
57
+ const origin = this.config.cors === true ? "*" : this.config.cors;
58
+ app.use((_req, res, next) => {
59
+ res.setHeader("Access-Control-Allow-Origin", origin);
60
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
61
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
62
+ next();
63
+ });
64
+ app.options("*", (_req, res) => res.sendStatus(204));
65
+ }
66
+ if (this.config.apiKey) {
67
+ app.use((req, res, next) => {
68
+ const token = req.headers.authorization?.replace("Bearer ", "");
69
+ if (token !== this.config.apiKey) {
70
+ res.status(401).json({ error: "Unauthorized" });
71
+ return;
72
+ }
73
+ next();
74
+ });
75
+ }
76
+ return app;
77
+ }
78
+ attachRoutes() {
79
+ const base = this.config.path ?? "";
80
+ this.app.get(`${base}/health`, (_req, res) => {
81
+ res.json({ status: "ok", agent: this.agent.name, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
82
+ });
83
+ this.app.post(`${base}/chat`, this.agent.handler());
84
+ }
85
+ buildMessage(body) {
86
+ return {
87
+ id: crypto.randomUUID(),
88
+ sessionId: body.sessionId ?? crypto.randomUUID(),
89
+ userId: body.userId ?? "web-user",
90
+ channel: "web",
91
+ text: body.message,
92
+ timestamp: /* @__PURE__ */ new Date()
93
+ };
94
+ }
95
+ };
96
+ }
97
+ });
98
+
99
+ // src/channels/telegram.ts
100
+ var telegram_exports = {};
101
+ __export(telegram_exports, {
102
+ TelegramChannel: () => TelegramChannel
103
+ });
104
+ var TelegramChannel;
105
+ var init_telegram = __esm({
106
+ "src/channels/telegram.ts"() {
107
+ "use strict";
108
+ TelegramChannel = class {
109
+ constructor(config) {
110
+ this.config = config;
111
+ if (!config.token) throw new Error("[@yesvara/svara] Telegram requires a bot token.");
112
+ this.baseUrl = `https://api.telegram.org/bot${config.token}`;
113
+ }
114
+ config;
115
+ name = "telegram";
116
+ agent;
117
+ baseUrl;
118
+ lastUpdateId = 0;
119
+ pollingTimer = null;
120
+ async mount(agent) {
121
+ this.agent = agent;
122
+ const me = await this.api("getMe");
123
+ console.log(`[@yesvara/svara] Telegram connected as @${me.username}`);
124
+ if (this.config.mode === "webhook" && this.config.webhookUrl) {
125
+ await this.api("setWebhook", { url: `${this.config.webhookUrl}/telegram/webhook` });
126
+ console.log(`[@yesvara/svara] Telegram webhook registered.`);
127
+ } else {
128
+ this.startPolling();
129
+ }
130
+ }
131
+ async send(sessionId, text) {
132
+ const chatId = parseInt(sessionId, 10);
133
+ if (!isNaN(chatId)) await this.sendMessage(chatId, text);
134
+ }
135
+ async stop() {
136
+ if (this.pollingTimer) clearInterval(this.pollingTimer);
137
+ }
138
+ startPolling() {
139
+ const interval = this.config.pollingInterval ?? 1e3;
140
+ console.log("[@yesvara/svara] Telegram polling started...");
141
+ this.pollingTimer = setInterval(async () => {
142
+ try {
143
+ const updates = await this.api("getUpdates", {
144
+ offset: this.lastUpdateId + 1,
145
+ allowed_updates: ["message"]
146
+ });
147
+ for (const update of updates) {
148
+ this.lastUpdateId = update.update_id;
149
+ if (update.message?.text) await this.handleUpdate(update);
150
+ }
151
+ } catch {
152
+ }
153
+ }, interval);
154
+ }
155
+ async handleUpdate(update) {
156
+ const msg = update.message;
157
+ const message = {
158
+ id: String(msg.message_id),
159
+ sessionId: String(msg.chat.id),
160
+ userId: String(msg.from.id),
161
+ channel: "telegram",
162
+ text: msg.text ?? "",
163
+ timestamp: new Date(msg.date * 1e3),
164
+ raw: msg
165
+ };
166
+ await this.api("sendChatAction", { chat_id: msg.chat.id, action: "typing" }).catch(() => {
167
+ });
168
+ try {
169
+ const result = await this.agent.receive(message);
170
+ for (const chunk of this.split(result.response, 4096)) {
171
+ await this.sendMessage(msg.chat.id, chunk);
172
+ }
173
+ } catch (err) {
174
+ await this.sendMessage(msg.chat.id, "Sorry, something went wrong. Please try again.");
175
+ console.error("[@yesvara/svara] Telegram error:", err.message);
176
+ }
177
+ }
178
+ async sendMessage(chatId, text) {
179
+ await this.api("sendMessage", { chat_id: chatId, text, parse_mode: "Markdown" });
180
+ }
181
+ async api(method, params) {
182
+ const res = await fetch(`${this.baseUrl}/${method}`, {
183
+ method: params ? "POST" : "GET",
184
+ headers: { "Content-Type": "application/json" },
185
+ body: params ? JSON.stringify(params) : void 0
186
+ });
187
+ const data = await res.json();
188
+ if (!data.ok) throw new Error(`Telegram API: ${data.description}`);
189
+ return data.result;
190
+ }
191
+ split(text, max) {
192
+ if (text.length <= max) return [text];
193
+ const chunks = [];
194
+ let rest = text;
195
+ while (rest.length > 0) {
196
+ chunks.push(rest.slice(0, max));
197
+ rest = rest.slice(max);
198
+ }
199
+ return chunks;
200
+ }
201
+ };
202
+ }
203
+ });
204
+
205
+ // src/channels/whatsapp.ts
206
+ var whatsapp_exports = {};
207
+ __export(whatsapp_exports, {
208
+ WhatsAppChannel: () => WhatsAppChannel
209
+ });
210
+ var WhatsAppChannel;
211
+ var init_whatsapp = __esm({
212
+ "src/channels/whatsapp.ts"() {
213
+ "use strict";
214
+ WhatsAppChannel = class {
215
+ constructor(config) {
216
+ this.config = config;
217
+ if (!config.token || !config.phoneId || !config.verifyToken) {
218
+ throw new Error(
219
+ "[@yesvara/svara] WhatsApp requires: token, phoneId, and verifyToken."
220
+ );
221
+ }
222
+ const version = config.apiVersion ?? "v19.0";
223
+ this.apiUrl = `https://graph.facebook.com/${version}/${config.phoneId}`;
224
+ }
225
+ config;
226
+ name = "whatsapp";
227
+ agent;
228
+ apiUrl;
229
+ async mount(agent) {
230
+ this.agent = agent;
231
+ const webChannel = agent.channels?.get("web");
232
+ const app = webChannel?.app;
233
+ if (!app) {
234
+ console.warn(
235
+ '[@yesvara/svara] WhatsApp: no "web" channel found. Add connectChannel("web", ...) before connectChannel("whatsapp", ...) so the webhook can be mounted.'
236
+ );
237
+ return;
238
+ }
239
+ app.get("/whatsapp/webhook", (req, res) => {
240
+ const { "hub.mode": mode, "hub.verify_token": token, "hub.challenge": challenge } = req.query;
241
+ if (mode === "subscribe" && token === this.config.verifyToken) {
242
+ console.log("[@yesvara/svara] WhatsApp webhook verified.");
243
+ res.status(200).send(challenge);
244
+ } else {
245
+ res.status(403).send("Forbidden");
246
+ }
247
+ });
248
+ app.post("/whatsapp/webhook", async (req, res) => {
249
+ res.sendStatus(200);
250
+ if (req.body.object !== "whatsapp_business_account") return;
251
+ for (const entry of req.body.entry) {
252
+ for (const change of entry.changes) {
253
+ for (const waMsg of change.value.messages ?? []) {
254
+ if (waMsg.type !== "text" || !waMsg.text?.body) continue;
255
+ await this.handle(waMsg).catch(
256
+ (err) => console.error("[@yesvara/svara] WhatsApp error:", err.message)
257
+ );
258
+ }
259
+ }
260
+ }
261
+ });
262
+ console.log("[@yesvara/svara] WhatsApp webhook mounted at /whatsapp/webhook");
263
+ }
264
+ async send(to, text) {
265
+ await this.sendMessage(to, text);
266
+ }
267
+ async stop() {
268
+ }
269
+ async handle(waMsg) {
270
+ const message = {
271
+ id: waMsg.id,
272
+ sessionId: waMsg.from,
273
+ userId: waMsg.from,
274
+ channel: "whatsapp",
275
+ text: waMsg.text?.body ?? "",
276
+ timestamp: new Date(parseInt(waMsg.timestamp) * 1e3),
277
+ raw: waMsg
278
+ };
279
+ try {
280
+ const result = await this.agent.receive(message);
281
+ for (const chunk of this.split(result.response, 4e3)) {
282
+ await this.sendMessage(waMsg.from, chunk);
283
+ }
284
+ } catch (err) {
285
+ await this.sendMessage(waMsg.from, "Sorry, something went wrong. Please try again.");
286
+ throw err;
287
+ }
288
+ }
289
+ async sendMessage(to, text) {
290
+ const res = await fetch(`${this.apiUrl}/messages`, {
291
+ method: "POST",
292
+ headers: {
293
+ Authorization: `Bearer ${this.config.token}`,
294
+ "Content-Type": "application/json"
295
+ },
296
+ body: JSON.stringify({
297
+ messaging_product: "whatsapp",
298
+ to,
299
+ type: "text",
300
+ text: { body: text }
301
+ })
302
+ });
303
+ if (!res.ok) {
304
+ const err = await res.json();
305
+ throw new Error(`WhatsApp API: ${err.error?.message}`);
306
+ }
307
+ }
308
+ split(text, max) {
309
+ if (text.length <= max) return [text];
310
+ const chunks = [];
311
+ let rest = text;
312
+ while (rest.length > 0) {
313
+ chunks.push(rest.slice(0, max));
314
+ rest = rest.slice(max);
315
+ }
316
+ return chunks;
317
+ }
318
+ };
319
+ }
320
+ });
321
+
322
+ // src/app/index.ts
323
+ import express from "express";
324
+ import { createServer } from "http";
325
+ var SvaraApp = class {
326
+ express;
327
+ server = null;
328
+ constructor(options = {}) {
329
+ this.express = express();
330
+ this.setup(options);
331
+ }
332
+ // ─── Public API ────────────────────────────────────────────────────────────
333
+ /**
334
+ * Mount an agent (or any Express handler) on a route.
335
+ * Returns `this` for chaining.
336
+ *
337
+ * @example
338
+ * app
339
+ * .route('/chat', supportAgent.handler())
340
+ * .route('/sales', salesAgent.handler());
341
+ */
342
+ route(path2, handler) {
343
+ this.express.post(path2, handler);
344
+ return this;
345
+ }
346
+ /**
347
+ * Add Express middleware (logging, auth, rate limiting, etc.)
348
+ *
349
+ * @example
350
+ * import rateLimit from 'express-rate-limit';
351
+ * app.use(rateLimit({ windowMs: 60_000, max: 100 }));
352
+ */
353
+ use(middleware) {
354
+ this.express.use(middleware);
355
+ return this;
356
+ }
357
+ /**
358
+ * Start listening on the given port.
359
+ *
360
+ * @example
361
+ * await app.listen(3000);
362
+ * // → [@yesvara/svara] Listening at http://localhost:3000
363
+ */
364
+ listen(port = 3e3) {
365
+ return new Promise((resolve, reject) => {
366
+ this.server = createServer(this.express);
367
+ this.server.listen(port, () => {
368
+ console.log(`[@yesvara/svara] Server running at http://localhost:${port}`);
369
+ resolve();
370
+ });
371
+ this.server.on("error", reject);
372
+ });
373
+ }
374
+ /**
375
+ * Stop the server gracefully.
376
+ */
377
+ stop() {
378
+ return new Promise((resolve) => {
379
+ if (this.server) {
380
+ this.server.close(() => resolve());
381
+ } else {
382
+ resolve();
383
+ }
384
+ });
385
+ }
386
+ /**
387
+ * Access the underlying Express app for advanced configuration.
388
+ *
389
+ * @example
390
+ * const expressApp = app.express();
391
+ * expressApp.set('trust proxy', 1);
392
+ */
393
+ getExpressApp() {
394
+ return this.express;
395
+ }
396
+ // ─── Private Setup ────────────────────────────────────────────────────────
397
+ setup(options) {
398
+ this.express.use(express.json({ limit: options.bodyLimit ?? "10mb" }));
399
+ if (options.cors) {
400
+ this.express.use((_req, res, next) => {
401
+ const origin = options.cors === true ? "*" : options.cors;
402
+ res.setHeader("Access-Control-Allow-Origin", origin);
403
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
404
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
405
+ next();
406
+ });
407
+ this.express.options("*", (_req, res) => res.sendStatus(204));
408
+ }
409
+ if (options.apiKey) {
410
+ this.express.use((req, res, next) => {
411
+ const token = req.headers.authorization?.replace("Bearer ", "");
412
+ if (token !== options.apiKey) {
413
+ res.status(401).json({ error: "Unauthorized", message: "Invalid API key." });
414
+ return;
415
+ }
416
+ next();
417
+ });
418
+ }
419
+ this.express.get("/health", (_req, res) => {
420
+ res.json({
421
+ status: "ok",
422
+ framework: "@yesvara/svara",
423
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
424
+ });
425
+ });
426
+ }
427
+ };
428
+
429
+ // src/core/agent.ts
430
+ import EventEmitter from "events";
431
+
432
+ // src/core/llm.ts
433
+ var OPENAI_PREFIXES = ["gpt-", "o1", "o3", "text-davinci", "chatgpt"];
434
+ var ANTHROPIC_PREFIXES = ["claude-"];
435
+ var GROQ_MODELS = [
436
+ "llama-3.1-405b",
437
+ "llama-3.1-70b",
438
+ "llama-3.1-8b",
439
+ "mixtral-8x7b",
440
+ "gemma-7b",
441
+ "gemma2-9b"
442
+ ];
443
+ function detectProvider(model) {
444
+ const m = model.toLowerCase();
445
+ if (OPENAI_PREFIXES.some((p) => m.startsWith(p))) return "openai";
446
+ if (ANTHROPIC_PREFIXES.some((p) => m.startsWith(p))) return "anthropic";
447
+ if (GROQ_MODELS.some((gm) => m.includes(gm)) && process.env.GROQ_API_KEY) {
448
+ return "groq";
449
+ }
450
+ return "ollama";
451
+ }
452
+ function resolveConfig(model, overrides = {}) {
453
+ const provider = overrides.provider ?? detectProvider(model);
454
+ return {
455
+ provider,
456
+ model,
457
+ temperature: 0.7,
458
+ timeout: 6e4,
459
+ ...overrides
460
+ };
461
+ }
462
+ var OpenAIAdapter = class {
463
+ constructor(config) {
464
+ this.config = config;
465
+ this.client = this.init();
466
+ }
467
+ config;
468
+ client;
469
+ init() {
470
+ try {
471
+ const { default: OpenAI } = __require("openai");
472
+ return new OpenAI({
473
+ apiKey: this.config.apiKey ?? process.env.OPENAI_API_KEY,
474
+ baseURL: this.config.baseURL,
475
+ timeout: this.config.timeout
476
+ });
477
+ } catch {
478
+ throw new SvaraLLMError(
479
+ "openai",
480
+ "Package not found. Run: npm install openai"
481
+ );
482
+ }
483
+ }
484
+ async chat(messages, tools, temperature) {
485
+ const client = this.client;
486
+ const response = await client.chat.completions.create({
487
+ model: this.config.model,
488
+ messages: messages.map(toOpenAIMessage),
489
+ tools: tools?.length ? tools.map(toOpenAITool) : void 0,
490
+ tool_choice: tools?.length ? "auto" : void 0,
491
+ temperature: temperature ?? this.config.temperature ?? 0.7,
492
+ max_tokens: this.config.maxTokens
493
+ });
494
+ const choice = response.choices[0];
495
+ const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
496
+ id: tc.id,
497
+ name: tc.function.name,
498
+ arguments: safeParseJSON(tc.function.arguments)
499
+ }));
500
+ return {
501
+ content: choice.message.content ?? "",
502
+ toolCalls: toolCalls.length ? toolCalls : void 0,
503
+ usage: {
504
+ promptTokens: response.usage.prompt_tokens,
505
+ completionTokens: response.usage.completion_tokens,
506
+ totalTokens: response.usage.total_tokens
507
+ },
508
+ model: response.model,
509
+ finishReason: choice.finish_reason === "tool_calls" ? "tool_calls" : "stop"
510
+ };
511
+ }
512
+ countTokens(text) {
513
+ return Math.ceil(text.length / 4);
514
+ }
515
+ };
516
+ var AnthropicAdapter = class {
517
+ constructor(config) {
518
+ this.config = config;
519
+ this.client = this.init();
520
+ }
521
+ config;
522
+ client;
523
+ init() {
524
+ try {
525
+ const { default: Anthropic } = __require("@anthropic-ai/sdk");
526
+ return new Anthropic({
527
+ apiKey: this.config.apiKey ?? process.env.ANTHROPIC_API_KEY,
528
+ baseURL: this.config.baseURL,
529
+ timeout: this.config.timeout
530
+ });
531
+ } catch {
532
+ throw new SvaraLLMError(
533
+ "anthropic",
534
+ "Package not found. Run: npm install @anthropic-ai/sdk"
535
+ );
536
+ }
537
+ }
538
+ async chat(messages, tools, temperature) {
539
+ const client = this.client;
540
+ const systemMsg = messages.find((m) => m.role === "system")?.content;
541
+ const chatMsgs = messages.filter((m) => m.role !== "system").map(toAnthropicMessage);
542
+ const response = await client.messages.create({
543
+ model: this.config.model,
544
+ system: systemMsg,
545
+ messages: chatMsgs,
546
+ tools: tools?.length ? tools.map(toAnthropicTool) : void 0,
547
+ max_tokens: this.config.maxTokens ?? 4096,
548
+ temperature: temperature ?? this.config.temperature ?? 0.7
549
+ });
550
+ const textParts = response.content.filter((c) => c.type === "text");
551
+ const toolParts = response.content.filter((c) => c.type === "tool_use");
552
+ const toolCalls = toolParts.map((c) => ({
553
+ id: c.id,
554
+ name: c.name,
555
+ arguments: c.input
556
+ }));
557
+ return {
558
+ content: textParts.map((c) => c.text).join(""),
559
+ toolCalls: toolCalls.length ? toolCalls : void 0,
560
+ usage: {
561
+ promptTokens: response.usage.input_tokens,
562
+ completionTokens: response.usage.output_tokens,
563
+ totalTokens: response.usage.input_tokens + response.usage.output_tokens
564
+ },
565
+ model: response.model,
566
+ finishReason: response.stop_reason === "tool_use" ? "tool_calls" : "stop"
567
+ };
568
+ }
569
+ countTokens(text) {
570
+ return Math.ceil(text.length / 4);
571
+ }
572
+ };
573
+ var OllamaAdapter = class {
574
+ constructor(config) {
575
+ this.config = config;
576
+ this.baseURL = config.baseURL ?? "http://localhost:11434";
577
+ }
578
+ config;
579
+ baseURL;
580
+ async chat(messages, _tools, temperature) {
581
+ const response = await fetch(`${this.baseURL}/api/chat`, {
582
+ method: "POST",
583
+ headers: { "Content-Type": "application/json" },
584
+ body: JSON.stringify({
585
+ model: this.config.model,
586
+ messages: messages.map((m) => ({ role: m.role, content: m.content })),
587
+ options: { temperature: temperature ?? this.config.temperature ?? 0.7 },
588
+ stream: false
589
+ }),
590
+ signal: AbortSignal.timeout(this.config.timeout ?? 6e4)
591
+ });
592
+ if (!response.ok) {
593
+ throw new SvaraLLMError("ollama", `Request failed: ${response.statusText}. Is Ollama running?`);
594
+ }
595
+ const data = await response.json();
596
+ return {
597
+ content: data.message.content,
598
+ usage: {
599
+ promptTokens: data.prompt_eval_count ?? 0,
600
+ completionTokens: data.eval_count ?? 0,
601
+ totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0)
602
+ },
603
+ model: data.model ?? this.config.model,
604
+ finishReason: "stop"
605
+ };
606
+ }
607
+ countTokens(text) {
608
+ return Math.ceil(text.length / 4);
609
+ }
610
+ };
611
+ var GroqAdapter = class extends OpenAIAdapter {
612
+ constructor(config) {
613
+ super({
614
+ ...config,
615
+ baseURL: config.baseURL ?? "https://api.groq.com/openai/v1",
616
+ apiKey: config.apiKey ?? process.env.GROQ_API_KEY
617
+ });
618
+ }
619
+ };
620
+ function createAdapter(config) {
621
+ switch (config.provider) {
622
+ case "openai":
623
+ return new OpenAIAdapter(config);
624
+ case "anthropic":
625
+ return new AnthropicAdapter(config);
626
+ case "ollama":
627
+ return new OllamaAdapter(config);
628
+ case "groq":
629
+ return new GroqAdapter(config);
630
+ default:
631
+ throw new Error(
632
+ `[@yesvara/svara] Unknown LLM provider: "${config.provider}".
633
+ Auto-supported: openai, anthropic, ollama, groq`
634
+ );
635
+ }
636
+ }
637
+ function toOpenAIMessage(msg) {
638
+ if (msg.role === "tool") {
639
+ return { role: "tool", tool_call_id: msg.toolCallId, content: msg.content };
640
+ }
641
+ if (msg.toolCalls?.length) {
642
+ return {
643
+ role: "assistant",
644
+ content: msg.content || null,
645
+ tool_calls: msg.toolCalls.map((tc) => ({
646
+ id: tc.id,
647
+ type: "function",
648
+ function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
649
+ }))
650
+ };
651
+ }
652
+ return { role: msg.role, content: msg.content };
653
+ }
654
+ function toOpenAITool(tool) {
655
+ return {
656
+ type: "function",
657
+ function: {
658
+ name: tool.name,
659
+ description: tool.description,
660
+ parameters: {
661
+ type: "object",
662
+ properties: Object.fromEntries(
663
+ Object.entries(tool.parameters).map(([k, p]) => [
664
+ k,
665
+ { type: p.type, description: p.description, enum: p.enum }
666
+ ])
667
+ ),
668
+ required: Object.entries(tool.parameters).filter(([, p]) => p.required).map(([k]) => k)
669
+ }
670
+ }
671
+ };
672
+ }
673
+ function toAnthropicMessage(msg) {
674
+ if (msg.role === "tool") {
675
+ return {
676
+ role: "user",
677
+ content: [{ type: "tool_result", tool_use_id: msg.toolCallId, content: msg.content }]
678
+ };
679
+ }
680
+ if (msg.toolCalls?.length) {
681
+ return {
682
+ role: "assistant",
683
+ content: [
684
+ ...msg.content ? [{ type: "text", text: msg.content }] : [],
685
+ ...msg.toolCalls.map((tc) => ({
686
+ type: "tool_use",
687
+ id: tc.id,
688
+ name: tc.name,
689
+ input: tc.arguments
690
+ }))
691
+ ]
692
+ };
693
+ }
694
+ return { role: msg.role, content: msg.content };
695
+ }
696
+ function toAnthropicTool(tool) {
697
+ return {
698
+ name: tool.name,
699
+ description: tool.description,
700
+ input_schema: {
701
+ type: "object",
702
+ properties: Object.fromEntries(
703
+ Object.entries(tool.parameters).map(([k, p]) => [
704
+ k,
705
+ { type: p.type, description: p.description, enum: p.enum }
706
+ ])
707
+ ),
708
+ required: Object.entries(tool.parameters).filter(([, p]) => p.required).map(([k]) => k)
709
+ }
710
+ };
711
+ }
712
+ var SvaraLLMError = class extends Error {
713
+ constructor(provider, message) {
714
+ super(`[@yesvara/svara] LLM error (${provider}): ${message}`);
715
+ this.provider = provider;
716
+ this.name = "SvaraLLMError";
717
+ }
718
+ provider;
719
+ };
720
+ function safeParseJSON(str) {
721
+ try {
722
+ return JSON.parse(str);
723
+ } catch {
724
+ return {};
725
+ }
726
+ }
727
+
728
+ // src/memory/conversation.ts
729
+ var ConversationMemory = class {
730
+ constructor(config) {
731
+ this.config = config;
732
+ }
733
+ config;
734
+ sessions = /* @__PURE__ */ new Map();
735
+ async getHistory(sessionId) {
736
+ return this.sessions.get(sessionId)?.messages ?? [];
737
+ }
738
+ async append(sessionId, messages) {
739
+ if (this.config.type === "none") return;
740
+ const store = this.sessions.get(sessionId) ?? {
741
+ messages: [],
742
+ createdAt: /* @__PURE__ */ new Date(),
743
+ updatedAt: /* @__PURE__ */ new Date()
744
+ };
745
+ store.messages.push(...messages);
746
+ store.updatedAt = /* @__PURE__ */ new Date();
747
+ if (store.messages.length > this.config.maxMessages) {
748
+ const system = store.messages.filter((m) => m.role === "system");
749
+ const rest = store.messages.filter((m) => m.role !== "system");
750
+ store.messages = [...system, ...rest.slice(-this.config.maxMessages)];
751
+ }
752
+ this.sessions.set(sessionId, store);
753
+ }
754
+ async clear(sessionId) {
755
+ this.sessions.delete(sessionId);
756
+ }
757
+ getSessionIds() {
758
+ return [...this.sessions.keys()];
759
+ }
760
+ };
761
+
762
+ // src/memory/context.ts
763
+ var ContextBuilder = class {
764
+ constructor(llm) {
765
+ this.llm = llm;
766
+ }
767
+ llm;
768
+ buildMessages(systemPrompt, history, userMessage, ragContext) {
769
+ const messages = [
770
+ { role: "system", content: systemPrompt },
771
+ // Exclude any system messages from history — we prepend our own
772
+ ...history.filter((m) => m.role !== "system")
773
+ ];
774
+ const content = ragContext ? this.augmentWithRAG(userMessage, ragContext) : userMessage;
775
+ messages.push({ role: "user", content });
776
+ return messages;
777
+ }
778
+ estimateTokens(messages) {
779
+ const text = messages.map((m) => m.content).join(" ");
780
+ return this.llm.countTokens(text);
781
+ }
782
+ augmentWithRAG(message, context) {
783
+ return [
784
+ "Use the following context to answer the question.",
785
+ "If the answer isn't in the context, say so honestly \u2014 don't guess.",
786
+ "",
787
+ "--- Context ---",
788
+ context,
789
+ "--- End Context ---",
790
+ "",
791
+ `Question: ${message}`
792
+ ].join("\n");
793
+ }
794
+ };
795
+
796
+ // src/tools/registry.ts
797
+ var ToolRegistry = class {
798
+ tools = /* @__PURE__ */ new Map();
799
+ register(tool) {
800
+ if (this.tools.has(tool.name)) {
801
+ throw new Error(
802
+ `[@yesvara/svara] Tool "${tool.name}" is already registered. Use a different name or call registry.update() to replace it.`
803
+ );
804
+ }
805
+ this.tools.set(tool.name, tool);
806
+ }
807
+ update(tool) {
808
+ this.tools.set(tool.name, tool);
809
+ }
810
+ unregister(name) {
811
+ this.tools.delete(name);
812
+ }
813
+ get(name) {
814
+ return this.tools.get(name);
815
+ }
816
+ getAll() {
817
+ return [...this.tools.values()];
818
+ }
819
+ has(name) {
820
+ return this.tools.has(name);
821
+ }
822
+ get size() {
823
+ return this.tools.size;
824
+ }
825
+ };
826
+
827
+ // src/tools/executor.ts
828
+ var ToolExecutor = class {
829
+ constructor(registry) {
830
+ this.registry = registry;
831
+ }
832
+ registry;
833
+ async execute(call, ctx) {
834
+ const start = Date.now();
835
+ const tool = this.registry.get(call.name);
836
+ if (!tool) {
837
+ return {
838
+ toolCallId: call.id,
839
+ name: call.name,
840
+ result: null,
841
+ error: `Tool "${call.name}" is not registered. Available: ${this.registry.getAll().map((t) => t.name).join(", ")}`,
842
+ duration: Date.now() - start
843
+ };
844
+ }
845
+ try {
846
+ const result = await Promise.race([
847
+ tool.run(call.arguments, ctx),
848
+ this.timeout(tool.timeout ?? 3e4, tool.name)
849
+ ]);
850
+ return { toolCallId: call.id, name: call.name, result, duration: Date.now() - start };
851
+ } catch (err) {
852
+ const message = err.message;
853
+ console.error(`[@yesvara/svara] Tool "${call.name}" failed: ${message}`);
854
+ return {
855
+ toolCallId: call.id,
856
+ name: call.name,
857
+ result: null,
858
+ error: message,
859
+ duration: Date.now() - start
860
+ };
861
+ }
862
+ }
863
+ async executeAll(calls, ctx) {
864
+ return Promise.all(calls.map((c) => this.execute(c, ctx)));
865
+ }
866
+ timeout(ms, name) {
867
+ return new Promise(
868
+ (_, reject) => setTimeout(() => reject(new Error(`Tool "${name}" timed out after ${ms}ms`)), ms)
869
+ );
870
+ }
871
+ };
872
+
873
+ // src/core/agent.ts
874
+ var SvaraAgent = class extends EventEmitter {
875
+ name;
876
+ llmConfig;
877
+ llm;
878
+ systemPrompt;
879
+ tools;
880
+ executor;
881
+ memory;
882
+ context;
883
+ maxIterations;
884
+ verbose;
885
+ channels = /* @__PURE__ */ new Map();
886
+ knowledgeBase = null;
887
+ knowledgePaths = [];
888
+ isStarted = false;
889
+ constructor(config) {
890
+ super();
891
+ this.name = config.name;
892
+ this.maxIterations = config.maxIterations ?? 10;
893
+ this.verbose = config.verbose ?? false;
894
+ this.systemPrompt = config.systemPrompt ?? `You are ${config.name}, a helpful and friendly AI assistant. Be concise and accurate.`;
895
+ this.llmConfig = resolveConfig(config.model, {
896
+ temperature: config.temperature,
897
+ maxTokens: config.maxTokens,
898
+ ...config.llm
899
+ });
900
+ this.llm = createAdapter(this.llmConfig);
901
+ const memCfg = config.memory ?? true;
902
+ const window = memCfg === false ? 0 : typeof memCfg === "object" ? memCfg.window ?? 20 : 20;
903
+ this.memory = new ConversationMemory({ type: "conversation", maxMessages: window });
904
+ this.context = new ContextBuilder(this.llm);
905
+ this.tools = new ToolRegistry();
906
+ this.executor = new ToolExecutor(this.tools);
907
+ config.tools?.forEach((t) => this.addTool(t));
908
+ if (config.knowledge) {
909
+ this.knowledgePaths = Array.isArray(config.knowledge) ? config.knowledge : [config.knowledge];
910
+ }
911
+ }
912
+ // ─── Public API ────────────────────────────────────────────────────────────
913
+ /**
914
+ * Send a message and get a reply. The simplest way to use an agent.
915
+ *
916
+ * @example
917
+ * const reply = await agent.chat('What is the weather in Tokyo?');
918
+ * console.log(reply); // "Currently 28°C and sunny in Tokyo."
919
+ *
920
+ * @param message The user's message.
921
+ * @param sessionId Optional session ID for multi-turn conversations.
922
+ * Defaults to 'default' — all calls share one history.
923
+ */
924
+ async chat(message, sessionId = "default") {
925
+ const result = await this.run(message, { sessionId });
926
+ return result.response;
927
+ }
928
+ /**
929
+ * Process a message and get the full result with metadata.
930
+ * Use this when you need usage stats, tool info, or session details.
931
+ *
932
+ * @example
933
+ * const result = await agent.process('Summarize my report', {
934
+ * sessionId: 'user-42',
935
+ * userId: 'alice@example.com',
936
+ * });
937
+ * console.log(result.response); // The agent's reply
938
+ * console.log(result.toolsUsed); // ['read_file', 'summarize']
939
+ * console.log(result.usage); // { totalTokens: 1234, ... }
940
+ */
941
+ async process(message, options) {
942
+ return this.run(message, options ?? {});
943
+ }
944
+ /**
945
+ * Register a tool the agent can call during a conversation.
946
+ * Returns `this` for chaining.
947
+ *
948
+ * @example
949
+ * agent
950
+ * .addTool(weatherTool)
951
+ * .addTool(emailTool)
952
+ * .addTool(databaseTool);
953
+ */
954
+ addTool(tool) {
955
+ const internal = {
956
+ name: tool.name,
957
+ description: tool.description,
958
+ parameters: tool.parameters ?? {},
959
+ run: tool.run,
960
+ category: tool.category,
961
+ timeout: tool.timeout
962
+ };
963
+ this.tools.register(internal);
964
+ return this;
965
+ }
966
+ /**
967
+ * Connect a messaging channel. The agent will receive and respond to
968
+ * messages from this channel automatically.
969
+ *
970
+ * @example
971
+ * agent.connectChannel('telegram', { token: process.env.TG_TOKEN });
972
+ * agent.connectChannel('whatsapp', {
973
+ * token: process.env.WA_TOKEN,
974
+ * phoneId: process.env.WA_PHONE_ID,
975
+ * verifyToken: process.env.WA_VERIFY_TOKEN,
976
+ * });
977
+ */
978
+ connectChannel(name, config) {
979
+ const channel = this.loadChannel(name, config);
980
+ this.channels.set(name, channel);
981
+ return this;
982
+ }
983
+ /**
984
+ * Returns an Express request handler for mounting on any HTTP server.
985
+ * POST body: `{ message: string, sessionId?: string, userId?: string }`
986
+ *
987
+ * @example With SvaraApp
988
+ * app.route('/chat', agent.handler());
989
+ *
990
+ * @example With existing Express app
991
+ * expressApp.post('/api/chat', agent.handler());
992
+ */
993
+ handler() {
994
+ return async (req, res) => {
995
+ const { message, sessionId, userId } = req.body;
996
+ if (!message?.trim()) {
997
+ res.status(400).json({
998
+ error: "Bad Request",
999
+ message: 'Request body must include a non-empty "message" field.'
1000
+ });
1001
+ return;
1002
+ }
1003
+ try {
1004
+ const result = await this.run(message, {
1005
+ sessionId: sessionId ?? req.headers["x-session-id"],
1006
+ userId
1007
+ });
1008
+ res.json({
1009
+ response: result.response,
1010
+ sessionId: result.sessionId,
1011
+ usage: result.usage,
1012
+ toolsUsed: result.toolsUsed
1013
+ });
1014
+ } catch (err) {
1015
+ const error = err;
1016
+ this.log("error", error.message);
1017
+ res.status(500).json({ error: "Internal Server Error", message: error.message });
1018
+ }
1019
+ };
1020
+ }
1021
+ /**
1022
+ * Initialize all channels and knowledge base, then start listening.
1023
+ * Call this once after you've configured the agent.
1024
+ *
1025
+ * @example
1026
+ * agent.connectChannel('web', { port: 3000 });
1027
+ * await agent.start(); // "Web channel running at http://localhost:3000"
1028
+ */
1029
+ async start() {
1030
+ if (this.isStarted) {
1031
+ console.warn(`[@yesvara/svara] ${this.name} is already running.`);
1032
+ return;
1033
+ }
1034
+ if (this.knowledgePaths.length) {
1035
+ await this.initKnowledge(this.knowledgePaths);
1036
+ }
1037
+ for (const [name, channel] of this.channels) {
1038
+ await channel.mount(this);
1039
+ this.log("info", `Channel "${name}" connected.`);
1040
+ this.emit("channel:ready", { channel: name });
1041
+ }
1042
+ this.isStarted = true;
1043
+ if (this.channels.size === 0) {
1044
+ console.warn(
1045
+ `[@yesvara/svara] ${this.name} has no channels configured.
1046
+ Add one: agent.connectChannel('web', { port: 3000 })`
1047
+ );
1048
+ }
1049
+ }
1050
+ /**
1051
+ * Gracefully shut down all channels.
1052
+ */
1053
+ async stop() {
1054
+ for (const [, channel] of this.channels) {
1055
+ await channel.stop();
1056
+ }
1057
+ this.isStarted = false;
1058
+ this.emit("stopped");
1059
+ }
1060
+ /**
1061
+ * Clear conversation history for a session.
1062
+ *
1063
+ * @example
1064
+ * agent.on('user:leave', (userId) => agent.clearHistory(userId));
1065
+ */
1066
+ async clearHistory(sessionId) {
1067
+ await this.memory.clear(sessionId);
1068
+ }
1069
+ /**
1070
+ * Add documents to the knowledge base at runtime (no restart needed).
1071
+ *
1072
+ * @example
1073
+ * agent.addKnowledge('./new-policies.pdf');
1074
+ */
1075
+ async addKnowledge(paths) {
1076
+ const arr = Array.isArray(paths) ? paths : [paths];
1077
+ if (!this.knowledgeBase) {
1078
+ await this.initKnowledge(arr);
1079
+ } else {
1080
+ await this.knowledgeBase.load(arr);
1081
+ }
1082
+ }
1083
+ // ─── Internal: Agentic Loop ───────────────────────────────────────────────
1084
+ /**
1085
+ * Receives a raw incoming message from a channel and processes it.
1086
+ * Called by channel handlers — not typically used directly.
1087
+ */
1088
+ async receive(msg) {
1089
+ return this.run(msg.text, {
1090
+ sessionId: msg.sessionId,
1091
+ userId: msg.userId
1092
+ });
1093
+ }
1094
+ async run(message, options) {
1095
+ const startTime = Date.now();
1096
+ const sessionId = options.sessionId ?? crypto.randomUUID();
1097
+ this.emit("message:received", { message, sessionId, userId: options.userId });
1098
+ const history = await this.memory.getHistory(sessionId);
1099
+ let ragContext = "";
1100
+ if (this.knowledgeBase) {
1101
+ ragContext = await this.knowledgeBase.retrieve(message);
1102
+ }
1103
+ const messages = this.context.buildMessages(
1104
+ this.systemPrompt,
1105
+ history,
1106
+ message,
1107
+ ragContext
1108
+ );
1109
+ const internalCtx = {
1110
+ sessionId,
1111
+ userId: options.userId ?? "unknown",
1112
+ agentName: this.name,
1113
+ history,
1114
+ metadata: options.metadata ?? {}
1115
+ };
1116
+ const toolsUsed = [];
1117
+ const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1118
+ let iterations = 0;
1119
+ let finalResponse = "";
1120
+ while (iterations < this.maxIterations) {
1121
+ iterations++;
1122
+ this.log("debug", `Iteration ${iterations}`);
1123
+ const allTools = this.tools.getAll();
1124
+ const llmResponse = await this.llm.chat(messages, allTools, this.llmConfig.temperature);
1125
+ totalUsage.promptTokens += llmResponse.usage.promptTokens;
1126
+ totalUsage.completionTokens += llmResponse.usage.completionTokens;
1127
+ totalUsage.totalTokens += llmResponse.usage.totalTokens;
1128
+ if (!llmResponse.toolCalls?.length) {
1129
+ finalResponse = llmResponse.content;
1130
+ messages.push({ role: "assistant", content: finalResponse });
1131
+ break;
1132
+ }
1133
+ messages.push({
1134
+ role: "assistant",
1135
+ content: llmResponse.content,
1136
+ toolCalls: llmResponse.toolCalls
1137
+ });
1138
+ this.emit("tool:call", {
1139
+ sessionId,
1140
+ tools: llmResponse.toolCalls.map((tc) => tc.name)
1141
+ });
1142
+ const results = await this.executor.executeAll(llmResponse.toolCalls, internalCtx);
1143
+ for (const result2 of results) {
1144
+ toolsUsed.push(result2.name);
1145
+ const content = result2.error ? `Error executing ${result2.name}: ${result2.error}` : JSON.stringify(result2.result, null, 2);
1146
+ messages.push({
1147
+ role: "tool",
1148
+ content,
1149
+ toolCallId: result2.toolCallId,
1150
+ name: result2.name
1151
+ });
1152
+ this.emit("tool:result", { sessionId, name: result2.name, result: result2.result });
1153
+ }
1154
+ }
1155
+ if (!finalResponse) {
1156
+ finalResponse = `I've reached the reasoning limit for this request. Please try a simpler question.`;
1157
+ }
1158
+ await this.memory.append(sessionId, [
1159
+ { role: "user", content: message },
1160
+ { role: "assistant", content: finalResponse }
1161
+ ]);
1162
+ const result = {
1163
+ response: finalResponse,
1164
+ sessionId,
1165
+ toolsUsed: [...new Set(toolsUsed)],
1166
+ iterations,
1167
+ usage: totalUsage,
1168
+ duration: Date.now() - startTime
1169
+ };
1170
+ this.emit("message:sent", { response: finalResponse, sessionId });
1171
+ return result;
1172
+ }
1173
+ // ─── Private Helpers ──────────────────────────────────────────────────────
1174
+ async initKnowledge(paths) {
1175
+ try {
1176
+ const { glob } = await import("glob");
1177
+ const { VectorRetriever: VectorRetriever2 } = await import("./retriever-4QY667XF.mjs");
1178
+ const retriever = new VectorRetriever2();
1179
+ await retriever.init({ embeddings: { provider: "openai" } });
1180
+ const files = [];
1181
+ for (const pattern of paths) {
1182
+ const matches = await glob(pattern);
1183
+ files.push(...matches);
1184
+ }
1185
+ if (files.length === 0) {
1186
+ console.warn(`[@yesvara/svara] No files found matching: ${paths.join(", ")}`);
1187
+ return;
1188
+ }
1189
+ await retriever.addDocuments(files);
1190
+ this.knowledgeBase = {
1191
+ load: async (p) => {
1192
+ const newFiles = [];
1193
+ for (const pattern of Array.isArray(p) ? p : [p]) {
1194
+ newFiles.push(...await glob(pattern));
1195
+ }
1196
+ await retriever.addDocuments(newFiles);
1197
+ },
1198
+ retrieve: (query, topK) => retriever.retrieve(query, topK)
1199
+ };
1200
+ this.log("info", `Knowledge base loaded: ${files.length} file(s).`);
1201
+ } catch (err) {
1202
+ console.warn(`[@yesvara/svara] Knowledge base init failed: ${err.message}`);
1203
+ }
1204
+ }
1205
+ loadChannel(name, config) {
1206
+ try {
1207
+ switch (name) {
1208
+ case "web": {
1209
+ const { WebChannel: WebChannel2 } = (init_web(), __toCommonJS(web_exports));
1210
+ return new WebChannel2(config);
1211
+ }
1212
+ case "telegram": {
1213
+ const { TelegramChannel: TelegramChannel2 } = (init_telegram(), __toCommonJS(telegram_exports));
1214
+ return new TelegramChannel2(config);
1215
+ }
1216
+ case "whatsapp": {
1217
+ const { WhatsAppChannel: WhatsAppChannel2 } = (init_whatsapp(), __toCommonJS(whatsapp_exports));
1218
+ return new WhatsAppChannel2(config);
1219
+ }
1220
+ default:
1221
+ throw new Error(`Unknown channel: "${name}"`);
1222
+ }
1223
+ } catch (err) {
1224
+ const error = err;
1225
+ if (error.message.startsWith("[@yesvara") || error.message.startsWith("Unknown")) throw error;
1226
+ throw new Error(`[@yesvara/svara] Failed to load channel "${name}": ${error.message}`);
1227
+ }
1228
+ }
1229
+ log(level, msg) {
1230
+ if (level === "error") {
1231
+ console.error(`[@yesvara/svara] ${this.name}: ${msg}`);
1232
+ } else if (this.verbose) {
1233
+ console.log(`[@yesvara/svara] ${this.name}: ${msg}`);
1234
+ }
1235
+ }
1236
+ };
1237
+
1238
+ // src/tools/index.ts
1239
+ function createTool(definition) {
1240
+ if (!definition.name?.trim()) {
1241
+ throw new Error('[@yesvara/svara] createTool: "name" is required.');
1242
+ }
1243
+ if (!definition.description?.trim()) {
1244
+ throw new Error(`[@yesvara/svara] createTool "${definition.name}": "description" is required.`);
1245
+ }
1246
+ if (typeof definition.run !== "function") {
1247
+ throw new Error(`[@yesvara/svara] createTool "${definition.name}": "run" must be a function.`);
1248
+ }
1249
+ if (!/^[a-zA-Z0-9_-]+$/.test(definition.name)) {
1250
+ throw new Error(
1251
+ `[@yesvara/svara] createTool: Invalid tool name "${definition.name}". Use only letters, numbers, underscores, or hyphens.`
1252
+ );
1253
+ }
1254
+ return {
1255
+ parameters: {},
1256
+ ...definition
1257
+ };
1258
+ }
1259
+
1260
+ // src/database/sqlite.ts
1261
+ import path from "path";
1262
+ import fs from "fs";
1263
+
1264
+ // src/database/schema.ts
1265
+ var SCHEMA_VERSION = 1;
1266
+ var CREATE_TABLES_SQL = `
1267
+ -- Schema version tracking
1268
+ CREATE TABLE IF NOT EXISTS svara_meta (
1269
+ key TEXT PRIMARY KEY,
1270
+ value TEXT NOT NULL
1271
+ );
1272
+
1273
+ -- Conversation history persistence
1274
+ CREATE TABLE IF NOT EXISTS svara_messages (
1275
+ id TEXT PRIMARY KEY,
1276
+ session_id TEXT NOT NULL,
1277
+ role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
1278
+ content TEXT NOT NULL,
1279
+ tool_call_id TEXT,
1280
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
1281
+ );
1282
+
1283
+ CREATE INDEX IF NOT EXISTS idx_messages_session
1284
+ ON svara_messages (session_id, created_at);
1285
+
1286
+ -- Session metadata
1287
+ CREATE TABLE IF NOT EXISTS svara_sessions (
1288
+ id TEXT PRIMARY KEY,
1289
+ user_id TEXT,
1290
+ channel TEXT NOT NULL,
1291
+ created_at INTEGER NOT NULL DEFAULT (unixepoch()),
1292
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
1293
+ metadata TEXT DEFAULT '{}'
1294
+ );
1295
+
1296
+ -- Vector store chunks for RAG
1297
+ CREATE TABLE IF NOT EXISTS svara_chunks (
1298
+ id TEXT PRIMARY KEY,
1299
+ document_id TEXT NOT NULL,
1300
+ content TEXT NOT NULL,
1301
+ chunk_index INTEGER NOT NULL,
1302
+ embedding BLOB, -- stored as binary float32 array
1303
+ source TEXT NOT NULL,
1304
+ metadata TEXT DEFAULT '{}',
1305
+ created_at INTEGER NOT NULL DEFAULT (unixepoch())
1306
+ );
1307
+
1308
+ CREATE INDEX IF NOT EXISTS idx_chunks_document
1309
+ ON svara_chunks (document_id);
1310
+
1311
+ -- Document registry
1312
+ CREATE TABLE IF NOT EXISTS svara_documents (
1313
+ id TEXT PRIMARY KEY,
1314
+ source TEXT NOT NULL UNIQUE,
1315
+ type TEXT NOT NULL,
1316
+ size INTEGER,
1317
+ hash TEXT,
1318
+ indexed_at INTEGER NOT NULL DEFAULT (unixepoch()),
1319
+ metadata TEXT DEFAULT '{}'
1320
+ );
1321
+
1322
+ -- Key-value store for arbitrary agent state
1323
+ CREATE TABLE IF NOT EXISTS svara_kv (
1324
+ key TEXT PRIMARY KEY,
1325
+ value TEXT NOT NULL,
1326
+ expires_at INTEGER, -- unix timestamp, NULL = no expiry
1327
+ updated_at INTEGER NOT NULL DEFAULT (unixepoch())
1328
+ );
1329
+ `;
1330
+ var INSERT_META_SQL = `
1331
+ INSERT OR REPLACE INTO svara_meta (key, value)
1332
+ VALUES ('schema_version', ?), ('created_at', ?);
1333
+ `;
1334
+
1335
+ // src/database/sqlite.ts
1336
+ var KVStore = class {
1337
+ constructor(db) {
1338
+ this.db = db;
1339
+ }
1340
+ db;
1341
+ /** Set a key-value pair, with optional TTL in seconds. */
1342
+ set(key, value, ttlSeconds) {
1343
+ const expiresAt = ttlSeconds ? Math.floor(Date.now() / 1e3) + ttlSeconds : null;
1344
+ this.db.prepare(`
1345
+ INSERT OR REPLACE INTO svara_kv (key, value, expires_at, updated_at)
1346
+ VALUES (?, ?, ?, unixepoch())
1347
+ `).run(key, JSON.stringify(value), expiresAt);
1348
+ }
1349
+ /** Get a value by key. Returns undefined if not found or expired. */
1350
+ get(key) {
1351
+ const row = this.db.prepare(`
1352
+ SELECT value, expires_at FROM svara_kv
1353
+ WHERE key = ? AND (expires_at IS NULL OR expires_at > unixepoch())
1354
+ `).get(key);
1355
+ if (!row) return void 0;
1356
+ return JSON.parse(row.value);
1357
+ }
1358
+ /** Delete a key. */
1359
+ delete(key) {
1360
+ this.db.prepare("DELETE FROM svara_kv WHERE key = ?").run(key);
1361
+ }
1362
+ /** Check if a key exists and is not expired. */
1363
+ has(key) {
1364
+ return this.get(key) !== void 0;
1365
+ }
1366
+ /** Get all keys matching a prefix. */
1367
+ keys(prefix = "") {
1368
+ const rows = this.db.prepare(`
1369
+ SELECT key FROM svara_kv
1370
+ WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > unixepoch())
1371
+ `).all(`${prefix}%`);
1372
+ return rows.map((r) => r.key);
1373
+ }
1374
+ };
1375
+ var SvaraDB = class {
1376
+ db;
1377
+ kv;
1378
+ constructor(dbPath = ":memory:") {
1379
+ if (dbPath !== ":memory:") {
1380
+ fs.mkdirSync(path.dirname(path.resolve(dbPath)), { recursive: true });
1381
+ }
1382
+ this.db = this.openDatabase(dbPath);
1383
+ this.configure();
1384
+ this.migrate();
1385
+ this.kv = new KVStore(this.db);
1386
+ }
1387
+ // ─── Query Helpers ────────────────────────────────────────────────────────
1388
+ /**
1389
+ * Run a SELECT and return all matching rows.
1390
+ */
1391
+ query(sql, params = []) {
1392
+ return this.db.prepare(sql).all(...params);
1393
+ }
1394
+ /**
1395
+ * Run a SELECT and return the first matching row.
1396
+ */
1397
+ queryOne(sql, params = []) {
1398
+ return this.db.prepare(sql).get(...params);
1399
+ }
1400
+ /**
1401
+ * Run an INSERT/UPDATE/DELETE. Returns affected row count.
1402
+ */
1403
+ run(sql, params = []) {
1404
+ return this.db.prepare(sql).run(...params).changes;
1405
+ }
1406
+ /**
1407
+ * Execute raw SQL (for DDL, migrations, etc.).
1408
+ */
1409
+ exec(sql) {
1410
+ this.db.exec(sql);
1411
+ }
1412
+ /**
1413
+ * Run multiple operations in a single transaction.
1414
+ *
1415
+ * @example
1416
+ * db.transaction(() => {
1417
+ * db.run('INSERT INTO orders ...', [...]);
1418
+ * db.run('UPDATE inventory ...', [...]);
1419
+ * });
1420
+ */
1421
+ transaction(fn) {
1422
+ return this.db.transaction(fn)();
1423
+ }
1424
+ /**
1425
+ * Close the database connection.
1426
+ */
1427
+ close() {
1428
+ this.db.close();
1429
+ }
1430
+ // ─── Internal Message Storage ─────────────────────────────────────────────
1431
+ saveMessage(params) {
1432
+ this.db.prepare(`
1433
+ INSERT OR REPLACE INTO svara_messages (id, session_id, role, content, tool_call_id)
1434
+ VALUES (?, ?, ?, ?, ?)
1435
+ `).run(
1436
+ params.id,
1437
+ params.sessionId,
1438
+ params.role,
1439
+ params.content,
1440
+ params.toolCallId ?? null
1441
+ );
1442
+ }
1443
+ getMessages(sessionId, limit = 50) {
1444
+ return this.db.prepare(`
1445
+ SELECT id, role, content, tool_call_id, created_at
1446
+ FROM svara_messages
1447
+ WHERE session_id = ?
1448
+ ORDER BY created_at ASC
1449
+ LIMIT ?
1450
+ `).all(sessionId, limit);
1451
+ }
1452
+ clearSession(sessionId) {
1453
+ this.db.prepare("DELETE FROM svara_messages WHERE session_id = ?").run(sessionId);
1454
+ }
1455
+ // ─── Private Setup ────────────────────────────────────────────────────────
1456
+ openDatabase(dbPath) {
1457
+ try {
1458
+ const Database = __require("better-sqlite3");
1459
+ return new Database(dbPath);
1460
+ } catch {
1461
+ throw new Error(
1462
+ '[SvaraJS] Database requires the "better-sqlite3" package.\nRun: npm install better-sqlite3'
1463
+ );
1464
+ }
1465
+ }
1466
+ configure() {
1467
+ this.db.pragma("journal_mode = WAL");
1468
+ this.db.pragma("synchronous = NORMAL");
1469
+ this.db.pragma("foreign_keys = ON");
1470
+ }
1471
+ migrate() {
1472
+ this.db.exec(CREATE_TABLES_SQL);
1473
+ const meta = this.db.prepare(
1474
+ "SELECT value FROM svara_meta WHERE key = 'schema_version'"
1475
+ ).get();
1476
+ if (!meta) {
1477
+ this.db.prepare(INSERT_META_SQL).run(
1478
+ String(SCHEMA_VERSION),
1479
+ (/* @__PURE__ */ new Date()).toISOString()
1480
+ );
1481
+ }
1482
+ }
1483
+ };
1484
+
1485
+ // src/index.ts
1486
+ init_web();
1487
+ init_telegram();
1488
+ init_whatsapp();
1489
+ var VERSION = "0.1.0";
1490
+ export {
1491
+ Chunker,
1492
+ DocumentLoader,
1493
+ SvaraAgent,
1494
+ SvaraApp,
1495
+ SvaraDB,
1496
+ TelegramChannel,
1497
+ VERSION,
1498
+ VectorRetriever,
1499
+ WebChannel,
1500
+ WhatsAppChannel,
1501
+ createTool
1502
+ };