@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.
- package/LICENSE +21 -0
- package/README.md +497 -0
- package/dist/chunk-CIESM3BP.mjs +33 -0
- package/dist/chunk-FEA5KIJN.mjs +418 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +328 -0
- package/dist/cli/index.mjs +39 -0
- package/dist/dev-OYGXXK2B.mjs +69 -0
- package/dist/index.d.mts +967 -0
- package/dist/index.d.ts +967 -0
- package/dist/index.js +1976 -0
- package/dist/index.mjs +1502 -0
- package/dist/new-7K4NIDZO.mjs +177 -0
- package/dist/retriever-4QY667XF.mjs +7 -0
- package/examples/01-basic/index.ts +26 -0
- package/examples/02-with-tools/index.ts +73 -0
- package/examples/03-rag-knowledge/index.ts +41 -0
- package/examples/04-multi-channel/index.ts +91 -0
- package/package.json +74 -0
- package/src/app/index.ts +176 -0
- package/src/channels/telegram.ts +122 -0
- package/src/channels/web.ts +118 -0
- package/src/channels/whatsapp.ts +161 -0
- package/src/cli/commands/dev.ts +87 -0
- package/src/cli/commands/new.ts +213 -0
- package/src/cli/index.ts +78 -0
- package/src/core/agent.ts +607 -0
- package/src/core/llm.ts +406 -0
- package/src/core/types.ts +183 -0
- package/src/database/schema.ts +79 -0
- package/src/database/sqlite.ts +239 -0
- package/src/index.ts +94 -0
- package/src/memory/context.ts +49 -0
- package/src/memory/conversation.ts +51 -0
- package/src/rag/chunker.ts +165 -0
- package/src/rag/loader.ts +216 -0
- package/src/rag/retriever.ts +248 -0
- package/src/tools/executor.ts +54 -0
- package/src/tools/index.ts +89 -0
- package/src/tools/registry.ts +44 -0
- package/src/types.ts +131 -0
- 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
|
+
};
|