this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 250 lines 7.1 kB view raw
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}