@yesvara/svara 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +497 -0
  3. package/dist/chunk-CIESM3BP.mjs +33 -0
  4. package/dist/chunk-FEA5KIJN.mjs +418 -0
  5. package/dist/cli/index.d.mts +1 -0
  6. package/dist/cli/index.d.ts +1 -0
  7. package/dist/cli/index.js +328 -0
  8. package/dist/cli/index.mjs +39 -0
  9. package/dist/dev-OYGXXK2B.mjs +69 -0
  10. package/dist/index.d.mts +967 -0
  11. package/dist/index.d.ts +967 -0
  12. package/dist/index.js +1976 -0
  13. package/dist/index.mjs +1502 -0
  14. package/dist/new-7K4NIDZO.mjs +177 -0
  15. package/dist/retriever-4QY667XF.mjs +7 -0
  16. package/examples/01-basic/index.ts +26 -0
  17. package/examples/02-with-tools/index.ts +73 -0
  18. package/examples/03-rag-knowledge/index.ts +41 -0
  19. package/examples/04-multi-channel/index.ts +91 -0
  20. package/package.json +74 -0
  21. package/src/app/index.ts +176 -0
  22. package/src/channels/telegram.ts +122 -0
  23. package/src/channels/web.ts +118 -0
  24. package/src/channels/whatsapp.ts +161 -0
  25. package/src/cli/commands/dev.ts +87 -0
  26. package/src/cli/commands/new.ts +213 -0
  27. package/src/cli/index.ts +78 -0
  28. package/src/core/agent.ts +607 -0
  29. package/src/core/llm.ts +406 -0
  30. package/src/core/types.ts +183 -0
  31. package/src/database/schema.ts +79 -0
  32. package/src/database/sqlite.ts +239 -0
  33. package/src/index.ts +94 -0
  34. package/src/memory/context.ts +49 -0
  35. package/src/memory/conversation.ts +51 -0
  36. package/src/rag/chunker.ts +165 -0
  37. package/src/rag/loader.ts +216 -0
  38. package/src/rag/retriever.ts +248 -0
  39. package/src/tools/executor.ts +54 -0
  40. package/src/tools/index.ts +89 -0
  41. package/src/tools/registry.ts +44 -0
  42. package/src/types.ts +131 -0
  43. package/tsconfig.json +26 -0
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SvaraJS CLI
4
+ *
5
+ * Usage:
6
+ * svara new <name> Create a new project
7
+ * svara dev Start dev server with hot-reload
8
+ * svara build Compile TypeScript to JavaScript
9
+ * svara --version Show version
10
+ * svara --help Show help
11
+ */
12
+
13
+ import { Command } from 'commander';
14
+ import { createRequire } from 'module';
15
+
16
+ const require = createRequire(import.meta.url);
17
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
18
+ const pkg = require('../../package.json') as { version: string; description: string };
19
+
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('svara')
24
+ .description(pkg.description)
25
+ .version(pkg.version, '-v, --version');
26
+
27
+ // ── svara new <name> ──────────────────────────────────────────────────────────
28
+ program
29
+ .command('new <name>')
30
+ .description('Create a new SvaraJS project')
31
+ .option('--provider <provider>', 'LLM provider (openai|anthropic|ollama)', 'openai')
32
+ .option('--channel <channels...>', 'Channels to include', ['web'])
33
+ .option('--no-install', 'Skip npm install')
34
+ .action(async (name: string, opts: { provider: string; channel: string[]; install: boolean }) => {
35
+ const { newProject } = await import('./commands/new.js');
36
+ await newProject({
37
+ name,
38
+ provider: opts.provider as 'openai' | 'anthropic' | 'ollama',
39
+ channels: opts.channel,
40
+ installDeps: opts.install,
41
+ });
42
+ });
43
+
44
+ // ── svara dev ─────────────────────────────────────────────────────────────────
45
+ program
46
+ .command('dev')
47
+ .description('Start development server with hot-reload')
48
+ .option('--entry <file>', 'Entry file', 'src/index.ts')
49
+ .option('--port <port>', 'Override PORT env variable')
50
+ .action(async (opts: { entry: string; port?: string }) => {
51
+ const { devServer } = await import('./commands/dev.js');
52
+ await devServer({
53
+ entry: opts.entry,
54
+ port: opts.port ? parseInt(opts.port, 10) : undefined,
55
+ });
56
+ });
57
+
58
+ // ── svara build ───────────────────────────────────────────────────────────────
59
+ program
60
+ .command('build')
61
+ .description('Compile TypeScript to JavaScript')
62
+ .action(async () => {
63
+ const { execSync } = await import('child_process');
64
+ console.log('🔨 Building...');
65
+ try {
66
+ execSync('npx tsc', { stdio: 'inherit' });
67
+ console.log('✅ Build complete → dist/');
68
+ } catch {
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+ program.parse(process.argv);
74
+
75
+ // Show help if no command given
76
+ if (!process.argv.slice(2).length) {
77
+ program.outputHelp();
78
+ }
@@ -0,0 +1,607 @@
1
+ /**
2
+ * @module SvaraAgent
3
+ *
4
+ * The heart of the framework. One class. Infinite possibilities.
5
+ *
6
+ * A SvaraAgent is a stateful AI agent that:
7
+ * - Holds a conversation across multiple turns (memory)
8
+ * - Can search your documents to answer questions (RAG)
9
+ * - Can call functions you define (tools)
10
+ * - Can receive messages from any channel (WhatsApp, Telegram, Web, etc.)
11
+ *
12
+ * @example Minimal — works in 5 lines
13
+ * ```ts
14
+ * const agent = new SvaraAgent({ name: 'Aria', model: 'gpt-4o' });
15
+ * const reply = await agent.chat('What is the capital of France?');
16
+ * console.log(reply); // "Paris"
17
+ * ```
18
+ *
19
+ * @example Full — production-ready bot
20
+ * ```ts
21
+ * const agent = new SvaraAgent({
22
+ * name: 'Support Bot',
23
+ * model: 'gpt-4o-mini',
24
+ * systemPrompt: 'You are a helpful support agent.',
25
+ * knowledge: './docs',
26
+ * memory: { window: 20 },
27
+ * });
28
+ *
29
+ * agent
30
+ * .addTool(emailTool)
31
+ * .addTool(databaseTool)
32
+ * .connectChannel('telegram', { token: process.env.TG_TOKEN });
33
+ *
34
+ * await agent.start();
35
+ * ```
36
+ */
37
+
38
+ import EventEmitter from 'events';
39
+ import type { RequestHandler } from 'express';
40
+ import { createAdapter, resolveConfig, type LLMAdapter } from './llm.js';
41
+ import type {
42
+ LLMConfig,
43
+ LLMMessage,
44
+ InternalTool,
45
+ InternalAgentContext,
46
+ AgentRunResult,
47
+ AgentRunOptions,
48
+ IncomingMessage,
49
+ ChannelName,
50
+ TokenUsage,
51
+ RAGRetriever,
52
+ } from './types.js';
53
+ import { ConversationMemory } from '../memory/conversation.js';
54
+ import { ContextBuilder } from '../memory/context.js';
55
+ import { ToolRegistry } from '../tools/registry.js';
56
+ import { ToolExecutor } from '../tools/executor.js';
57
+ import type { Tool } from '../types.js';
58
+
59
+ // ─── Channel Interface (implemented in channels/) ─────────────────────────────
60
+
61
+ export interface SvaraChannel {
62
+ readonly name: ChannelName;
63
+ mount(agent: SvaraAgent): Promise<void>;
64
+ send(sessionId: string, text: string): Promise<void>;
65
+ stop(): Promise<void>;
66
+ }
67
+
68
+ // ─── RAG Interface (implemented in rag/) ─────────────────────────────────────
69
+
70
+ export interface KnowledgeBase {
71
+ load(paths: string | string[]): Promise<void>;
72
+ retrieve(query: string, topK?: number): Promise<string>;
73
+ }
74
+
75
+ // ─── Config ──────────────────────────────────────────────────────────────────
76
+
77
+ export interface AgentConfig {
78
+ /**
79
+ * Display name for this agent.
80
+ * Used in logs, system prompts, and the CLI.
81
+ */
82
+ name: string;
83
+
84
+ /**
85
+ * LLM model to use. Provider is auto-detected from the name.
86
+ *
87
+ * @example 'gpt-4o' — OpenAI (needs OPENAI_API_KEY)
88
+ * @example 'claude-opus-4-6' — Anthropic (needs ANTHROPIC_API_KEY)
89
+ * @example 'llama3' — Ollama (local, needs Ollama running)
90
+ * @example 'gpt-4o-mini' — OpenAI (cheaper, faster)
91
+ */
92
+ model: string;
93
+
94
+ /**
95
+ * Instruction that shapes the agent's personality and behavior.
96
+ * If omitted, a sensible default is used based on `name`.
97
+ */
98
+ systemPrompt?: string;
99
+
100
+ /**
101
+ * Path(s) to your documents for RAG (Retrieval Augmented Generation).
102
+ * Supports PDF, Markdown, TXT, DOCX, HTML, JSON. Glob patterns welcome.
103
+ *
104
+ * @example './docs'
105
+ * @example ['./faqs.pdf', './policies/*.md']
106
+ */
107
+ knowledge?: string | string[];
108
+
109
+ /**
110
+ * Conversation memory configuration.
111
+ * - `true` — enable with defaults (20 message window)
112
+ * - `false` — disable (stateless, every call is fresh)
113
+ * - object — custom configuration
114
+ *
115
+ * @default true
116
+ */
117
+ memory?: boolean | { window?: number };
118
+
119
+ /**
120
+ * Tools (function calls) the agent can use.
121
+ * Can also add tools later with `agent.addTool()`.
122
+ */
123
+ tools?: Tool[];
124
+
125
+ /**
126
+ * LLM temperature — controls creativity vs. precision.
127
+ * 0 = deterministic, 2 = very creative. Default: 0.7
128
+ */
129
+ temperature?: number;
130
+
131
+ /**
132
+ * Max output tokens per LLM call. Default: provider-dependent.
133
+ */
134
+ maxTokens?: number;
135
+
136
+ /**
137
+ * Maximum agentic loop iterations (tool calls) per message.
138
+ * Prevents infinite loops. Default: 10
139
+ */
140
+ maxIterations?: number;
141
+
142
+ /**
143
+ * Advanced: override LLM provider or add custom endpoint.
144
+ * Usually not needed — `model` auto-detects the provider.
145
+ */
146
+ llm?: Partial<LLMConfig>;
147
+
148
+ /**
149
+ * Print detailed logs of every LLM call, tool execution, and memory operation.
150
+ * Useful during development. Default: false
151
+ */
152
+ verbose?: boolean;
153
+ }
154
+
155
+ // ─── SvaraAgent ──────────────────────────────────────────────────────────────
156
+
157
+ export class SvaraAgent extends EventEmitter {
158
+ readonly name: string;
159
+
160
+ private readonly llmConfig: LLMConfig;
161
+ private readonly llm: LLMAdapter;
162
+ private readonly systemPrompt: string;
163
+ private readonly tools: ToolRegistry;
164
+ private readonly executor: ToolExecutor;
165
+ private readonly memory: ConversationMemory;
166
+ private readonly context: ContextBuilder;
167
+ private readonly maxIterations: number;
168
+ private readonly verbose: boolean;
169
+
170
+ private channels: Map<ChannelName, SvaraChannel> = new Map();
171
+ private knowledgeBase: KnowledgeBase | null = null;
172
+ private knowledgePaths: string[] = [];
173
+ private isStarted = false;
174
+
175
+ constructor(config: AgentConfig) {
176
+ super();
177
+
178
+ this.name = config.name;
179
+ this.maxIterations = config.maxIterations ?? 10;
180
+ this.verbose = config.verbose ?? false;
181
+
182
+ this.systemPrompt = config.systemPrompt
183
+ ?? `You are ${config.name}, a helpful and friendly AI assistant. Be concise and accurate.`;
184
+
185
+ // Resolve LLM config from model name
186
+ this.llmConfig = resolveConfig(config.model, {
187
+ temperature: config.temperature,
188
+ maxTokens: config.maxTokens,
189
+ ...config.llm,
190
+ });
191
+ this.llm = createAdapter(this.llmConfig);
192
+
193
+ // Memory
194
+ const memCfg = config.memory ?? true;
195
+ const window = memCfg === false ? 0 : (typeof memCfg === 'object' ? (memCfg.window ?? 20) : 20);
196
+ this.memory = new ConversationMemory({ type: 'conversation', maxMessages: window });
197
+
198
+ this.context = new ContextBuilder(this.llm);
199
+ this.tools = new ToolRegistry();
200
+ this.executor = new ToolExecutor(this.tools);
201
+
202
+ // Register initial tools
203
+ config.tools?.forEach((t) => this.addTool(t));
204
+
205
+ // Store knowledge paths for lazy initialization
206
+ if (config.knowledge) {
207
+ this.knowledgePaths = Array.isArray(config.knowledge)
208
+ ? config.knowledge
209
+ : [config.knowledge];
210
+ }
211
+ }
212
+
213
+ // ─── Public API ────────────────────────────────────────────────────────────
214
+
215
+ /**
216
+ * Send a message and get a reply. The simplest way to use an agent.
217
+ *
218
+ * @example
219
+ * const reply = await agent.chat('What is the weather in Tokyo?');
220
+ * console.log(reply); // "Currently 28°C and sunny in Tokyo."
221
+ *
222
+ * @param message The user's message.
223
+ * @param sessionId Optional session ID for multi-turn conversations.
224
+ * Defaults to 'default' — all calls share one history.
225
+ */
226
+ async chat(message: string, sessionId = 'default'): Promise<string> {
227
+ const result = await this.run(message, { sessionId });
228
+ return result.response;
229
+ }
230
+
231
+ /**
232
+ * Process a message and get the full result with metadata.
233
+ * Use this when you need usage stats, tool info, or session details.
234
+ *
235
+ * @example
236
+ * const result = await agent.process('Summarize my report', {
237
+ * sessionId: 'user-42',
238
+ * userId: 'alice@example.com',
239
+ * });
240
+ * console.log(result.response); // The agent's reply
241
+ * console.log(result.toolsUsed); // ['read_file', 'summarize']
242
+ * console.log(result.usage); // { totalTokens: 1234, ... }
243
+ */
244
+ async process(message: string, options?: AgentRunOptions): Promise<AgentRunResult> {
245
+ return this.run(message, options ?? {});
246
+ }
247
+
248
+ /**
249
+ * Register a tool the agent can call during a conversation.
250
+ * Returns `this` for chaining.
251
+ *
252
+ * @example
253
+ * agent
254
+ * .addTool(weatherTool)
255
+ * .addTool(emailTool)
256
+ * .addTool(databaseTool);
257
+ */
258
+ addTool(tool: Tool): this {
259
+ // Map public Tool to internal format
260
+ const internal: InternalTool = {
261
+ name: tool.name,
262
+ description: tool.description,
263
+ parameters: tool.parameters ?? {},
264
+ run: tool.run,
265
+ category: tool.category,
266
+ timeout: tool.timeout,
267
+ };
268
+ this.tools.register(internal);
269
+ return this;
270
+ }
271
+
272
+ /**
273
+ * Connect a messaging channel. The agent will receive and respond to
274
+ * messages from this channel automatically.
275
+ *
276
+ * @example
277
+ * agent.connectChannel('telegram', { token: process.env.TG_TOKEN });
278
+ * agent.connectChannel('whatsapp', {
279
+ * token: process.env.WA_TOKEN,
280
+ * phoneId: process.env.WA_PHONE_ID,
281
+ * verifyToken: process.env.WA_VERIFY_TOKEN,
282
+ * });
283
+ */
284
+ connectChannel(name: ChannelName, config: Record<string, unknown>): this {
285
+ const channel = this.loadChannel(name, config);
286
+ this.channels.set(name, channel);
287
+ return this;
288
+ }
289
+
290
+ /**
291
+ * Returns an Express request handler for mounting on any HTTP server.
292
+ * POST body: `{ message: string, sessionId?: string, userId?: string }`
293
+ *
294
+ * @example With SvaraApp
295
+ * app.route('/chat', agent.handler());
296
+ *
297
+ * @example With existing Express app
298
+ * expressApp.post('/api/chat', agent.handler());
299
+ */
300
+ handler(): RequestHandler {
301
+ return async (req, res) => {
302
+ const { message, sessionId, userId } = req.body as {
303
+ message?: string;
304
+ sessionId?: string;
305
+ userId?: string;
306
+ };
307
+
308
+ if (!message?.trim()) {
309
+ res.status(400).json({
310
+ error: 'Bad Request',
311
+ message: 'Request body must include a non-empty "message" field.',
312
+ });
313
+ return;
314
+ }
315
+
316
+ try {
317
+ const result = await this.run(message, {
318
+ sessionId: sessionId ?? req.headers['x-session-id'] as string,
319
+ userId,
320
+ });
321
+
322
+ res.json({
323
+ response: result.response,
324
+ sessionId: result.sessionId,
325
+ usage: result.usage,
326
+ toolsUsed: result.toolsUsed,
327
+ });
328
+ } catch (err) {
329
+ const error = err as Error;
330
+ this.log('error', error.message);
331
+ res.status(500).json({ error: 'Internal Server Error', message: error.message });
332
+ }
333
+ };
334
+ }
335
+
336
+ /**
337
+ * Initialize all channels and knowledge base, then start listening.
338
+ * Call this once after you've configured the agent.
339
+ *
340
+ * @example
341
+ * agent.connectChannel('web', { port: 3000 });
342
+ * await agent.start(); // "Web channel running at http://localhost:3000"
343
+ */
344
+ async start(): Promise<void> {
345
+ if (this.isStarted) {
346
+ console.warn(`[@yesvara/svara] ${this.name} is already running.`);
347
+ return;
348
+ }
349
+
350
+ // Init knowledge base
351
+ if (this.knowledgePaths.length) {
352
+ await this.initKnowledge(this.knowledgePaths);
353
+ }
354
+
355
+ // Mount all channels
356
+ for (const [name, channel] of this.channels) {
357
+ await channel.mount(this);
358
+ this.log('info', `Channel "${name}" connected.`);
359
+ this.emit('channel:ready', { channel: name });
360
+ }
361
+
362
+ this.isStarted = true;
363
+
364
+ if (this.channels.size === 0) {
365
+ console.warn(
366
+ `[@yesvara/svara] ${this.name} has no channels configured.\n` +
367
+ ` Add one: agent.connectChannel('web', { port: 3000 })`
368
+ );
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Gracefully shut down all channels.
374
+ */
375
+ async stop(): Promise<void> {
376
+ for (const [, channel] of this.channels) {
377
+ await channel.stop();
378
+ }
379
+ this.isStarted = false;
380
+ this.emit('stopped');
381
+ }
382
+
383
+ /**
384
+ * Clear conversation history for a session.
385
+ *
386
+ * @example
387
+ * agent.on('user:leave', (userId) => agent.clearHistory(userId));
388
+ */
389
+ async clearHistory(sessionId: string): Promise<void> {
390
+ await this.memory.clear(sessionId);
391
+ }
392
+
393
+ /**
394
+ * Add documents to the knowledge base at runtime (no restart needed).
395
+ *
396
+ * @example
397
+ * agent.addKnowledge('./new-policies.pdf');
398
+ */
399
+ async addKnowledge(paths: string | string[]): Promise<void> {
400
+ const arr = Array.isArray(paths) ? paths : [paths];
401
+ if (!this.knowledgeBase) {
402
+ await this.initKnowledge(arr);
403
+ } else {
404
+ await this.knowledgeBase.load(arr);
405
+ }
406
+ }
407
+
408
+ // ─── Internal: Agentic Loop ───────────────────────────────────────────────
409
+
410
+ /**
411
+ * Receives a raw incoming message from a channel and processes it.
412
+ * Called by channel handlers — not typically used directly.
413
+ */
414
+ async receive(msg: IncomingMessage): Promise<AgentRunResult> {
415
+ return this.run(msg.text, {
416
+ sessionId: msg.sessionId,
417
+ userId: msg.userId,
418
+ });
419
+ }
420
+
421
+ private async run(message: string, options: AgentRunOptions): Promise<AgentRunResult> {
422
+ const startTime = Date.now();
423
+ const sessionId = options.sessionId ?? crypto.randomUUID();
424
+
425
+ this.emit('message:received', { message, sessionId, userId: options.userId });
426
+
427
+ // Build LLM message history
428
+ const history = await this.memory.getHistory(sessionId);
429
+
430
+ // RAG retrieval
431
+ let ragContext = '';
432
+ if (this.knowledgeBase) {
433
+ ragContext = await this.knowledgeBase.retrieve(message);
434
+ }
435
+
436
+ const messages = this.context.buildMessages(
437
+ this.systemPrompt,
438
+ history,
439
+ message,
440
+ ragContext
441
+ );
442
+
443
+ const internalCtx: InternalAgentContext = {
444
+ sessionId,
445
+ userId: options.userId ?? 'unknown',
446
+ agentName: this.name,
447
+ history,
448
+ metadata: options.metadata ?? {},
449
+ };
450
+
451
+ // ── Agentic Loop ──────────────────────────────────────────────────────
452
+ const toolsUsed: string[] = [];
453
+ const totalUsage: TokenUsage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
454
+ let iterations = 0;
455
+ let finalResponse = '';
456
+
457
+ while (iterations < this.maxIterations) {
458
+ iterations++;
459
+ this.log('debug', `Iteration ${iterations}`);
460
+
461
+ const allTools = this.tools.getAll();
462
+ const llmResponse = await this.llm.chat(messages, allTools, this.llmConfig.temperature);
463
+
464
+ totalUsage.promptTokens += llmResponse.usage.promptTokens;
465
+ totalUsage.completionTokens += llmResponse.usage.completionTokens;
466
+ totalUsage.totalTokens += llmResponse.usage.totalTokens;
467
+
468
+ // No tool calls — agent has a final answer
469
+ if (!llmResponse.toolCalls?.length) {
470
+ finalResponse = llmResponse.content;
471
+ messages.push({ role: 'assistant', content: finalResponse });
472
+ break;
473
+ }
474
+
475
+ // Append assistant message (with tool calls) to context
476
+ messages.push({
477
+ role: 'assistant',
478
+ content: llmResponse.content,
479
+ toolCalls: llmResponse.toolCalls,
480
+ });
481
+
482
+ this.emit('tool:call', {
483
+ sessionId,
484
+ tools: llmResponse.toolCalls.map((tc) => tc.name),
485
+ });
486
+
487
+ // Execute all tool calls concurrently
488
+ const results = await this.executor.executeAll(llmResponse.toolCalls, internalCtx);
489
+
490
+ for (const result of results) {
491
+ toolsUsed.push(result.name);
492
+ const content = result.error
493
+ ? `Error executing ${result.name}: ${result.error}`
494
+ : JSON.stringify(result.result, null, 2);
495
+
496
+ messages.push({
497
+ role: 'tool',
498
+ content,
499
+ toolCallId: result.toolCallId,
500
+ name: result.name,
501
+ });
502
+
503
+ this.emit('tool:result', { sessionId, name: result.name, result: result.result });
504
+ }
505
+ }
506
+
507
+ if (!finalResponse) {
508
+ finalResponse = `I've reached the reasoning limit for this request. Please try a simpler question.`;
509
+ }
510
+
511
+ // Persist to memory
512
+ await this.memory.append(sessionId, [
513
+ { role: 'user', content: message },
514
+ { role: 'assistant', content: finalResponse },
515
+ ]);
516
+
517
+ const result: AgentRunResult = {
518
+ response: finalResponse,
519
+ sessionId,
520
+ toolsUsed: [...new Set(toolsUsed)],
521
+ iterations,
522
+ usage: totalUsage,
523
+ duration: Date.now() - startTime,
524
+ };
525
+
526
+ this.emit('message:sent', { response: finalResponse, sessionId });
527
+ return result;
528
+ }
529
+
530
+ // ─── Private Helpers ──────────────────────────────────────────────────────
531
+
532
+ private async initKnowledge(paths: string[]): Promise<void> {
533
+ try {
534
+ const { glob } = await import('glob');
535
+ const { VectorRetriever } = await import('../rag/retriever.js');
536
+
537
+ const retriever = new VectorRetriever();
538
+ await retriever.init({ embeddings: { provider: 'openai' } });
539
+
540
+ const files: string[] = [];
541
+ for (const pattern of paths) {
542
+ const matches = await glob(pattern);
543
+ files.push(...matches);
544
+ }
545
+
546
+ if (files.length === 0) {
547
+ console.warn(`[@yesvara/svara] No files found matching: ${paths.join(', ')}`);
548
+ return;
549
+ }
550
+
551
+ await retriever.addDocuments(files);
552
+ this.knowledgeBase = {
553
+ load: async (p) => {
554
+ const newFiles: string[] = [];
555
+ for (const pattern of (Array.isArray(p) ? p : [p])) {
556
+ newFiles.push(...await glob(pattern));
557
+ }
558
+ await retriever.addDocuments(newFiles);
559
+ },
560
+ retrieve: (query, topK) => retriever.retrieve(query, topK),
561
+ };
562
+
563
+ this.log('info', `Knowledge base loaded: ${files.length} file(s).`);
564
+ } catch (err) {
565
+ console.warn(`[@yesvara/svara] Knowledge base init failed: ${(err as Error).message}`);
566
+ }
567
+ }
568
+
569
+ private loadChannel(name: ChannelName, config: Record<string, unknown>): SvaraChannel {
570
+ try {
571
+ switch (name) {
572
+ case 'web': {
573
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
574
+ const { WebChannel } = require('../channels/web.js') as { WebChannel: new (c: unknown) => SvaraChannel };
575
+ return new WebChannel(config);
576
+ }
577
+ case 'telegram': {
578
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
579
+ const { TelegramChannel } = require('../channels/telegram.js') as { TelegramChannel: new (c: unknown) => SvaraChannel };
580
+ return new TelegramChannel(config);
581
+ }
582
+ case 'whatsapp': {
583
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
584
+ const { WhatsAppChannel } = require('../channels/whatsapp.js') as { WhatsAppChannel: new (c: unknown) => SvaraChannel };
585
+ return new WhatsAppChannel(config);
586
+ }
587
+ default:
588
+ throw new Error(`Unknown channel: "${name as string}"`);
589
+ }
590
+ } catch (err) {
591
+ const error = err as Error;
592
+ if (error.message.startsWith('[@yesvara') || error.message.startsWith('Unknown')) throw error;
593
+ throw new Error(`[@yesvara/svara] Failed to load channel "${name}": ${error.message}`);
594
+ }
595
+ }
596
+
597
+ private log(level: 'info' | 'debug' | 'error', msg: string): void {
598
+ if (level === 'error') {
599
+ console.error(`[@yesvara/svara] ${this.name}: ${msg}`);
600
+ } else if (this.verbose) {
601
+ console.log(`[@yesvara/svara] ${this.name}: ${msg}`);
602
+ }
603
+ }
604
+ }
605
+
606
+ // Export RAGRetriever interface
607
+ export type { RAGRetriever };