@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/src/cli/index.ts
ADDED
|
@@ -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 };
|