this repo has no description
1/**
2 * Tool dispatcher for Letta agents
3 *
4 * This module provides:
5 * - Type definitions for tool handlers
6 * - Tool registry for managing available tools
7 * - Dispatcher for routing tool calls to appropriate handlers
8 * - Letta-compatible tool definitions for agent creation
9 */
10
11import type { ToolCreateParams } from '@letta-ai/letta-client/resources/tools.js';
12import { config } from '../config';
13
14/**
15 * Context passed to tool handlers
16 */
17export interface ToolContext {
18 /** Telegram user ID of the user invoking the tool */
19 userId: number;
20}
21
22/**
23 * A tool handler function that processes tool calls
24 *
25 * @param args - Tool arguments (validated against JSON schema)
26 * @param context - Context information (user ID, etc.)
27 * @returns Tool result (serializable to JSON)
28 */
29export type ToolHandler<TArgs = unknown, TResult = unknown> = (args: TArgs, context: ToolContext) => Promise<TResult>;
30
31/**
32 * Tool definition for registration
33 */
34export interface ToolDefinition<TArgs = unknown, TResult = unknown> {
35 /** Tool name (must match Letta tool name) */
36 name: string;
37
38 /** Human-readable description of what the tool does */
39 description: string;
40
41 /** JSON Schema for tool parameters */
42 parameters: Record<string, unknown>;
43
44 /** Handler function to execute when tool is called */
45 handler: ToolHandler<TArgs, TResult>;
46}
47
48/**
49 * Registry of available tools
50 */
51class ToolRegistry {
52 // Use unknown for the map to accept any generic parameters
53 private tools = new Map<string, ToolDefinition>();
54
55 /**
56 * Register a tool with the dispatcher
57 *
58 * @param definition - Tool definition including handler
59 */
60 register<TArgs, TResult>(definition: ToolDefinition<TArgs, TResult>): void {
61 if (this.tools.has(definition.name)) {
62 throw new Error(`Tool '${definition.name}' is already registered`);
63 }
64
65 // Cast to unknown to allow any generic parameters
66 this.tools.set(definition.name, definition as ToolDefinition);
67 console.log(`Registered tool: ${definition.name}`);
68 }
69
70 /**
71 * Get a tool by name
72 *
73 * @param name - Tool name
74 * @returns Tool definition or undefined if not found
75 */
76 get(name: string): ToolDefinition | undefined {
77 return this.tools.get(name);
78 }
79
80 /**
81 * Get all registered tools
82 *
83 * @returns Array of all tool definitions
84 */
85 getAll(): ToolDefinition[] {
86 return Array.from(this.tools.values());
87 }
88
89 /**
90 * Check if a tool is registered
91 *
92 * @param name - Tool name
93 * @returns True if tool exists
94 */
95 has(name: string): boolean {
96 return this.tools.has(name);
97 }
98
99 /**
100 * Clear all registered tools (primarily for testing)
101 */
102 clear(): void {
103 this.tools.clear();
104 }
105}
106
107/**
108 * Singleton tool registry instance
109 */
110export const toolRegistry = new ToolRegistry();
111
112/**
113 * Dispatch a tool call to the appropriate handler
114 *
115 * @param name - Tool name
116 * @param args - Tool arguments (should match tool's JSON schema)
117 * @param context - Tool execution context
118 * @returns Tool execution result
119 * @throws Error if tool is not found or execution fails
120 */
121export async function dispatchTool(name: string, args: unknown, context: ToolContext): Promise<unknown> {
122 const tool = toolRegistry.get(name);
123
124 if (!tool) {
125 throw new Error(`Tool '${name}' is not registered`);
126 }
127
128 try {
129 const result = await tool.handler(args, context);
130 return result;
131 } catch (error: unknown) {
132 const errorMessage = error instanceof Error ? error.message : 'Unknown error';
133 throw new Error(`Failed to execute tool '${name}': ${errorMessage}`);
134 }
135}
136
137/**
138 * Convert a tool definition to Letta-compatible format
139 *
140 * This generates the source code and parameters that Letta needs
141 * to register the tool on the Letta server.
142 *
143 * @param definition - Tool definition
144 * @returns Letta tool creation parameters
145 */
146/**
147 * Generate Python function signature from JSON schema parameters
148 */
149function generatePythonParams(parameters: Record<string, unknown>): {
150 signature: string;
151 docParams: string;
152 argsDict: string;
153} {
154 const propsRaw = parameters['properties'];
155 const props = (propsRaw as Record<string, Record<string, unknown>> | undefined) ?? {};
156 const requiredRaw = parameters['required'];
157 const required = (requiredRaw as string[] | undefined) ?? [];
158
159 const params: string[] = [];
160 const docLines: string[] = [];
161 const dictEntries: string[] = [];
162
163 for (const [name, schema] of Object.entries(props)) {
164 const pyType = schema['type'] === 'integer' ? 'int' : schema['type'] === 'boolean' ? 'bool' : 'str';
165 const isRequired = required.includes(name);
166 const descRaw = schema['description'];
167 const desc = typeof descRaw === 'string' ? descRaw : '';
168
169 if (isRequired) {
170 params.push(`${name}: ${pyType}`);
171 } else {
172 params.push(`${name}: ${pyType} = None`);
173 }
174
175 docLines.push(` ${name}: ${desc}`);
176 dictEntries.push(`"${name}": ${name}`);
177 }
178
179 return {
180 signature: params.join(', '),
181 docParams: docLines.length > 0 ? '\n\n Args:\n' + docLines.join('\n') : '',
182 argsDict: dictEntries.join(', '),
183 };
184}
185
186export function toLettaToolCreate(definition: ToolDefinition): ToolCreateParams {
187 // Generate Python source code that Letta can execute
188 // Tools are proxied through a webhook to our Bun handlers
189 const webhookUrl = config.TOOL_WEBHOOK_URL;
190
191 // Generate explicit parameters from JSON schema so Letta knows what args to pass
192 const { signature, docParams, argsDict } = generatePythonParams(definition.parameters);
193
194 const sourceCode = `
195def ${definition.name}(${signature}):
196 """${definition.description}${docParams}
197 """
198 import requests
199
200 webhook_url = "${webhookUrl}"
201 args = {${argsDict}}
202
203 try:
204 response = requests.post(
205 f"{webhook_url}/tools/${definition.name}",
206 json=args,
207 timeout=30
208 )
209 response.raise_for_status()
210 return response.json()
211 except Exception as e:
212 return {"error": str(e)}
213`.trim();
214
215 // Letta's json_schema format: flat object with name, description, parameters
216 // (different from OpenAI's nested {type: 'function', function: {...}} format)
217 return {
218 source_code: sourceCode,
219 description: definition.description,
220 source_type: 'python',
221 pip_requirements: [{ name: 'requests' }],
222 json_schema: {
223 name: definition.name,
224 description: definition.description,
225 parameters: definition.parameters,
226 },
227 };
228}
229
230/**
231 * Get all registered tools in Letta-compatible format
232 *
233 * @returns Array of Letta tool creation parameters
234 */
235export function getAllLettaToolsCreate(): ToolCreateParams[] {
236 return toolRegistry.getAll().map(toLettaToolCreate);
237}
238
239/**
240 * Register a tool and return its definition for chaining
241 *
242 * @param definition - Tool definition
243 * @returns The same tool definition for convenience
244 */
245export function registerTool<TArgs = unknown, TResult = unknown>(
246 definition: ToolDefinition<TArgs, TResult>
247): ToolDefinition<TArgs, TResult> {
248 toolRegistry.register<TArgs, TResult>(definition);
249 return definition;
250}