@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.js
ADDED
|
@@ -0,0 +1,1976 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
|
+
};
|
|
11
|
+
var __export = (target, all) => {
|
|
12
|
+
for (var name in all)
|
|
13
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
14
|
+
};
|
|
15
|
+
var __copyProps = (to, from, except, desc) => {
|
|
16
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
17
|
+
for (let key of __getOwnPropNames(from))
|
|
18
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
19
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
24
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
25
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
26
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
27
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
28
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
|
+
mod
|
|
30
|
+
));
|
|
31
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
|
+
|
|
33
|
+
// src/rag/loader.ts
|
|
34
|
+
var import_promises, import_path, import_crypto, TextFileLoader, JsonFileLoader, HtmlFileLoader, PdfFileLoader, DocxFileLoader, DocumentLoader;
|
|
35
|
+
var init_loader = __esm({
|
|
36
|
+
"src/rag/loader.ts"() {
|
|
37
|
+
"use strict";
|
|
38
|
+
import_promises = __toESM(require("fs/promises"));
|
|
39
|
+
import_path = __toESM(require("path"));
|
|
40
|
+
import_crypto = __toESM(require("crypto"));
|
|
41
|
+
TextFileLoader = class {
|
|
42
|
+
extensions = [".txt", ".md", ".mdx", ".rst", ".csv", ".log"];
|
|
43
|
+
async load(filePath) {
|
|
44
|
+
return import_promises.default.readFile(filePath, "utf-8");
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
JsonFileLoader = class {
|
|
48
|
+
extensions = [".json", ".jsonl"];
|
|
49
|
+
async load(filePath) {
|
|
50
|
+
const raw = await import_promises.default.readFile(filePath, "utf-8");
|
|
51
|
+
if (import_path.default.extname(filePath) === ".jsonl") {
|
|
52
|
+
return raw.split("\n").filter(Boolean).map((line) => {
|
|
53
|
+
const obj = JSON.parse(line);
|
|
54
|
+
return Object.values(obj).join(" ");
|
|
55
|
+
}).join("\n");
|
|
56
|
+
}
|
|
57
|
+
const data = JSON.parse(raw);
|
|
58
|
+
return JSON.stringify(data, null, 2);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
HtmlFileLoader = class {
|
|
62
|
+
extensions = [".html", ".htm"];
|
|
63
|
+
async load(filePath) {
|
|
64
|
+
const raw = await import_promises.default.readFile(filePath, "utf-8");
|
|
65
|
+
return raw.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
PdfFileLoader = class {
|
|
69
|
+
extensions = [".pdf"];
|
|
70
|
+
async load(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
const pdfParse = require("pdf-parse");
|
|
73
|
+
const buffer = await import_promises.default.readFile(filePath);
|
|
74
|
+
const data = await pdfParse(buffer);
|
|
75
|
+
return data.text;
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(
|
|
78
|
+
'[SvaraJS] PDF loading requires the "pdf-parse" package.\nRun: npm install pdf-parse'
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
DocxFileLoader = class {
|
|
84
|
+
extensions = [".docx"];
|
|
85
|
+
async load(filePath) {
|
|
86
|
+
try {
|
|
87
|
+
const mammoth = require("mammoth");
|
|
88
|
+
const result = await mammoth.extractRawText({ path: filePath });
|
|
89
|
+
return result.value;
|
|
90
|
+
} catch {
|
|
91
|
+
throw new Error(
|
|
92
|
+
'[SvaraJS] DOCX loading requires the "mammoth" package.\nRun: npm install mammoth'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
DocumentLoader = class {
|
|
98
|
+
loaders;
|
|
99
|
+
extensionMap;
|
|
100
|
+
constructor() {
|
|
101
|
+
this.loaders = [
|
|
102
|
+
new TextFileLoader(),
|
|
103
|
+
new JsonFileLoader(),
|
|
104
|
+
new HtmlFileLoader(),
|
|
105
|
+
new PdfFileLoader(),
|
|
106
|
+
new DocxFileLoader()
|
|
107
|
+
];
|
|
108
|
+
this.extensionMap = /* @__PURE__ */ new Map();
|
|
109
|
+
for (const loader of this.loaders) {
|
|
110
|
+
for (const ext of loader.extensions) {
|
|
111
|
+
this.extensionMap.set(ext, loader);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Load a single file into a Document.
|
|
117
|
+
*/
|
|
118
|
+
async load(filePath) {
|
|
119
|
+
const ext = import_path.default.extname(filePath).toLowerCase();
|
|
120
|
+
const loader = this.extensionMap.get(ext);
|
|
121
|
+
if (!loader) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`[SvaraJS] Unsupported file type: "${ext}". Supported: ${[...this.extensionMap.keys()].join(", ")}`
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const content = await loader.load(filePath);
|
|
127
|
+
const stats = await import_promises.default.stat(filePath);
|
|
128
|
+
return {
|
|
129
|
+
id: this.hashFile(filePath),
|
|
130
|
+
content,
|
|
131
|
+
type: this.detectType(ext),
|
|
132
|
+
source: filePath,
|
|
133
|
+
metadata: {
|
|
134
|
+
filename: import_path.default.basename(filePath),
|
|
135
|
+
extension: ext,
|
|
136
|
+
size: stats.size,
|
|
137
|
+
lastModified: stats.mtime.toISOString()
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Load multiple files. Silently skips unreadable files with a warning.
|
|
143
|
+
*/
|
|
144
|
+
async loadMany(filePaths) {
|
|
145
|
+
const results = [];
|
|
146
|
+
for (const filePath of filePaths) {
|
|
147
|
+
try {
|
|
148
|
+
const doc = await this.load(filePath);
|
|
149
|
+
results.push(doc);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
console.warn(`[SvaraJS:RAG] Skipping "${filePath}": ${err.message}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return results;
|
|
155
|
+
}
|
|
156
|
+
/** Check if this loader supports a given file extension. */
|
|
157
|
+
supports(filePath) {
|
|
158
|
+
const ext = import_path.default.extname(filePath).toLowerCase();
|
|
159
|
+
return this.extensionMap.has(ext);
|
|
160
|
+
}
|
|
161
|
+
detectType(ext) {
|
|
162
|
+
const map = {
|
|
163
|
+
".txt": "text",
|
|
164
|
+
".md": "markdown",
|
|
165
|
+
".mdx": "markdown",
|
|
166
|
+
".pdf": "pdf",
|
|
167
|
+
".html": "html",
|
|
168
|
+
".htm": "html",
|
|
169
|
+
".json": "json",
|
|
170
|
+
".jsonl": "json",
|
|
171
|
+
".docx": "docx"
|
|
172
|
+
};
|
|
173
|
+
return map[ext] ?? "text";
|
|
174
|
+
}
|
|
175
|
+
hashFile(filePath) {
|
|
176
|
+
return import_crypto.default.createHash("md5").update(filePath).digest("hex");
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// src/rag/chunker.ts
|
|
183
|
+
var import_crypto2, Chunker;
|
|
184
|
+
var init_chunker = __esm({
|
|
185
|
+
"src/rag/chunker.ts"() {
|
|
186
|
+
"use strict";
|
|
187
|
+
import_crypto2 = __toESM(require("crypto"));
|
|
188
|
+
Chunker = class {
|
|
189
|
+
options;
|
|
190
|
+
constructor(options = {}) {
|
|
191
|
+
this.options = {
|
|
192
|
+
strategy: options.strategy ?? "sentence",
|
|
193
|
+
size: options.size ?? 2e3,
|
|
194
|
+
overlap: options.overlap ?? 200
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Split a document into overlapping chunks.
|
|
199
|
+
* Returns the document with populated `chunks` field.
|
|
200
|
+
*/
|
|
201
|
+
chunk(document) {
|
|
202
|
+
const text = document.content.trim();
|
|
203
|
+
if (!text) return [];
|
|
204
|
+
let texts;
|
|
205
|
+
switch (this.options.strategy) {
|
|
206
|
+
case "fixed":
|
|
207
|
+
texts = this.fixedChunk(text);
|
|
208
|
+
break;
|
|
209
|
+
case "paragraph":
|
|
210
|
+
texts = this.paragraphChunk(text);
|
|
211
|
+
break;
|
|
212
|
+
case "sentence":
|
|
213
|
+
default:
|
|
214
|
+
texts = this.sentenceChunk(text);
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
return texts.filter((t) => t.trim().length > 0).map((content, index) => ({
|
|
218
|
+
id: this.chunkId(document.id, index),
|
|
219
|
+
documentId: document.id,
|
|
220
|
+
content: content.trim(),
|
|
221
|
+
index,
|
|
222
|
+
metadata: {
|
|
223
|
+
...document.metadata,
|
|
224
|
+
chunkIndex: index,
|
|
225
|
+
strategy: this.options.strategy,
|
|
226
|
+
charCount: content.length
|
|
227
|
+
}
|
|
228
|
+
}));
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* Chunk multiple documents at once.
|
|
232
|
+
*/
|
|
233
|
+
chunkMany(documents) {
|
|
234
|
+
return documents.flatMap((doc) => this.chunk(doc));
|
|
235
|
+
}
|
|
236
|
+
// ─── Strategies ───────────────────────────────────────────────────────────
|
|
237
|
+
/** Split into fixed-size windows with overlap. Good for code and structured data. */
|
|
238
|
+
fixedChunk(text) {
|
|
239
|
+
const { size, overlap } = this.options;
|
|
240
|
+
const chunks = [];
|
|
241
|
+
let start = 0;
|
|
242
|
+
while (start < text.length) {
|
|
243
|
+
const end = Math.min(start + size, text.length);
|
|
244
|
+
chunks.push(text.slice(start, end));
|
|
245
|
+
start += size - overlap;
|
|
246
|
+
}
|
|
247
|
+
return chunks;
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Split by sentences, grouping them until size limit.
|
|
251
|
+
* Best for prose text — preserves natural reading units.
|
|
252
|
+
*/
|
|
253
|
+
sentenceChunk(text) {
|
|
254
|
+
const sentences = this.splitSentences(text);
|
|
255
|
+
return this.groupBySize(sentences);
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Split by paragraphs (double newline), grouping small ones.
|
|
259
|
+
* Best for documentation, articles, and manuals.
|
|
260
|
+
*/
|
|
261
|
+
paragraphChunk(text) {
|
|
262
|
+
const paragraphs = text.split(/\n{2,}/).map((p) => p.trim()).filter(Boolean);
|
|
263
|
+
return this.groupBySize(paragraphs);
|
|
264
|
+
}
|
|
265
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────
|
|
266
|
+
splitSentences(text) {
|
|
267
|
+
return text.split(/(?<=[.!?])\s+(?=[A-Z"'(])/).map((s) => s.trim()).filter(Boolean);
|
|
268
|
+
}
|
|
269
|
+
groupBySize(units) {
|
|
270
|
+
const { size, overlap } = this.options;
|
|
271
|
+
const chunks = [];
|
|
272
|
+
let current = "";
|
|
273
|
+
let overlapBuffer = "";
|
|
274
|
+
for (const unit of units) {
|
|
275
|
+
if (current.length + unit.length + 1 > size && current.length > 0) {
|
|
276
|
+
chunks.push(current);
|
|
277
|
+
current = overlapBuffer + (overlapBuffer ? " " : "") + unit;
|
|
278
|
+
overlapBuffer = "";
|
|
279
|
+
} else {
|
|
280
|
+
current += (current ? " " : "") + unit;
|
|
281
|
+
}
|
|
282
|
+
if (current.length > overlap) {
|
|
283
|
+
overlapBuffer = current.slice(-overlap);
|
|
284
|
+
} else {
|
|
285
|
+
overlapBuffer = current;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (current.trim()) chunks.push(current);
|
|
289
|
+
return chunks;
|
|
290
|
+
}
|
|
291
|
+
chunkId(documentId, index) {
|
|
292
|
+
return import_crypto2.default.createHash("md5").update(`${documentId}:${index}`).digest("hex");
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// src/rag/retriever.ts
|
|
299
|
+
var retriever_exports = {};
|
|
300
|
+
__export(retriever_exports, {
|
|
301
|
+
VectorRetriever: () => VectorRetriever
|
|
302
|
+
});
|
|
303
|
+
function cosineSimilarity(a, b) {
|
|
304
|
+
if (a.length !== b.length) return 0;
|
|
305
|
+
let dot = 0;
|
|
306
|
+
let normA = 0;
|
|
307
|
+
let normB = 0;
|
|
308
|
+
for (let i = 0; i < a.length; i++) {
|
|
309
|
+
dot += a[i] * b[i];
|
|
310
|
+
normA += a[i] * a[i];
|
|
311
|
+
normB += b[i] * b[i];
|
|
312
|
+
}
|
|
313
|
+
const denominator = Math.sqrt(normA) * Math.sqrt(normB);
|
|
314
|
+
return denominator === 0 ? 0 : dot / denominator;
|
|
315
|
+
}
|
|
316
|
+
var OpenAIEmbeddings, OllamaEmbeddings, InMemoryVectorStore, VectorRetriever;
|
|
317
|
+
var init_retriever = __esm({
|
|
318
|
+
"src/rag/retriever.ts"() {
|
|
319
|
+
"use strict";
|
|
320
|
+
init_loader();
|
|
321
|
+
init_chunker();
|
|
322
|
+
OpenAIEmbeddings = class {
|
|
323
|
+
client;
|
|
324
|
+
model;
|
|
325
|
+
constructor(apiKey, model = "text-embedding-3-small") {
|
|
326
|
+
this.model = model;
|
|
327
|
+
try {
|
|
328
|
+
const { default: OpenAI } = require("openai");
|
|
329
|
+
this.client = new OpenAI({ apiKey: apiKey ?? process.env.OPENAI_API_KEY });
|
|
330
|
+
} catch {
|
|
331
|
+
throw new Error('[SvaraJS] OpenAI embeddings require the "openai" package.');
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async embed(texts) {
|
|
335
|
+
const client = this.client;
|
|
336
|
+
const BATCH_SIZE = 100;
|
|
337
|
+
const results = [];
|
|
338
|
+
for (let i = 0; i < texts.length; i += BATCH_SIZE) {
|
|
339
|
+
const batch = texts.slice(i, i + BATCH_SIZE);
|
|
340
|
+
const response = await client.embeddings.create({
|
|
341
|
+
model: this.model,
|
|
342
|
+
input: batch
|
|
343
|
+
});
|
|
344
|
+
results.push(...response.data.map((d) => d.embedding));
|
|
345
|
+
}
|
|
346
|
+
return results;
|
|
347
|
+
}
|
|
348
|
+
async embedOne(text) {
|
|
349
|
+
const [embedding] = await this.embed([text]);
|
|
350
|
+
return embedding;
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
OllamaEmbeddings = class {
|
|
354
|
+
baseURL;
|
|
355
|
+
model;
|
|
356
|
+
constructor(model = "nomic-embed-text", baseURL = "http://localhost:11434") {
|
|
357
|
+
this.model = model;
|
|
358
|
+
this.baseURL = baseURL;
|
|
359
|
+
}
|
|
360
|
+
async embed(texts) {
|
|
361
|
+
return Promise.all(texts.map((t) => this.embedOne(t)));
|
|
362
|
+
}
|
|
363
|
+
async embedOne(text) {
|
|
364
|
+
const response = await fetch(`${this.baseURL}/api/embeddings`, {
|
|
365
|
+
method: "POST",
|
|
366
|
+
headers: { "Content-Type": "application/json" },
|
|
367
|
+
body: JSON.stringify({ model: this.model, prompt: text })
|
|
368
|
+
});
|
|
369
|
+
if (!response.ok) {
|
|
370
|
+
throw new Error(`[SvaraJS] Ollama embeddings failed: ${response.statusText}`);
|
|
371
|
+
}
|
|
372
|
+
const data = await response.json();
|
|
373
|
+
return data.embedding;
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
InMemoryVectorStore = class {
|
|
377
|
+
entries = [];
|
|
378
|
+
add(chunk, embedding) {
|
|
379
|
+
const existing = this.entries.findIndex((e) => e.chunk.id === chunk.id);
|
|
380
|
+
if (existing >= 0) {
|
|
381
|
+
this.entries[existing] = { chunk, embedding };
|
|
382
|
+
} else {
|
|
383
|
+
this.entries.push({ chunk, embedding });
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
search(queryEmbedding, topK, threshold = 0) {
|
|
387
|
+
const scored = this.entries.map((entry) => ({
|
|
388
|
+
chunk: entry.chunk,
|
|
389
|
+
score: cosineSimilarity(queryEmbedding, entry.embedding)
|
|
390
|
+
}));
|
|
391
|
+
return scored.filter((s) => s.score >= threshold).sort((a, b) => b.score - a.score).slice(0, topK).map((s) => s.chunk);
|
|
392
|
+
}
|
|
393
|
+
get size() {
|
|
394
|
+
return this.entries.length;
|
|
395
|
+
}
|
|
396
|
+
};
|
|
397
|
+
VectorRetriever = class {
|
|
398
|
+
embedder;
|
|
399
|
+
store;
|
|
400
|
+
loader;
|
|
401
|
+
chunker;
|
|
402
|
+
config;
|
|
403
|
+
constructor() {
|
|
404
|
+
this.store = new InMemoryVectorStore();
|
|
405
|
+
this.loader = new DocumentLoader();
|
|
406
|
+
this.chunker = new Chunker();
|
|
407
|
+
}
|
|
408
|
+
async init(config) {
|
|
409
|
+
this.config = config;
|
|
410
|
+
if (config.chunking) {
|
|
411
|
+
this.chunker = new Chunker({
|
|
412
|
+
strategy: config.chunking.strategy ?? "sentence",
|
|
413
|
+
size: config.chunking.size ? config.chunking.size * 4 : 2e3,
|
|
414
|
+
// rough token→char
|
|
415
|
+
overlap: config.chunking.overlap ? config.chunking.overlap * 4 : 200
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
const emb = config.embeddings ?? { provider: "openai" };
|
|
419
|
+
switch (emb.provider) {
|
|
420
|
+
case "openai":
|
|
421
|
+
this.embedder = new OpenAIEmbeddings(emb.apiKey, emb.model);
|
|
422
|
+
break;
|
|
423
|
+
case "ollama":
|
|
424
|
+
this.embedder = new OllamaEmbeddings(emb.model);
|
|
425
|
+
break;
|
|
426
|
+
default:
|
|
427
|
+
throw new Error(`[SvaraJS] Unknown embeddings provider: "${emb.provider}"`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
async addDocuments(filePaths) {
|
|
431
|
+
const documents = await this.loader.loadMany(filePaths);
|
|
432
|
+
if (!documents.length) return;
|
|
433
|
+
const chunks = this.chunker.chunkMany(documents);
|
|
434
|
+
if (!chunks.length) return;
|
|
435
|
+
console.log(`[SvaraJS:RAG] Embedding ${chunks.length} chunk(s)...`);
|
|
436
|
+
const embeddings = await this.embedder.embed(chunks.map((c) => c.content));
|
|
437
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
438
|
+
this.store.add(chunks[i], embeddings[i]);
|
|
439
|
+
}
|
|
440
|
+
console.log(`[SvaraJS:RAG] Vector store now has ${this.store.size} chunk(s).`);
|
|
441
|
+
}
|
|
442
|
+
async retrieve(query, topK = 5) {
|
|
443
|
+
if (this.store.size === 0) return "";
|
|
444
|
+
const queryEmbedding = await this.embedder.embedOne(query);
|
|
445
|
+
const threshold = this.config.retrieval?.threshold ?? 0.3;
|
|
446
|
+
const chunks = this.store.search(queryEmbedding, topK, threshold);
|
|
447
|
+
if (!chunks.length) return "";
|
|
448
|
+
return chunks.map((chunk, i) => `[Context ${i + 1}]
|
|
449
|
+
Source: ${String(chunk.metadata.filename ?? chunk.documentId)}
|
|
450
|
+
${chunk.content}`).join("\n\n---\n\n");
|
|
451
|
+
}
|
|
452
|
+
async retrieveChunks(query, topK = 5) {
|
|
453
|
+
const queryEmbedding = await this.embedder.embedOne(query);
|
|
454
|
+
const threshold = this.config.retrieval?.threshold ?? 0.3;
|
|
455
|
+
const chunks = this.store.search(queryEmbedding, topK, threshold);
|
|
456
|
+
return {
|
|
457
|
+
chunks,
|
|
458
|
+
query,
|
|
459
|
+
totalFound: chunks.length
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
// src/channels/web.ts
|
|
467
|
+
var web_exports = {};
|
|
468
|
+
__export(web_exports, {
|
|
469
|
+
WebChannel: () => WebChannel
|
|
470
|
+
});
|
|
471
|
+
var import_express2, WebChannel;
|
|
472
|
+
var init_web = __esm({
|
|
473
|
+
"src/channels/web.ts"() {
|
|
474
|
+
"use strict";
|
|
475
|
+
import_express2 = __toESM(require("express"));
|
|
476
|
+
WebChannel = class {
|
|
477
|
+
constructor(config = {}) {
|
|
478
|
+
this.config = config;
|
|
479
|
+
this.app = this.buildApp();
|
|
480
|
+
}
|
|
481
|
+
config;
|
|
482
|
+
name = "web";
|
|
483
|
+
app;
|
|
484
|
+
server = null;
|
|
485
|
+
agent;
|
|
486
|
+
async mount(agent) {
|
|
487
|
+
this.agent = agent;
|
|
488
|
+
this.attachRoutes();
|
|
489
|
+
const port = this.config.port ?? 3e3;
|
|
490
|
+
return new Promise((resolve, reject) => {
|
|
491
|
+
this.server = this.app.listen(port, () => {
|
|
492
|
+
console.log(`[@yesvara/svara] Web channel running at http://localhost:${port}`);
|
|
493
|
+
resolve();
|
|
494
|
+
});
|
|
495
|
+
this.server.on("error", reject);
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
async send(_sessionId, _text) {
|
|
499
|
+
}
|
|
500
|
+
async stop() {
|
|
501
|
+
return new Promise((resolve) => {
|
|
502
|
+
this.server?.close(() => resolve()) ?? resolve();
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
// ─── Private ───────────────────────────────────────────────────────────────
|
|
506
|
+
buildApp() {
|
|
507
|
+
const app = (0, import_express2.default)();
|
|
508
|
+
app.use(import_express2.default.json({ limit: "10mb" }));
|
|
509
|
+
if (this.config.cors) {
|
|
510
|
+
const origin = this.config.cors === true ? "*" : this.config.cors;
|
|
511
|
+
app.use((_req, res, next) => {
|
|
512
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
513
|
+
res.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS");
|
|
514
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
515
|
+
next();
|
|
516
|
+
});
|
|
517
|
+
app.options("*", (_req, res) => res.sendStatus(204));
|
|
518
|
+
}
|
|
519
|
+
if (this.config.apiKey) {
|
|
520
|
+
app.use((req, res, next) => {
|
|
521
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
522
|
+
if (token !== this.config.apiKey) {
|
|
523
|
+
res.status(401).json({ error: "Unauthorized" });
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
next();
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
return app;
|
|
530
|
+
}
|
|
531
|
+
attachRoutes() {
|
|
532
|
+
const base = this.config.path ?? "";
|
|
533
|
+
this.app.get(`${base}/health`, (_req, res) => {
|
|
534
|
+
res.json({ status: "ok", agent: this.agent.name, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
|
|
535
|
+
});
|
|
536
|
+
this.app.post(`${base}/chat`, this.agent.handler());
|
|
537
|
+
}
|
|
538
|
+
buildMessage(body) {
|
|
539
|
+
return {
|
|
540
|
+
id: crypto.randomUUID(),
|
|
541
|
+
sessionId: body.sessionId ?? crypto.randomUUID(),
|
|
542
|
+
userId: body.userId ?? "web-user",
|
|
543
|
+
channel: "web",
|
|
544
|
+
text: body.message,
|
|
545
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
// src/channels/telegram.ts
|
|
553
|
+
var telegram_exports = {};
|
|
554
|
+
__export(telegram_exports, {
|
|
555
|
+
TelegramChannel: () => TelegramChannel
|
|
556
|
+
});
|
|
557
|
+
var TelegramChannel;
|
|
558
|
+
var init_telegram = __esm({
|
|
559
|
+
"src/channels/telegram.ts"() {
|
|
560
|
+
"use strict";
|
|
561
|
+
TelegramChannel = class {
|
|
562
|
+
constructor(config) {
|
|
563
|
+
this.config = config;
|
|
564
|
+
if (!config.token) throw new Error("[@yesvara/svara] Telegram requires a bot token.");
|
|
565
|
+
this.baseUrl = `https://api.telegram.org/bot${config.token}`;
|
|
566
|
+
}
|
|
567
|
+
config;
|
|
568
|
+
name = "telegram";
|
|
569
|
+
agent;
|
|
570
|
+
baseUrl;
|
|
571
|
+
lastUpdateId = 0;
|
|
572
|
+
pollingTimer = null;
|
|
573
|
+
async mount(agent) {
|
|
574
|
+
this.agent = agent;
|
|
575
|
+
const me = await this.api("getMe");
|
|
576
|
+
console.log(`[@yesvara/svara] Telegram connected as @${me.username}`);
|
|
577
|
+
if (this.config.mode === "webhook" && this.config.webhookUrl) {
|
|
578
|
+
await this.api("setWebhook", { url: `${this.config.webhookUrl}/telegram/webhook` });
|
|
579
|
+
console.log(`[@yesvara/svara] Telegram webhook registered.`);
|
|
580
|
+
} else {
|
|
581
|
+
this.startPolling();
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
async send(sessionId, text) {
|
|
585
|
+
const chatId = parseInt(sessionId, 10);
|
|
586
|
+
if (!isNaN(chatId)) await this.sendMessage(chatId, text);
|
|
587
|
+
}
|
|
588
|
+
async stop() {
|
|
589
|
+
if (this.pollingTimer) clearInterval(this.pollingTimer);
|
|
590
|
+
}
|
|
591
|
+
startPolling() {
|
|
592
|
+
const interval = this.config.pollingInterval ?? 1e3;
|
|
593
|
+
console.log("[@yesvara/svara] Telegram polling started...");
|
|
594
|
+
this.pollingTimer = setInterval(async () => {
|
|
595
|
+
try {
|
|
596
|
+
const updates = await this.api("getUpdates", {
|
|
597
|
+
offset: this.lastUpdateId + 1,
|
|
598
|
+
allowed_updates: ["message"]
|
|
599
|
+
});
|
|
600
|
+
for (const update of updates) {
|
|
601
|
+
this.lastUpdateId = update.update_id;
|
|
602
|
+
if (update.message?.text) await this.handleUpdate(update);
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
}, interval);
|
|
607
|
+
}
|
|
608
|
+
async handleUpdate(update) {
|
|
609
|
+
const msg = update.message;
|
|
610
|
+
const message = {
|
|
611
|
+
id: String(msg.message_id),
|
|
612
|
+
sessionId: String(msg.chat.id),
|
|
613
|
+
userId: String(msg.from.id),
|
|
614
|
+
channel: "telegram",
|
|
615
|
+
text: msg.text ?? "",
|
|
616
|
+
timestamp: new Date(msg.date * 1e3),
|
|
617
|
+
raw: msg
|
|
618
|
+
};
|
|
619
|
+
await this.api("sendChatAction", { chat_id: msg.chat.id, action: "typing" }).catch(() => {
|
|
620
|
+
});
|
|
621
|
+
try {
|
|
622
|
+
const result = await this.agent.receive(message);
|
|
623
|
+
for (const chunk of this.split(result.response, 4096)) {
|
|
624
|
+
await this.sendMessage(msg.chat.id, chunk);
|
|
625
|
+
}
|
|
626
|
+
} catch (err) {
|
|
627
|
+
await this.sendMessage(msg.chat.id, "Sorry, something went wrong. Please try again.");
|
|
628
|
+
console.error("[@yesvara/svara] Telegram error:", err.message);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
async sendMessage(chatId, text) {
|
|
632
|
+
await this.api("sendMessage", { chat_id: chatId, text, parse_mode: "Markdown" });
|
|
633
|
+
}
|
|
634
|
+
async api(method, params) {
|
|
635
|
+
const res = await fetch(`${this.baseUrl}/${method}`, {
|
|
636
|
+
method: params ? "POST" : "GET",
|
|
637
|
+
headers: { "Content-Type": "application/json" },
|
|
638
|
+
body: params ? JSON.stringify(params) : void 0
|
|
639
|
+
});
|
|
640
|
+
const data = await res.json();
|
|
641
|
+
if (!data.ok) throw new Error(`Telegram API: ${data.description}`);
|
|
642
|
+
return data.result;
|
|
643
|
+
}
|
|
644
|
+
split(text, max) {
|
|
645
|
+
if (text.length <= max) return [text];
|
|
646
|
+
const chunks = [];
|
|
647
|
+
let rest = text;
|
|
648
|
+
while (rest.length > 0) {
|
|
649
|
+
chunks.push(rest.slice(0, max));
|
|
650
|
+
rest = rest.slice(max);
|
|
651
|
+
}
|
|
652
|
+
return chunks;
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// src/channels/whatsapp.ts
|
|
659
|
+
var whatsapp_exports = {};
|
|
660
|
+
__export(whatsapp_exports, {
|
|
661
|
+
WhatsAppChannel: () => WhatsAppChannel
|
|
662
|
+
});
|
|
663
|
+
var WhatsAppChannel;
|
|
664
|
+
var init_whatsapp = __esm({
|
|
665
|
+
"src/channels/whatsapp.ts"() {
|
|
666
|
+
"use strict";
|
|
667
|
+
WhatsAppChannel = class {
|
|
668
|
+
constructor(config) {
|
|
669
|
+
this.config = config;
|
|
670
|
+
if (!config.token || !config.phoneId || !config.verifyToken) {
|
|
671
|
+
throw new Error(
|
|
672
|
+
"[@yesvara/svara] WhatsApp requires: token, phoneId, and verifyToken."
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
const version = config.apiVersion ?? "v19.0";
|
|
676
|
+
this.apiUrl = `https://graph.facebook.com/${version}/${config.phoneId}`;
|
|
677
|
+
}
|
|
678
|
+
config;
|
|
679
|
+
name = "whatsapp";
|
|
680
|
+
agent;
|
|
681
|
+
apiUrl;
|
|
682
|
+
async mount(agent) {
|
|
683
|
+
this.agent = agent;
|
|
684
|
+
const webChannel = agent.channels?.get("web");
|
|
685
|
+
const app = webChannel?.app;
|
|
686
|
+
if (!app) {
|
|
687
|
+
console.warn(
|
|
688
|
+
'[@yesvara/svara] WhatsApp: no "web" channel found. Add connectChannel("web", ...) before connectChannel("whatsapp", ...) so the webhook can be mounted.'
|
|
689
|
+
);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
app.get("/whatsapp/webhook", (req, res) => {
|
|
693
|
+
const { "hub.mode": mode, "hub.verify_token": token, "hub.challenge": challenge } = req.query;
|
|
694
|
+
if (mode === "subscribe" && token === this.config.verifyToken) {
|
|
695
|
+
console.log("[@yesvara/svara] WhatsApp webhook verified.");
|
|
696
|
+
res.status(200).send(challenge);
|
|
697
|
+
} else {
|
|
698
|
+
res.status(403).send("Forbidden");
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
app.post("/whatsapp/webhook", async (req, res) => {
|
|
702
|
+
res.sendStatus(200);
|
|
703
|
+
if (req.body.object !== "whatsapp_business_account") return;
|
|
704
|
+
for (const entry of req.body.entry) {
|
|
705
|
+
for (const change of entry.changes) {
|
|
706
|
+
for (const waMsg of change.value.messages ?? []) {
|
|
707
|
+
if (waMsg.type !== "text" || !waMsg.text?.body) continue;
|
|
708
|
+
await this.handle(waMsg).catch(
|
|
709
|
+
(err) => console.error("[@yesvara/svara] WhatsApp error:", err.message)
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
});
|
|
715
|
+
console.log("[@yesvara/svara] WhatsApp webhook mounted at /whatsapp/webhook");
|
|
716
|
+
}
|
|
717
|
+
async send(to, text) {
|
|
718
|
+
await this.sendMessage(to, text);
|
|
719
|
+
}
|
|
720
|
+
async stop() {
|
|
721
|
+
}
|
|
722
|
+
async handle(waMsg) {
|
|
723
|
+
const message = {
|
|
724
|
+
id: waMsg.id,
|
|
725
|
+
sessionId: waMsg.from,
|
|
726
|
+
userId: waMsg.from,
|
|
727
|
+
channel: "whatsapp",
|
|
728
|
+
text: waMsg.text?.body ?? "",
|
|
729
|
+
timestamp: new Date(parseInt(waMsg.timestamp) * 1e3),
|
|
730
|
+
raw: waMsg
|
|
731
|
+
};
|
|
732
|
+
try {
|
|
733
|
+
const result = await this.agent.receive(message);
|
|
734
|
+
for (const chunk of this.split(result.response, 4e3)) {
|
|
735
|
+
await this.sendMessage(waMsg.from, chunk);
|
|
736
|
+
}
|
|
737
|
+
} catch (err) {
|
|
738
|
+
await this.sendMessage(waMsg.from, "Sorry, something went wrong. Please try again.");
|
|
739
|
+
throw err;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async sendMessage(to, text) {
|
|
743
|
+
const res = await fetch(`${this.apiUrl}/messages`, {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
Authorization: `Bearer ${this.config.token}`,
|
|
747
|
+
"Content-Type": "application/json"
|
|
748
|
+
},
|
|
749
|
+
body: JSON.stringify({
|
|
750
|
+
messaging_product: "whatsapp",
|
|
751
|
+
to,
|
|
752
|
+
type: "text",
|
|
753
|
+
text: { body: text }
|
|
754
|
+
})
|
|
755
|
+
});
|
|
756
|
+
if (!res.ok) {
|
|
757
|
+
const err = await res.json();
|
|
758
|
+
throw new Error(`WhatsApp API: ${err.error?.message}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
split(text, max) {
|
|
762
|
+
if (text.length <= max) return [text];
|
|
763
|
+
const chunks = [];
|
|
764
|
+
let rest = text;
|
|
765
|
+
while (rest.length > 0) {
|
|
766
|
+
chunks.push(rest.slice(0, max));
|
|
767
|
+
rest = rest.slice(max);
|
|
768
|
+
}
|
|
769
|
+
return chunks;
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
// src/index.ts
|
|
776
|
+
var index_exports = {};
|
|
777
|
+
__export(index_exports, {
|
|
778
|
+
Chunker: () => Chunker,
|
|
779
|
+
DocumentLoader: () => DocumentLoader,
|
|
780
|
+
SvaraAgent: () => SvaraAgent,
|
|
781
|
+
SvaraApp: () => SvaraApp,
|
|
782
|
+
SvaraDB: () => SvaraDB,
|
|
783
|
+
TelegramChannel: () => TelegramChannel,
|
|
784
|
+
VERSION: () => VERSION,
|
|
785
|
+
VectorRetriever: () => VectorRetriever,
|
|
786
|
+
WebChannel: () => WebChannel,
|
|
787
|
+
WhatsAppChannel: () => WhatsAppChannel,
|
|
788
|
+
createTool: () => createTool
|
|
789
|
+
});
|
|
790
|
+
module.exports = __toCommonJS(index_exports);
|
|
791
|
+
|
|
792
|
+
// src/app/index.ts
|
|
793
|
+
var import_express = __toESM(require("express"));
|
|
794
|
+
var import_http = require("http");
|
|
795
|
+
var SvaraApp = class {
|
|
796
|
+
express;
|
|
797
|
+
server = null;
|
|
798
|
+
constructor(options = {}) {
|
|
799
|
+
this.express = (0, import_express.default)();
|
|
800
|
+
this.setup(options);
|
|
801
|
+
}
|
|
802
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
803
|
+
/**
|
|
804
|
+
* Mount an agent (or any Express handler) on a route.
|
|
805
|
+
* Returns `this` for chaining.
|
|
806
|
+
*
|
|
807
|
+
* @example
|
|
808
|
+
* app
|
|
809
|
+
* .route('/chat', supportAgent.handler())
|
|
810
|
+
* .route('/sales', salesAgent.handler());
|
|
811
|
+
*/
|
|
812
|
+
route(path3, handler) {
|
|
813
|
+
this.express.post(path3, handler);
|
|
814
|
+
return this;
|
|
815
|
+
}
|
|
816
|
+
/**
|
|
817
|
+
* Add Express middleware (logging, auth, rate limiting, etc.)
|
|
818
|
+
*
|
|
819
|
+
* @example
|
|
820
|
+
* import rateLimit from 'express-rate-limit';
|
|
821
|
+
* app.use(rateLimit({ windowMs: 60_000, max: 100 }));
|
|
822
|
+
*/
|
|
823
|
+
use(middleware) {
|
|
824
|
+
this.express.use(middleware);
|
|
825
|
+
return this;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Start listening on the given port.
|
|
829
|
+
*
|
|
830
|
+
* @example
|
|
831
|
+
* await app.listen(3000);
|
|
832
|
+
* // → [@yesvara/svara] Listening at http://localhost:3000
|
|
833
|
+
*/
|
|
834
|
+
listen(port = 3e3) {
|
|
835
|
+
return new Promise((resolve, reject) => {
|
|
836
|
+
this.server = (0, import_http.createServer)(this.express);
|
|
837
|
+
this.server.listen(port, () => {
|
|
838
|
+
console.log(`[@yesvara/svara] Server running at http://localhost:${port}`);
|
|
839
|
+
resolve();
|
|
840
|
+
});
|
|
841
|
+
this.server.on("error", reject);
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
/**
|
|
845
|
+
* Stop the server gracefully.
|
|
846
|
+
*/
|
|
847
|
+
stop() {
|
|
848
|
+
return new Promise((resolve) => {
|
|
849
|
+
if (this.server) {
|
|
850
|
+
this.server.close(() => resolve());
|
|
851
|
+
} else {
|
|
852
|
+
resolve();
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Access the underlying Express app for advanced configuration.
|
|
858
|
+
*
|
|
859
|
+
* @example
|
|
860
|
+
* const expressApp = app.express();
|
|
861
|
+
* expressApp.set('trust proxy', 1);
|
|
862
|
+
*/
|
|
863
|
+
getExpressApp() {
|
|
864
|
+
return this.express;
|
|
865
|
+
}
|
|
866
|
+
// ─── Private Setup ────────────────────────────────────────────────────────
|
|
867
|
+
setup(options) {
|
|
868
|
+
this.express.use(import_express.default.json({ limit: options.bodyLimit ?? "10mb" }));
|
|
869
|
+
if (options.cors) {
|
|
870
|
+
this.express.use((_req, res, next) => {
|
|
871
|
+
const origin = options.cors === true ? "*" : options.cors;
|
|
872
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
873
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
874
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization");
|
|
875
|
+
next();
|
|
876
|
+
});
|
|
877
|
+
this.express.options("*", (_req, res) => res.sendStatus(204));
|
|
878
|
+
}
|
|
879
|
+
if (options.apiKey) {
|
|
880
|
+
this.express.use((req, res, next) => {
|
|
881
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
882
|
+
if (token !== options.apiKey) {
|
|
883
|
+
res.status(401).json({ error: "Unauthorized", message: "Invalid API key." });
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
next();
|
|
887
|
+
});
|
|
888
|
+
}
|
|
889
|
+
this.express.get("/health", (_req, res) => {
|
|
890
|
+
res.json({
|
|
891
|
+
status: "ok",
|
|
892
|
+
framework: "@yesvara/svara",
|
|
893
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
894
|
+
});
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/core/agent.ts
|
|
900
|
+
var import_events = __toESM(require("events"));
|
|
901
|
+
|
|
902
|
+
// src/core/llm.ts
|
|
903
|
+
var OPENAI_PREFIXES = ["gpt-", "o1", "o3", "text-davinci", "chatgpt"];
|
|
904
|
+
var ANTHROPIC_PREFIXES = ["claude-"];
|
|
905
|
+
var GROQ_MODELS = [
|
|
906
|
+
"llama-3.1-405b",
|
|
907
|
+
"llama-3.1-70b",
|
|
908
|
+
"llama-3.1-8b",
|
|
909
|
+
"mixtral-8x7b",
|
|
910
|
+
"gemma-7b",
|
|
911
|
+
"gemma2-9b"
|
|
912
|
+
];
|
|
913
|
+
function detectProvider(model) {
|
|
914
|
+
const m = model.toLowerCase();
|
|
915
|
+
if (OPENAI_PREFIXES.some((p) => m.startsWith(p))) return "openai";
|
|
916
|
+
if (ANTHROPIC_PREFIXES.some((p) => m.startsWith(p))) return "anthropic";
|
|
917
|
+
if (GROQ_MODELS.some((gm) => m.includes(gm)) && process.env.GROQ_API_KEY) {
|
|
918
|
+
return "groq";
|
|
919
|
+
}
|
|
920
|
+
return "ollama";
|
|
921
|
+
}
|
|
922
|
+
function resolveConfig(model, overrides = {}) {
|
|
923
|
+
const provider = overrides.provider ?? detectProvider(model);
|
|
924
|
+
return {
|
|
925
|
+
provider,
|
|
926
|
+
model,
|
|
927
|
+
temperature: 0.7,
|
|
928
|
+
timeout: 6e4,
|
|
929
|
+
...overrides
|
|
930
|
+
};
|
|
931
|
+
}
|
|
932
|
+
var OpenAIAdapter = class {
|
|
933
|
+
constructor(config) {
|
|
934
|
+
this.config = config;
|
|
935
|
+
this.client = this.init();
|
|
936
|
+
}
|
|
937
|
+
config;
|
|
938
|
+
client;
|
|
939
|
+
init() {
|
|
940
|
+
try {
|
|
941
|
+
const { default: OpenAI } = require("openai");
|
|
942
|
+
return new OpenAI({
|
|
943
|
+
apiKey: this.config.apiKey ?? process.env.OPENAI_API_KEY,
|
|
944
|
+
baseURL: this.config.baseURL,
|
|
945
|
+
timeout: this.config.timeout
|
|
946
|
+
});
|
|
947
|
+
} catch {
|
|
948
|
+
throw new SvaraLLMError(
|
|
949
|
+
"openai",
|
|
950
|
+
"Package not found. Run: npm install openai"
|
|
951
|
+
);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async chat(messages, tools, temperature) {
|
|
955
|
+
const client = this.client;
|
|
956
|
+
const response = await client.chat.completions.create({
|
|
957
|
+
model: this.config.model,
|
|
958
|
+
messages: messages.map(toOpenAIMessage),
|
|
959
|
+
tools: tools?.length ? tools.map(toOpenAITool) : void 0,
|
|
960
|
+
tool_choice: tools?.length ? "auto" : void 0,
|
|
961
|
+
temperature: temperature ?? this.config.temperature ?? 0.7,
|
|
962
|
+
max_tokens: this.config.maxTokens
|
|
963
|
+
});
|
|
964
|
+
const choice = response.choices[0];
|
|
965
|
+
const toolCalls = (choice.message.tool_calls ?? []).map((tc) => ({
|
|
966
|
+
id: tc.id,
|
|
967
|
+
name: tc.function.name,
|
|
968
|
+
arguments: safeParseJSON(tc.function.arguments)
|
|
969
|
+
}));
|
|
970
|
+
return {
|
|
971
|
+
content: choice.message.content ?? "",
|
|
972
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
973
|
+
usage: {
|
|
974
|
+
promptTokens: response.usage.prompt_tokens,
|
|
975
|
+
completionTokens: response.usage.completion_tokens,
|
|
976
|
+
totalTokens: response.usage.total_tokens
|
|
977
|
+
},
|
|
978
|
+
model: response.model,
|
|
979
|
+
finishReason: choice.finish_reason === "tool_calls" ? "tool_calls" : "stop"
|
|
980
|
+
};
|
|
981
|
+
}
|
|
982
|
+
countTokens(text) {
|
|
983
|
+
return Math.ceil(text.length / 4);
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
var AnthropicAdapter = class {
|
|
987
|
+
constructor(config) {
|
|
988
|
+
this.config = config;
|
|
989
|
+
this.client = this.init();
|
|
990
|
+
}
|
|
991
|
+
config;
|
|
992
|
+
client;
|
|
993
|
+
init() {
|
|
994
|
+
try {
|
|
995
|
+
const { default: Anthropic } = require("@anthropic-ai/sdk");
|
|
996
|
+
return new Anthropic({
|
|
997
|
+
apiKey: this.config.apiKey ?? process.env.ANTHROPIC_API_KEY,
|
|
998
|
+
baseURL: this.config.baseURL,
|
|
999
|
+
timeout: this.config.timeout
|
|
1000
|
+
});
|
|
1001
|
+
} catch {
|
|
1002
|
+
throw new SvaraLLMError(
|
|
1003
|
+
"anthropic",
|
|
1004
|
+
"Package not found. Run: npm install @anthropic-ai/sdk"
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
async chat(messages, tools, temperature) {
|
|
1009
|
+
const client = this.client;
|
|
1010
|
+
const systemMsg = messages.find((m) => m.role === "system")?.content;
|
|
1011
|
+
const chatMsgs = messages.filter((m) => m.role !== "system").map(toAnthropicMessage);
|
|
1012
|
+
const response = await client.messages.create({
|
|
1013
|
+
model: this.config.model,
|
|
1014
|
+
system: systemMsg,
|
|
1015
|
+
messages: chatMsgs,
|
|
1016
|
+
tools: tools?.length ? tools.map(toAnthropicTool) : void 0,
|
|
1017
|
+
max_tokens: this.config.maxTokens ?? 4096,
|
|
1018
|
+
temperature: temperature ?? this.config.temperature ?? 0.7
|
|
1019
|
+
});
|
|
1020
|
+
const textParts = response.content.filter((c) => c.type === "text");
|
|
1021
|
+
const toolParts = response.content.filter((c) => c.type === "tool_use");
|
|
1022
|
+
const toolCalls = toolParts.map((c) => ({
|
|
1023
|
+
id: c.id,
|
|
1024
|
+
name: c.name,
|
|
1025
|
+
arguments: c.input
|
|
1026
|
+
}));
|
|
1027
|
+
return {
|
|
1028
|
+
content: textParts.map((c) => c.text).join(""),
|
|
1029
|
+
toolCalls: toolCalls.length ? toolCalls : void 0,
|
|
1030
|
+
usage: {
|
|
1031
|
+
promptTokens: response.usage.input_tokens,
|
|
1032
|
+
completionTokens: response.usage.output_tokens,
|
|
1033
|
+
totalTokens: response.usage.input_tokens + response.usage.output_tokens
|
|
1034
|
+
},
|
|
1035
|
+
model: response.model,
|
|
1036
|
+
finishReason: response.stop_reason === "tool_use" ? "tool_calls" : "stop"
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
1039
|
+
countTokens(text) {
|
|
1040
|
+
return Math.ceil(text.length / 4);
|
|
1041
|
+
}
|
|
1042
|
+
};
|
|
1043
|
+
var OllamaAdapter = class {
|
|
1044
|
+
constructor(config) {
|
|
1045
|
+
this.config = config;
|
|
1046
|
+
this.baseURL = config.baseURL ?? "http://localhost:11434";
|
|
1047
|
+
}
|
|
1048
|
+
config;
|
|
1049
|
+
baseURL;
|
|
1050
|
+
async chat(messages, _tools, temperature) {
|
|
1051
|
+
const response = await fetch(`${this.baseURL}/api/chat`, {
|
|
1052
|
+
method: "POST",
|
|
1053
|
+
headers: { "Content-Type": "application/json" },
|
|
1054
|
+
body: JSON.stringify({
|
|
1055
|
+
model: this.config.model,
|
|
1056
|
+
messages: messages.map((m) => ({ role: m.role, content: m.content })),
|
|
1057
|
+
options: { temperature: temperature ?? this.config.temperature ?? 0.7 },
|
|
1058
|
+
stream: false
|
|
1059
|
+
}),
|
|
1060
|
+
signal: AbortSignal.timeout(this.config.timeout ?? 6e4)
|
|
1061
|
+
});
|
|
1062
|
+
if (!response.ok) {
|
|
1063
|
+
throw new SvaraLLMError("ollama", `Request failed: ${response.statusText}. Is Ollama running?`);
|
|
1064
|
+
}
|
|
1065
|
+
const data = await response.json();
|
|
1066
|
+
return {
|
|
1067
|
+
content: data.message.content,
|
|
1068
|
+
usage: {
|
|
1069
|
+
promptTokens: data.prompt_eval_count ?? 0,
|
|
1070
|
+
completionTokens: data.eval_count ?? 0,
|
|
1071
|
+
totalTokens: (data.prompt_eval_count ?? 0) + (data.eval_count ?? 0)
|
|
1072
|
+
},
|
|
1073
|
+
model: data.model ?? this.config.model,
|
|
1074
|
+
finishReason: "stop"
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
countTokens(text) {
|
|
1078
|
+
return Math.ceil(text.length / 4);
|
|
1079
|
+
}
|
|
1080
|
+
};
|
|
1081
|
+
var GroqAdapter = class extends OpenAIAdapter {
|
|
1082
|
+
constructor(config) {
|
|
1083
|
+
super({
|
|
1084
|
+
...config,
|
|
1085
|
+
baseURL: config.baseURL ?? "https://api.groq.com/openai/v1",
|
|
1086
|
+
apiKey: config.apiKey ?? process.env.GROQ_API_KEY
|
|
1087
|
+
});
|
|
1088
|
+
}
|
|
1089
|
+
};
|
|
1090
|
+
function createAdapter(config) {
|
|
1091
|
+
switch (config.provider) {
|
|
1092
|
+
case "openai":
|
|
1093
|
+
return new OpenAIAdapter(config);
|
|
1094
|
+
case "anthropic":
|
|
1095
|
+
return new AnthropicAdapter(config);
|
|
1096
|
+
case "ollama":
|
|
1097
|
+
return new OllamaAdapter(config);
|
|
1098
|
+
case "groq":
|
|
1099
|
+
return new GroqAdapter(config);
|
|
1100
|
+
default:
|
|
1101
|
+
throw new Error(
|
|
1102
|
+
`[@yesvara/svara] Unknown LLM provider: "${config.provider}".
|
|
1103
|
+
Auto-supported: openai, anthropic, ollama, groq`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
function toOpenAIMessage(msg) {
|
|
1108
|
+
if (msg.role === "tool") {
|
|
1109
|
+
return { role: "tool", tool_call_id: msg.toolCallId, content: msg.content };
|
|
1110
|
+
}
|
|
1111
|
+
if (msg.toolCalls?.length) {
|
|
1112
|
+
return {
|
|
1113
|
+
role: "assistant",
|
|
1114
|
+
content: msg.content || null,
|
|
1115
|
+
tool_calls: msg.toolCalls.map((tc) => ({
|
|
1116
|
+
id: tc.id,
|
|
1117
|
+
type: "function",
|
|
1118
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments) }
|
|
1119
|
+
}))
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
return { role: msg.role, content: msg.content };
|
|
1123
|
+
}
|
|
1124
|
+
function toOpenAITool(tool) {
|
|
1125
|
+
return {
|
|
1126
|
+
type: "function",
|
|
1127
|
+
function: {
|
|
1128
|
+
name: tool.name,
|
|
1129
|
+
description: tool.description,
|
|
1130
|
+
parameters: {
|
|
1131
|
+
type: "object",
|
|
1132
|
+
properties: Object.fromEntries(
|
|
1133
|
+
Object.entries(tool.parameters).map(([k, p]) => [
|
|
1134
|
+
k,
|
|
1135
|
+
{ type: p.type, description: p.description, enum: p.enum }
|
|
1136
|
+
])
|
|
1137
|
+
),
|
|
1138
|
+
required: Object.entries(tool.parameters).filter(([, p]) => p.required).map(([k]) => k)
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
function toAnthropicMessage(msg) {
|
|
1144
|
+
if (msg.role === "tool") {
|
|
1145
|
+
return {
|
|
1146
|
+
role: "user",
|
|
1147
|
+
content: [{ type: "tool_result", tool_use_id: msg.toolCallId, content: msg.content }]
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (msg.toolCalls?.length) {
|
|
1151
|
+
return {
|
|
1152
|
+
role: "assistant",
|
|
1153
|
+
content: [
|
|
1154
|
+
...msg.content ? [{ type: "text", text: msg.content }] : [],
|
|
1155
|
+
...msg.toolCalls.map((tc) => ({
|
|
1156
|
+
type: "tool_use",
|
|
1157
|
+
id: tc.id,
|
|
1158
|
+
name: tc.name,
|
|
1159
|
+
input: tc.arguments
|
|
1160
|
+
}))
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
return { role: msg.role, content: msg.content };
|
|
1165
|
+
}
|
|
1166
|
+
function toAnthropicTool(tool) {
|
|
1167
|
+
return {
|
|
1168
|
+
name: tool.name,
|
|
1169
|
+
description: tool.description,
|
|
1170
|
+
input_schema: {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties: Object.fromEntries(
|
|
1173
|
+
Object.entries(tool.parameters).map(([k, p]) => [
|
|
1174
|
+
k,
|
|
1175
|
+
{ type: p.type, description: p.description, enum: p.enum }
|
|
1176
|
+
])
|
|
1177
|
+
),
|
|
1178
|
+
required: Object.entries(tool.parameters).filter(([, p]) => p.required).map(([k]) => k)
|
|
1179
|
+
}
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
var SvaraLLMError = class extends Error {
|
|
1183
|
+
constructor(provider, message) {
|
|
1184
|
+
super(`[@yesvara/svara] LLM error (${provider}): ${message}`);
|
|
1185
|
+
this.provider = provider;
|
|
1186
|
+
this.name = "SvaraLLMError";
|
|
1187
|
+
}
|
|
1188
|
+
provider;
|
|
1189
|
+
};
|
|
1190
|
+
function safeParseJSON(str) {
|
|
1191
|
+
try {
|
|
1192
|
+
return JSON.parse(str);
|
|
1193
|
+
} catch {
|
|
1194
|
+
return {};
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
// src/memory/conversation.ts
|
|
1199
|
+
var ConversationMemory = class {
|
|
1200
|
+
constructor(config) {
|
|
1201
|
+
this.config = config;
|
|
1202
|
+
}
|
|
1203
|
+
config;
|
|
1204
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1205
|
+
async getHistory(sessionId) {
|
|
1206
|
+
return this.sessions.get(sessionId)?.messages ?? [];
|
|
1207
|
+
}
|
|
1208
|
+
async append(sessionId, messages) {
|
|
1209
|
+
if (this.config.type === "none") return;
|
|
1210
|
+
const store = this.sessions.get(sessionId) ?? {
|
|
1211
|
+
messages: [],
|
|
1212
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
1213
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
1214
|
+
};
|
|
1215
|
+
store.messages.push(...messages);
|
|
1216
|
+
store.updatedAt = /* @__PURE__ */ new Date();
|
|
1217
|
+
if (store.messages.length > this.config.maxMessages) {
|
|
1218
|
+
const system = store.messages.filter((m) => m.role === "system");
|
|
1219
|
+
const rest = store.messages.filter((m) => m.role !== "system");
|
|
1220
|
+
store.messages = [...system, ...rest.slice(-this.config.maxMessages)];
|
|
1221
|
+
}
|
|
1222
|
+
this.sessions.set(sessionId, store);
|
|
1223
|
+
}
|
|
1224
|
+
async clear(sessionId) {
|
|
1225
|
+
this.sessions.delete(sessionId);
|
|
1226
|
+
}
|
|
1227
|
+
getSessionIds() {
|
|
1228
|
+
return [...this.sessions.keys()];
|
|
1229
|
+
}
|
|
1230
|
+
};
|
|
1231
|
+
|
|
1232
|
+
// src/memory/context.ts
|
|
1233
|
+
var ContextBuilder = class {
|
|
1234
|
+
constructor(llm) {
|
|
1235
|
+
this.llm = llm;
|
|
1236
|
+
}
|
|
1237
|
+
llm;
|
|
1238
|
+
buildMessages(systemPrompt, history, userMessage, ragContext) {
|
|
1239
|
+
const messages = [
|
|
1240
|
+
{ role: "system", content: systemPrompt },
|
|
1241
|
+
// Exclude any system messages from history — we prepend our own
|
|
1242
|
+
...history.filter((m) => m.role !== "system")
|
|
1243
|
+
];
|
|
1244
|
+
const content = ragContext ? this.augmentWithRAG(userMessage, ragContext) : userMessage;
|
|
1245
|
+
messages.push({ role: "user", content });
|
|
1246
|
+
return messages;
|
|
1247
|
+
}
|
|
1248
|
+
estimateTokens(messages) {
|
|
1249
|
+
const text = messages.map((m) => m.content).join(" ");
|
|
1250
|
+
return this.llm.countTokens(text);
|
|
1251
|
+
}
|
|
1252
|
+
augmentWithRAG(message, context) {
|
|
1253
|
+
return [
|
|
1254
|
+
"Use the following context to answer the question.",
|
|
1255
|
+
"If the answer isn't in the context, say so honestly \u2014 don't guess.",
|
|
1256
|
+
"",
|
|
1257
|
+
"--- Context ---",
|
|
1258
|
+
context,
|
|
1259
|
+
"--- End Context ---",
|
|
1260
|
+
"",
|
|
1261
|
+
`Question: ${message}`
|
|
1262
|
+
].join("\n");
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
// src/tools/registry.ts
|
|
1267
|
+
var ToolRegistry = class {
|
|
1268
|
+
tools = /* @__PURE__ */ new Map();
|
|
1269
|
+
register(tool) {
|
|
1270
|
+
if (this.tools.has(tool.name)) {
|
|
1271
|
+
throw new Error(
|
|
1272
|
+
`[@yesvara/svara] Tool "${tool.name}" is already registered. Use a different name or call registry.update() to replace it.`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
this.tools.set(tool.name, tool);
|
|
1276
|
+
}
|
|
1277
|
+
update(tool) {
|
|
1278
|
+
this.tools.set(tool.name, tool);
|
|
1279
|
+
}
|
|
1280
|
+
unregister(name) {
|
|
1281
|
+
this.tools.delete(name);
|
|
1282
|
+
}
|
|
1283
|
+
get(name) {
|
|
1284
|
+
return this.tools.get(name);
|
|
1285
|
+
}
|
|
1286
|
+
getAll() {
|
|
1287
|
+
return [...this.tools.values()];
|
|
1288
|
+
}
|
|
1289
|
+
has(name) {
|
|
1290
|
+
return this.tools.has(name);
|
|
1291
|
+
}
|
|
1292
|
+
get size() {
|
|
1293
|
+
return this.tools.size;
|
|
1294
|
+
}
|
|
1295
|
+
};
|
|
1296
|
+
|
|
1297
|
+
// src/tools/executor.ts
|
|
1298
|
+
var ToolExecutor = class {
|
|
1299
|
+
constructor(registry) {
|
|
1300
|
+
this.registry = registry;
|
|
1301
|
+
}
|
|
1302
|
+
registry;
|
|
1303
|
+
async execute(call, ctx) {
|
|
1304
|
+
const start = Date.now();
|
|
1305
|
+
const tool = this.registry.get(call.name);
|
|
1306
|
+
if (!tool) {
|
|
1307
|
+
return {
|
|
1308
|
+
toolCallId: call.id,
|
|
1309
|
+
name: call.name,
|
|
1310
|
+
result: null,
|
|
1311
|
+
error: `Tool "${call.name}" is not registered. Available: ${this.registry.getAll().map((t) => t.name).join(", ")}`,
|
|
1312
|
+
duration: Date.now() - start
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
try {
|
|
1316
|
+
const result = await Promise.race([
|
|
1317
|
+
tool.run(call.arguments, ctx),
|
|
1318
|
+
this.timeout(tool.timeout ?? 3e4, tool.name)
|
|
1319
|
+
]);
|
|
1320
|
+
return { toolCallId: call.id, name: call.name, result, duration: Date.now() - start };
|
|
1321
|
+
} catch (err) {
|
|
1322
|
+
const message = err.message;
|
|
1323
|
+
console.error(`[@yesvara/svara] Tool "${call.name}" failed: ${message}`);
|
|
1324
|
+
return {
|
|
1325
|
+
toolCallId: call.id,
|
|
1326
|
+
name: call.name,
|
|
1327
|
+
result: null,
|
|
1328
|
+
error: message,
|
|
1329
|
+
duration: Date.now() - start
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
async executeAll(calls, ctx) {
|
|
1334
|
+
return Promise.all(calls.map((c) => this.execute(c, ctx)));
|
|
1335
|
+
}
|
|
1336
|
+
timeout(ms, name) {
|
|
1337
|
+
return new Promise(
|
|
1338
|
+
(_, reject) => setTimeout(() => reject(new Error(`Tool "${name}" timed out after ${ms}ms`)), ms)
|
|
1339
|
+
);
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
1343
|
+
// src/core/agent.ts
|
|
1344
|
+
var SvaraAgent = class extends import_events.default {
|
|
1345
|
+
name;
|
|
1346
|
+
llmConfig;
|
|
1347
|
+
llm;
|
|
1348
|
+
systemPrompt;
|
|
1349
|
+
tools;
|
|
1350
|
+
executor;
|
|
1351
|
+
memory;
|
|
1352
|
+
context;
|
|
1353
|
+
maxIterations;
|
|
1354
|
+
verbose;
|
|
1355
|
+
channels = /* @__PURE__ */ new Map();
|
|
1356
|
+
knowledgeBase = null;
|
|
1357
|
+
knowledgePaths = [];
|
|
1358
|
+
isStarted = false;
|
|
1359
|
+
constructor(config) {
|
|
1360
|
+
super();
|
|
1361
|
+
this.name = config.name;
|
|
1362
|
+
this.maxIterations = config.maxIterations ?? 10;
|
|
1363
|
+
this.verbose = config.verbose ?? false;
|
|
1364
|
+
this.systemPrompt = config.systemPrompt ?? `You are ${config.name}, a helpful and friendly AI assistant. Be concise and accurate.`;
|
|
1365
|
+
this.llmConfig = resolveConfig(config.model, {
|
|
1366
|
+
temperature: config.temperature,
|
|
1367
|
+
maxTokens: config.maxTokens,
|
|
1368
|
+
...config.llm
|
|
1369
|
+
});
|
|
1370
|
+
this.llm = createAdapter(this.llmConfig);
|
|
1371
|
+
const memCfg = config.memory ?? true;
|
|
1372
|
+
const window = memCfg === false ? 0 : typeof memCfg === "object" ? memCfg.window ?? 20 : 20;
|
|
1373
|
+
this.memory = new ConversationMemory({ type: "conversation", maxMessages: window });
|
|
1374
|
+
this.context = new ContextBuilder(this.llm);
|
|
1375
|
+
this.tools = new ToolRegistry();
|
|
1376
|
+
this.executor = new ToolExecutor(this.tools);
|
|
1377
|
+
config.tools?.forEach((t) => this.addTool(t));
|
|
1378
|
+
if (config.knowledge) {
|
|
1379
|
+
this.knowledgePaths = Array.isArray(config.knowledge) ? config.knowledge : [config.knowledge];
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
// ─── Public API ────────────────────────────────────────────────────────────
|
|
1383
|
+
/**
|
|
1384
|
+
* Send a message and get a reply. The simplest way to use an agent.
|
|
1385
|
+
*
|
|
1386
|
+
* @example
|
|
1387
|
+
* const reply = await agent.chat('What is the weather in Tokyo?');
|
|
1388
|
+
* console.log(reply); // "Currently 28°C and sunny in Tokyo."
|
|
1389
|
+
*
|
|
1390
|
+
* @param message The user's message.
|
|
1391
|
+
* @param sessionId Optional session ID for multi-turn conversations.
|
|
1392
|
+
* Defaults to 'default' — all calls share one history.
|
|
1393
|
+
*/
|
|
1394
|
+
async chat(message, sessionId = "default") {
|
|
1395
|
+
const result = await this.run(message, { sessionId });
|
|
1396
|
+
return result.response;
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Process a message and get the full result with metadata.
|
|
1400
|
+
* Use this when you need usage stats, tool info, or session details.
|
|
1401
|
+
*
|
|
1402
|
+
* @example
|
|
1403
|
+
* const result = await agent.process('Summarize my report', {
|
|
1404
|
+
* sessionId: 'user-42',
|
|
1405
|
+
* userId: 'alice@example.com',
|
|
1406
|
+
* });
|
|
1407
|
+
* console.log(result.response); // The agent's reply
|
|
1408
|
+
* console.log(result.toolsUsed); // ['read_file', 'summarize']
|
|
1409
|
+
* console.log(result.usage); // { totalTokens: 1234, ... }
|
|
1410
|
+
*/
|
|
1411
|
+
async process(message, options) {
|
|
1412
|
+
return this.run(message, options ?? {});
|
|
1413
|
+
}
|
|
1414
|
+
/**
|
|
1415
|
+
* Register a tool the agent can call during a conversation.
|
|
1416
|
+
* Returns `this` for chaining.
|
|
1417
|
+
*
|
|
1418
|
+
* @example
|
|
1419
|
+
* agent
|
|
1420
|
+
* .addTool(weatherTool)
|
|
1421
|
+
* .addTool(emailTool)
|
|
1422
|
+
* .addTool(databaseTool);
|
|
1423
|
+
*/
|
|
1424
|
+
addTool(tool) {
|
|
1425
|
+
const internal = {
|
|
1426
|
+
name: tool.name,
|
|
1427
|
+
description: tool.description,
|
|
1428
|
+
parameters: tool.parameters ?? {},
|
|
1429
|
+
run: tool.run,
|
|
1430
|
+
category: tool.category,
|
|
1431
|
+
timeout: tool.timeout
|
|
1432
|
+
};
|
|
1433
|
+
this.tools.register(internal);
|
|
1434
|
+
return this;
|
|
1435
|
+
}
|
|
1436
|
+
/**
|
|
1437
|
+
* Connect a messaging channel. The agent will receive and respond to
|
|
1438
|
+
* messages from this channel automatically.
|
|
1439
|
+
*
|
|
1440
|
+
* @example
|
|
1441
|
+
* agent.connectChannel('telegram', { token: process.env.TG_TOKEN });
|
|
1442
|
+
* agent.connectChannel('whatsapp', {
|
|
1443
|
+
* token: process.env.WA_TOKEN,
|
|
1444
|
+
* phoneId: process.env.WA_PHONE_ID,
|
|
1445
|
+
* verifyToken: process.env.WA_VERIFY_TOKEN,
|
|
1446
|
+
* });
|
|
1447
|
+
*/
|
|
1448
|
+
connectChannel(name, config) {
|
|
1449
|
+
const channel = this.loadChannel(name, config);
|
|
1450
|
+
this.channels.set(name, channel);
|
|
1451
|
+
return this;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* Returns an Express request handler for mounting on any HTTP server.
|
|
1455
|
+
* POST body: `{ message: string, sessionId?: string, userId?: string }`
|
|
1456
|
+
*
|
|
1457
|
+
* @example With SvaraApp
|
|
1458
|
+
* app.route('/chat', agent.handler());
|
|
1459
|
+
*
|
|
1460
|
+
* @example With existing Express app
|
|
1461
|
+
* expressApp.post('/api/chat', agent.handler());
|
|
1462
|
+
*/
|
|
1463
|
+
handler() {
|
|
1464
|
+
return async (req, res) => {
|
|
1465
|
+
const { message, sessionId, userId } = req.body;
|
|
1466
|
+
if (!message?.trim()) {
|
|
1467
|
+
res.status(400).json({
|
|
1468
|
+
error: "Bad Request",
|
|
1469
|
+
message: 'Request body must include a non-empty "message" field.'
|
|
1470
|
+
});
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
try {
|
|
1474
|
+
const result = await this.run(message, {
|
|
1475
|
+
sessionId: sessionId ?? req.headers["x-session-id"],
|
|
1476
|
+
userId
|
|
1477
|
+
});
|
|
1478
|
+
res.json({
|
|
1479
|
+
response: result.response,
|
|
1480
|
+
sessionId: result.sessionId,
|
|
1481
|
+
usage: result.usage,
|
|
1482
|
+
toolsUsed: result.toolsUsed
|
|
1483
|
+
});
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
const error = err;
|
|
1486
|
+
this.log("error", error.message);
|
|
1487
|
+
res.status(500).json({ error: "Internal Server Error", message: error.message });
|
|
1488
|
+
}
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Initialize all channels and knowledge base, then start listening.
|
|
1493
|
+
* Call this once after you've configured the agent.
|
|
1494
|
+
*
|
|
1495
|
+
* @example
|
|
1496
|
+
* agent.connectChannel('web', { port: 3000 });
|
|
1497
|
+
* await agent.start(); // "Web channel running at http://localhost:3000"
|
|
1498
|
+
*/
|
|
1499
|
+
async start() {
|
|
1500
|
+
if (this.isStarted) {
|
|
1501
|
+
console.warn(`[@yesvara/svara] ${this.name} is already running.`);
|
|
1502
|
+
return;
|
|
1503
|
+
}
|
|
1504
|
+
if (this.knowledgePaths.length) {
|
|
1505
|
+
await this.initKnowledge(this.knowledgePaths);
|
|
1506
|
+
}
|
|
1507
|
+
for (const [name, channel] of this.channels) {
|
|
1508
|
+
await channel.mount(this);
|
|
1509
|
+
this.log("info", `Channel "${name}" connected.`);
|
|
1510
|
+
this.emit("channel:ready", { channel: name });
|
|
1511
|
+
}
|
|
1512
|
+
this.isStarted = true;
|
|
1513
|
+
if (this.channels.size === 0) {
|
|
1514
|
+
console.warn(
|
|
1515
|
+
`[@yesvara/svara] ${this.name} has no channels configured.
|
|
1516
|
+
Add one: agent.connectChannel('web', { port: 3000 })`
|
|
1517
|
+
);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
/**
|
|
1521
|
+
* Gracefully shut down all channels.
|
|
1522
|
+
*/
|
|
1523
|
+
async stop() {
|
|
1524
|
+
for (const [, channel] of this.channels) {
|
|
1525
|
+
await channel.stop();
|
|
1526
|
+
}
|
|
1527
|
+
this.isStarted = false;
|
|
1528
|
+
this.emit("stopped");
|
|
1529
|
+
}
|
|
1530
|
+
/**
|
|
1531
|
+
* Clear conversation history for a session.
|
|
1532
|
+
*
|
|
1533
|
+
* @example
|
|
1534
|
+
* agent.on('user:leave', (userId) => agent.clearHistory(userId));
|
|
1535
|
+
*/
|
|
1536
|
+
async clearHistory(sessionId) {
|
|
1537
|
+
await this.memory.clear(sessionId);
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Add documents to the knowledge base at runtime (no restart needed).
|
|
1541
|
+
*
|
|
1542
|
+
* @example
|
|
1543
|
+
* agent.addKnowledge('./new-policies.pdf');
|
|
1544
|
+
*/
|
|
1545
|
+
async addKnowledge(paths) {
|
|
1546
|
+
const arr = Array.isArray(paths) ? paths : [paths];
|
|
1547
|
+
if (!this.knowledgeBase) {
|
|
1548
|
+
await this.initKnowledge(arr);
|
|
1549
|
+
} else {
|
|
1550
|
+
await this.knowledgeBase.load(arr);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
// ─── Internal: Agentic Loop ───────────────────────────────────────────────
|
|
1554
|
+
/**
|
|
1555
|
+
* Receives a raw incoming message from a channel and processes it.
|
|
1556
|
+
* Called by channel handlers — not typically used directly.
|
|
1557
|
+
*/
|
|
1558
|
+
async receive(msg) {
|
|
1559
|
+
return this.run(msg.text, {
|
|
1560
|
+
sessionId: msg.sessionId,
|
|
1561
|
+
userId: msg.userId
|
|
1562
|
+
});
|
|
1563
|
+
}
|
|
1564
|
+
async run(message, options) {
|
|
1565
|
+
const startTime = Date.now();
|
|
1566
|
+
const sessionId = options.sessionId ?? crypto.randomUUID();
|
|
1567
|
+
this.emit("message:received", { message, sessionId, userId: options.userId });
|
|
1568
|
+
const history = await this.memory.getHistory(sessionId);
|
|
1569
|
+
let ragContext = "";
|
|
1570
|
+
if (this.knowledgeBase) {
|
|
1571
|
+
ragContext = await this.knowledgeBase.retrieve(message);
|
|
1572
|
+
}
|
|
1573
|
+
const messages = this.context.buildMessages(
|
|
1574
|
+
this.systemPrompt,
|
|
1575
|
+
history,
|
|
1576
|
+
message,
|
|
1577
|
+
ragContext
|
|
1578
|
+
);
|
|
1579
|
+
const internalCtx = {
|
|
1580
|
+
sessionId,
|
|
1581
|
+
userId: options.userId ?? "unknown",
|
|
1582
|
+
agentName: this.name,
|
|
1583
|
+
history,
|
|
1584
|
+
metadata: options.metadata ?? {}
|
|
1585
|
+
};
|
|
1586
|
+
const toolsUsed = [];
|
|
1587
|
+
const totalUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
1588
|
+
let iterations = 0;
|
|
1589
|
+
let finalResponse = "";
|
|
1590
|
+
while (iterations < this.maxIterations) {
|
|
1591
|
+
iterations++;
|
|
1592
|
+
this.log("debug", `Iteration ${iterations}`);
|
|
1593
|
+
const allTools = this.tools.getAll();
|
|
1594
|
+
const llmResponse = await this.llm.chat(messages, allTools, this.llmConfig.temperature);
|
|
1595
|
+
totalUsage.promptTokens += llmResponse.usage.promptTokens;
|
|
1596
|
+
totalUsage.completionTokens += llmResponse.usage.completionTokens;
|
|
1597
|
+
totalUsage.totalTokens += llmResponse.usage.totalTokens;
|
|
1598
|
+
if (!llmResponse.toolCalls?.length) {
|
|
1599
|
+
finalResponse = llmResponse.content;
|
|
1600
|
+
messages.push({ role: "assistant", content: finalResponse });
|
|
1601
|
+
break;
|
|
1602
|
+
}
|
|
1603
|
+
messages.push({
|
|
1604
|
+
role: "assistant",
|
|
1605
|
+
content: llmResponse.content,
|
|
1606
|
+
toolCalls: llmResponse.toolCalls
|
|
1607
|
+
});
|
|
1608
|
+
this.emit("tool:call", {
|
|
1609
|
+
sessionId,
|
|
1610
|
+
tools: llmResponse.toolCalls.map((tc) => tc.name)
|
|
1611
|
+
});
|
|
1612
|
+
const results = await this.executor.executeAll(llmResponse.toolCalls, internalCtx);
|
|
1613
|
+
for (const result2 of results) {
|
|
1614
|
+
toolsUsed.push(result2.name);
|
|
1615
|
+
const content = result2.error ? `Error executing ${result2.name}: ${result2.error}` : JSON.stringify(result2.result, null, 2);
|
|
1616
|
+
messages.push({
|
|
1617
|
+
role: "tool",
|
|
1618
|
+
content,
|
|
1619
|
+
toolCallId: result2.toolCallId,
|
|
1620
|
+
name: result2.name
|
|
1621
|
+
});
|
|
1622
|
+
this.emit("tool:result", { sessionId, name: result2.name, result: result2.result });
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
if (!finalResponse) {
|
|
1626
|
+
finalResponse = `I've reached the reasoning limit for this request. Please try a simpler question.`;
|
|
1627
|
+
}
|
|
1628
|
+
await this.memory.append(sessionId, [
|
|
1629
|
+
{ role: "user", content: message },
|
|
1630
|
+
{ role: "assistant", content: finalResponse }
|
|
1631
|
+
]);
|
|
1632
|
+
const result = {
|
|
1633
|
+
response: finalResponse,
|
|
1634
|
+
sessionId,
|
|
1635
|
+
toolsUsed: [...new Set(toolsUsed)],
|
|
1636
|
+
iterations,
|
|
1637
|
+
usage: totalUsage,
|
|
1638
|
+
duration: Date.now() - startTime
|
|
1639
|
+
};
|
|
1640
|
+
this.emit("message:sent", { response: finalResponse, sessionId });
|
|
1641
|
+
return result;
|
|
1642
|
+
}
|
|
1643
|
+
// ─── Private Helpers ──────────────────────────────────────────────────────
|
|
1644
|
+
async initKnowledge(paths) {
|
|
1645
|
+
try {
|
|
1646
|
+
const { glob } = await import("glob");
|
|
1647
|
+
const { VectorRetriever: VectorRetriever2 } = await Promise.resolve().then(() => (init_retriever(), retriever_exports));
|
|
1648
|
+
const retriever = new VectorRetriever2();
|
|
1649
|
+
await retriever.init({ embeddings: { provider: "openai" } });
|
|
1650
|
+
const files = [];
|
|
1651
|
+
for (const pattern of paths) {
|
|
1652
|
+
const matches = await glob(pattern);
|
|
1653
|
+
files.push(...matches);
|
|
1654
|
+
}
|
|
1655
|
+
if (files.length === 0) {
|
|
1656
|
+
console.warn(`[@yesvara/svara] No files found matching: ${paths.join(", ")}`);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
await retriever.addDocuments(files);
|
|
1660
|
+
this.knowledgeBase = {
|
|
1661
|
+
load: async (p) => {
|
|
1662
|
+
const newFiles = [];
|
|
1663
|
+
for (const pattern of Array.isArray(p) ? p : [p]) {
|
|
1664
|
+
newFiles.push(...await glob(pattern));
|
|
1665
|
+
}
|
|
1666
|
+
await retriever.addDocuments(newFiles);
|
|
1667
|
+
},
|
|
1668
|
+
retrieve: (query, topK) => retriever.retrieve(query, topK)
|
|
1669
|
+
};
|
|
1670
|
+
this.log("info", `Knowledge base loaded: ${files.length} file(s).`);
|
|
1671
|
+
} catch (err) {
|
|
1672
|
+
console.warn(`[@yesvara/svara] Knowledge base init failed: ${err.message}`);
|
|
1673
|
+
}
|
|
1674
|
+
}
|
|
1675
|
+
loadChannel(name, config) {
|
|
1676
|
+
try {
|
|
1677
|
+
switch (name) {
|
|
1678
|
+
case "web": {
|
|
1679
|
+
const { WebChannel: WebChannel2 } = (init_web(), __toCommonJS(web_exports));
|
|
1680
|
+
return new WebChannel2(config);
|
|
1681
|
+
}
|
|
1682
|
+
case "telegram": {
|
|
1683
|
+
const { TelegramChannel: TelegramChannel2 } = (init_telegram(), __toCommonJS(telegram_exports));
|
|
1684
|
+
return new TelegramChannel2(config);
|
|
1685
|
+
}
|
|
1686
|
+
case "whatsapp": {
|
|
1687
|
+
const { WhatsAppChannel: WhatsAppChannel2 } = (init_whatsapp(), __toCommonJS(whatsapp_exports));
|
|
1688
|
+
return new WhatsAppChannel2(config);
|
|
1689
|
+
}
|
|
1690
|
+
default:
|
|
1691
|
+
throw new Error(`Unknown channel: "${name}"`);
|
|
1692
|
+
}
|
|
1693
|
+
} catch (err) {
|
|
1694
|
+
const error = err;
|
|
1695
|
+
if (error.message.startsWith("[@yesvara") || error.message.startsWith("Unknown")) throw error;
|
|
1696
|
+
throw new Error(`[@yesvara/svara] Failed to load channel "${name}": ${error.message}`);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
log(level, msg) {
|
|
1700
|
+
if (level === "error") {
|
|
1701
|
+
console.error(`[@yesvara/svara] ${this.name}: ${msg}`);
|
|
1702
|
+
} else if (this.verbose) {
|
|
1703
|
+
console.log(`[@yesvara/svara] ${this.name}: ${msg}`);
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
// src/tools/index.ts
|
|
1709
|
+
function createTool(definition) {
|
|
1710
|
+
if (!definition.name?.trim()) {
|
|
1711
|
+
throw new Error('[@yesvara/svara] createTool: "name" is required.');
|
|
1712
|
+
}
|
|
1713
|
+
if (!definition.description?.trim()) {
|
|
1714
|
+
throw new Error(`[@yesvara/svara] createTool "${definition.name}": "description" is required.`);
|
|
1715
|
+
}
|
|
1716
|
+
if (typeof definition.run !== "function") {
|
|
1717
|
+
throw new Error(`[@yesvara/svara] createTool "${definition.name}": "run" must be a function.`);
|
|
1718
|
+
}
|
|
1719
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(definition.name)) {
|
|
1720
|
+
throw new Error(
|
|
1721
|
+
`[@yesvara/svara] createTool: Invalid tool name "${definition.name}". Use only letters, numbers, underscores, or hyphens.`
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1724
|
+
return {
|
|
1725
|
+
parameters: {},
|
|
1726
|
+
...definition
|
|
1727
|
+
};
|
|
1728
|
+
}
|
|
1729
|
+
|
|
1730
|
+
// src/database/sqlite.ts
|
|
1731
|
+
var import_path2 = __toESM(require("path"));
|
|
1732
|
+
var import_fs = __toESM(require("fs"));
|
|
1733
|
+
|
|
1734
|
+
// src/database/schema.ts
|
|
1735
|
+
var SCHEMA_VERSION = 1;
|
|
1736
|
+
var CREATE_TABLES_SQL = `
|
|
1737
|
+
-- Schema version tracking
|
|
1738
|
+
CREATE TABLE IF NOT EXISTS svara_meta (
|
|
1739
|
+
key TEXT PRIMARY KEY,
|
|
1740
|
+
value TEXT NOT NULL
|
|
1741
|
+
);
|
|
1742
|
+
|
|
1743
|
+
-- Conversation history persistence
|
|
1744
|
+
CREATE TABLE IF NOT EXISTS svara_messages (
|
|
1745
|
+
id TEXT PRIMARY KEY,
|
|
1746
|
+
session_id TEXT NOT NULL,
|
|
1747
|
+
role TEXT NOT NULL CHECK(role IN ('user', 'assistant', 'system', 'tool')),
|
|
1748
|
+
content TEXT NOT NULL,
|
|
1749
|
+
tool_call_id TEXT,
|
|
1750
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1751
|
+
);
|
|
1752
|
+
|
|
1753
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session
|
|
1754
|
+
ON svara_messages (session_id, created_at);
|
|
1755
|
+
|
|
1756
|
+
-- Session metadata
|
|
1757
|
+
CREATE TABLE IF NOT EXISTS svara_sessions (
|
|
1758
|
+
id TEXT PRIMARY KEY,
|
|
1759
|
+
user_id TEXT,
|
|
1760
|
+
channel TEXT NOT NULL,
|
|
1761
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1762
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1763
|
+
metadata TEXT DEFAULT '{}'
|
|
1764
|
+
);
|
|
1765
|
+
|
|
1766
|
+
-- Vector store chunks for RAG
|
|
1767
|
+
CREATE TABLE IF NOT EXISTS svara_chunks (
|
|
1768
|
+
id TEXT PRIMARY KEY,
|
|
1769
|
+
document_id TEXT NOT NULL,
|
|
1770
|
+
content TEXT NOT NULL,
|
|
1771
|
+
chunk_index INTEGER NOT NULL,
|
|
1772
|
+
embedding BLOB, -- stored as binary float32 array
|
|
1773
|
+
source TEXT NOT NULL,
|
|
1774
|
+
metadata TEXT DEFAULT '{}',
|
|
1775
|
+
created_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1776
|
+
);
|
|
1777
|
+
|
|
1778
|
+
CREATE INDEX IF NOT EXISTS idx_chunks_document
|
|
1779
|
+
ON svara_chunks (document_id);
|
|
1780
|
+
|
|
1781
|
+
-- Document registry
|
|
1782
|
+
CREATE TABLE IF NOT EXISTS svara_documents (
|
|
1783
|
+
id TEXT PRIMARY KEY,
|
|
1784
|
+
source TEXT NOT NULL UNIQUE,
|
|
1785
|
+
type TEXT NOT NULL,
|
|
1786
|
+
size INTEGER,
|
|
1787
|
+
hash TEXT,
|
|
1788
|
+
indexed_at INTEGER NOT NULL DEFAULT (unixepoch()),
|
|
1789
|
+
metadata TEXT DEFAULT '{}'
|
|
1790
|
+
);
|
|
1791
|
+
|
|
1792
|
+
-- Key-value store for arbitrary agent state
|
|
1793
|
+
CREATE TABLE IF NOT EXISTS svara_kv (
|
|
1794
|
+
key TEXT PRIMARY KEY,
|
|
1795
|
+
value TEXT NOT NULL,
|
|
1796
|
+
expires_at INTEGER, -- unix timestamp, NULL = no expiry
|
|
1797
|
+
updated_at INTEGER NOT NULL DEFAULT (unixepoch())
|
|
1798
|
+
);
|
|
1799
|
+
`;
|
|
1800
|
+
var INSERT_META_SQL = `
|
|
1801
|
+
INSERT OR REPLACE INTO svara_meta (key, value)
|
|
1802
|
+
VALUES ('schema_version', ?), ('created_at', ?);
|
|
1803
|
+
`;
|
|
1804
|
+
|
|
1805
|
+
// src/database/sqlite.ts
|
|
1806
|
+
var KVStore = class {
|
|
1807
|
+
constructor(db) {
|
|
1808
|
+
this.db = db;
|
|
1809
|
+
}
|
|
1810
|
+
db;
|
|
1811
|
+
/** Set a key-value pair, with optional TTL in seconds. */
|
|
1812
|
+
set(key, value, ttlSeconds) {
|
|
1813
|
+
const expiresAt = ttlSeconds ? Math.floor(Date.now() / 1e3) + ttlSeconds : null;
|
|
1814
|
+
this.db.prepare(`
|
|
1815
|
+
INSERT OR REPLACE INTO svara_kv (key, value, expires_at, updated_at)
|
|
1816
|
+
VALUES (?, ?, ?, unixepoch())
|
|
1817
|
+
`).run(key, JSON.stringify(value), expiresAt);
|
|
1818
|
+
}
|
|
1819
|
+
/** Get a value by key. Returns undefined if not found or expired. */
|
|
1820
|
+
get(key) {
|
|
1821
|
+
const row = this.db.prepare(`
|
|
1822
|
+
SELECT value, expires_at FROM svara_kv
|
|
1823
|
+
WHERE key = ? AND (expires_at IS NULL OR expires_at > unixepoch())
|
|
1824
|
+
`).get(key);
|
|
1825
|
+
if (!row) return void 0;
|
|
1826
|
+
return JSON.parse(row.value);
|
|
1827
|
+
}
|
|
1828
|
+
/** Delete a key. */
|
|
1829
|
+
delete(key) {
|
|
1830
|
+
this.db.prepare("DELETE FROM svara_kv WHERE key = ?").run(key);
|
|
1831
|
+
}
|
|
1832
|
+
/** Check if a key exists and is not expired. */
|
|
1833
|
+
has(key) {
|
|
1834
|
+
return this.get(key) !== void 0;
|
|
1835
|
+
}
|
|
1836
|
+
/** Get all keys matching a prefix. */
|
|
1837
|
+
keys(prefix = "") {
|
|
1838
|
+
const rows = this.db.prepare(`
|
|
1839
|
+
SELECT key FROM svara_kv
|
|
1840
|
+
WHERE key LIKE ? AND (expires_at IS NULL OR expires_at > unixepoch())
|
|
1841
|
+
`).all(`${prefix}%`);
|
|
1842
|
+
return rows.map((r) => r.key);
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
var SvaraDB = class {
|
|
1846
|
+
db;
|
|
1847
|
+
kv;
|
|
1848
|
+
constructor(dbPath = ":memory:") {
|
|
1849
|
+
if (dbPath !== ":memory:") {
|
|
1850
|
+
import_fs.default.mkdirSync(import_path2.default.dirname(import_path2.default.resolve(dbPath)), { recursive: true });
|
|
1851
|
+
}
|
|
1852
|
+
this.db = this.openDatabase(dbPath);
|
|
1853
|
+
this.configure();
|
|
1854
|
+
this.migrate();
|
|
1855
|
+
this.kv = new KVStore(this.db);
|
|
1856
|
+
}
|
|
1857
|
+
// ─── Query Helpers ────────────────────────────────────────────────────────
|
|
1858
|
+
/**
|
|
1859
|
+
* Run a SELECT and return all matching rows.
|
|
1860
|
+
*/
|
|
1861
|
+
query(sql, params = []) {
|
|
1862
|
+
return this.db.prepare(sql).all(...params);
|
|
1863
|
+
}
|
|
1864
|
+
/**
|
|
1865
|
+
* Run a SELECT and return the first matching row.
|
|
1866
|
+
*/
|
|
1867
|
+
queryOne(sql, params = []) {
|
|
1868
|
+
return this.db.prepare(sql).get(...params);
|
|
1869
|
+
}
|
|
1870
|
+
/**
|
|
1871
|
+
* Run an INSERT/UPDATE/DELETE. Returns affected row count.
|
|
1872
|
+
*/
|
|
1873
|
+
run(sql, params = []) {
|
|
1874
|
+
return this.db.prepare(sql).run(...params).changes;
|
|
1875
|
+
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Execute raw SQL (for DDL, migrations, etc.).
|
|
1878
|
+
*/
|
|
1879
|
+
exec(sql) {
|
|
1880
|
+
this.db.exec(sql);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1883
|
+
* Run multiple operations in a single transaction.
|
|
1884
|
+
*
|
|
1885
|
+
* @example
|
|
1886
|
+
* db.transaction(() => {
|
|
1887
|
+
* db.run('INSERT INTO orders ...', [...]);
|
|
1888
|
+
* db.run('UPDATE inventory ...', [...]);
|
|
1889
|
+
* });
|
|
1890
|
+
*/
|
|
1891
|
+
transaction(fn) {
|
|
1892
|
+
return this.db.transaction(fn)();
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Close the database connection.
|
|
1896
|
+
*/
|
|
1897
|
+
close() {
|
|
1898
|
+
this.db.close();
|
|
1899
|
+
}
|
|
1900
|
+
// ─── Internal Message Storage ─────────────────────────────────────────────
|
|
1901
|
+
saveMessage(params) {
|
|
1902
|
+
this.db.prepare(`
|
|
1903
|
+
INSERT OR REPLACE INTO svara_messages (id, session_id, role, content, tool_call_id)
|
|
1904
|
+
VALUES (?, ?, ?, ?, ?)
|
|
1905
|
+
`).run(
|
|
1906
|
+
params.id,
|
|
1907
|
+
params.sessionId,
|
|
1908
|
+
params.role,
|
|
1909
|
+
params.content,
|
|
1910
|
+
params.toolCallId ?? null
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
getMessages(sessionId, limit = 50) {
|
|
1914
|
+
return this.db.prepare(`
|
|
1915
|
+
SELECT id, role, content, tool_call_id, created_at
|
|
1916
|
+
FROM svara_messages
|
|
1917
|
+
WHERE session_id = ?
|
|
1918
|
+
ORDER BY created_at ASC
|
|
1919
|
+
LIMIT ?
|
|
1920
|
+
`).all(sessionId, limit);
|
|
1921
|
+
}
|
|
1922
|
+
clearSession(sessionId) {
|
|
1923
|
+
this.db.prepare("DELETE FROM svara_messages WHERE session_id = ?").run(sessionId);
|
|
1924
|
+
}
|
|
1925
|
+
// ─── Private Setup ────────────────────────────────────────────────────────
|
|
1926
|
+
openDatabase(dbPath) {
|
|
1927
|
+
try {
|
|
1928
|
+
const Database = require("better-sqlite3");
|
|
1929
|
+
return new Database(dbPath);
|
|
1930
|
+
} catch {
|
|
1931
|
+
throw new Error(
|
|
1932
|
+
'[SvaraJS] Database requires the "better-sqlite3" package.\nRun: npm install better-sqlite3'
|
|
1933
|
+
);
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
configure() {
|
|
1937
|
+
this.db.pragma("journal_mode = WAL");
|
|
1938
|
+
this.db.pragma("synchronous = NORMAL");
|
|
1939
|
+
this.db.pragma("foreign_keys = ON");
|
|
1940
|
+
}
|
|
1941
|
+
migrate() {
|
|
1942
|
+
this.db.exec(CREATE_TABLES_SQL);
|
|
1943
|
+
const meta = this.db.prepare(
|
|
1944
|
+
"SELECT value FROM svara_meta WHERE key = 'schema_version'"
|
|
1945
|
+
).get();
|
|
1946
|
+
if (!meta) {
|
|
1947
|
+
this.db.prepare(INSERT_META_SQL).run(
|
|
1948
|
+
String(SCHEMA_VERSION),
|
|
1949
|
+
(/* @__PURE__ */ new Date()).toISOString()
|
|
1950
|
+
);
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
// src/index.ts
|
|
1956
|
+
init_web();
|
|
1957
|
+
init_telegram();
|
|
1958
|
+
init_whatsapp();
|
|
1959
|
+
init_loader();
|
|
1960
|
+
init_chunker();
|
|
1961
|
+
init_retriever();
|
|
1962
|
+
var VERSION = "0.1.0";
|
|
1963
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1964
|
+
0 && (module.exports = {
|
|
1965
|
+
Chunker,
|
|
1966
|
+
DocumentLoader,
|
|
1967
|
+
SvaraAgent,
|
|
1968
|
+
SvaraApp,
|
|
1969
|
+
SvaraDB,
|
|
1970
|
+
TelegramChannel,
|
|
1971
|
+
VERSION,
|
|
1972
|
+
VectorRetriever,
|
|
1973
|
+
WebChannel,
|
|
1974
|
+
WhatsAppChannel,
|
|
1975
|
+
createTool
|
|
1976
|
+
});
|