a digital person for bluesky
1#!/usr/bin/env python3 2"""Register X-specific tools with a Letta agent.""" 3import logging 4from typing import List 5from letta_client import Letta 6from rich.console import Console 7from rich.table import Table 8from x import get_x_letta_config 9 10# Import standalone functions and their schemas 11from tools.blocks import ( 12 attach_x_user_blocks, detach_x_user_blocks, 13 AttachXUserBlocksArgs, DetachXUserBlocksArgs 14) 15from tools.halt import halt_activity, HaltArgs 16from tools.ignore import ignore_notification, IgnoreNotificationArgs 17from tools.whitewind import create_whitewind_blog_post, WhitewindPostArgs 18from tools.ack import annotate_ack, AnnotateAckArgs 19from tools.webpage import fetch_webpage, WebpageArgs 20 21# Import X thread tool 22from tools.x_thread import add_post_to_x_thread, XThreadPostArgs 23 24# Import X post tool 25from tools.x_post import post_to_x, PostToXArgs 26 27# Import X search tool 28from tools.search_x import search_x_posts, SearchXArgs 29 30letta_config = get_x_letta_config() 31logging.basicConfig(level=logging.INFO) 32logger = logging.getLogger(__name__) 33console = Console() 34 35# X-specific tool configurations 36X_TOOL_CONFIGS = [ 37 # Keep these core tools 38 { 39 "func": halt_activity, 40 "args_schema": HaltArgs, 41 "description": "Signal to halt all bot activity and terminate X bot", 42 "tags": ["control", "halt", "terminate"] 43 }, 44 { 45 "func": ignore_notification, 46 "args_schema": IgnoreNotificationArgs, 47 "description": "Explicitly ignore an X notification without replying", 48 "tags": ["notification", "ignore", "control", "bot"] 49 }, 50 { 51 "func": annotate_ack, 52 "args_schema": AnnotateAckArgs, 53 "description": "Add a note to the acknowledgment record for the current X interaction", 54 "tags": ["acknowledgment", "note", "annotation", "metadata"] 55 }, 56 { 57 "func": create_whitewind_blog_post, 58 "args_schema": WhitewindPostArgs, 59 "description": "Create a blog post on Whitewind with markdown support", 60 "tags": ["whitewind", "blog", "post", "markdown"] 61 }, 62 { 63 "func": fetch_webpage, 64 "args_schema": WebpageArgs, 65 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader", 66 "tags": ["web", "fetch", "webpage", "markdown", "jina"] 67 }, 68 69 # X user block management tools 70 { 71 "func": attach_x_user_blocks, 72 "args_schema": AttachXUserBlocksArgs, 73 "description": "Attach X user-specific memory blocks to the agent. Creates blocks if they don't exist.", 74 "tags": ["memory", "blocks", "user", "x", "twitter"] 75 }, 76 { 77 "func": detach_x_user_blocks, 78 "args_schema": DetachXUserBlocksArgs, 79 "description": "Detach X user-specific memory blocks from the agent. Blocks are preserved for later use.", 80 "tags": ["memory", "blocks", "user", "x", "twitter"] 81 }, 82 83 # X thread tool 84 { 85 "func": add_post_to_x_thread, 86 "args_schema": XThreadPostArgs, 87 "description": "Add a single post to the current X reply thread atomically", 88 "tags": ["x", "twitter", "reply", "thread", "atomic"] 89 }, 90 91 # X post tool 92 { 93 "func": post_to_x, 94 "args_schema": PostToXArgs, 95 "description": "Create a new standalone post on X (Twitter)", 96 "tags": ["x", "twitter", "post", "create", "standalone"] 97 }, 98 99 # X search tool 100 { 101 "func": search_x_posts, 102 "args_schema": SearchXArgs, 103 "description": "Get recent posts from a specific X (Twitter) user", 104 "tags": ["x", "twitter", "search", "posts", "user"] 105 } 106] 107 108def register_x_tools(agent_id: str = None, tools: List[str] = None): 109 """Register X-specific tools with a Letta agent. 110 111 Args: 112 agent_id: ID of the agent to attach tools to. If None, uses config default. 113 tools: List of tool names to register. If None, registers all tools. 114 """ 115 # Use agent ID from config if not provided 116 if agent_id is None: 117 agent_id = letta_config['agent_id'] 118 119 try: 120 # Initialize Letta client with API key and base_url from config 121 client_params = { 122 'token': letta_config['api_key'], 123 'timeout': letta_config['timeout'] 124 } 125 if letta_config.get('base_url'): 126 client_params['base_url'] = letta_config['base_url'] 127 client = Letta(**client_params) 128 129 # Get the agent by ID 130 try: 131 agent = client.agents.retrieve(agent_id=agent_id) 132 except Exception as e: 133 console.print(f"[red]Error: Agent '{agent_id}' not found[/red]") 134 console.print(f"Error details: {e}") 135 return 136 137 # Filter tools if specific ones requested 138 tools_to_register = X_TOOL_CONFIGS 139 if tools: 140 tools_to_register = [t for t in X_TOOL_CONFIGS if t["func"] and t["func"].__name__ in tools] 141 if len(tools_to_register) != len(tools): 142 registered_names = {t["func"].__name__ for t in tools_to_register if t["func"]} 143 missing = set(tools) - registered_names 144 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]") 145 146 # Create results table 147 table = Table(title=f"X Tool Registration for Agent '{agent.name}' ({agent_id})") 148 table.add_column("Tool", style="cyan") 149 table.add_column("Status", style="green") 150 table.add_column("Description") 151 152 # Register each tool 153 for tool_config in tools_to_register: 154 func = tool_config["func"] 155 if not func: 156 continue 157 158 tool_name = func.__name__ 159 160 try: 161 # Create or update the tool using the standalone function 162 created_tool = client.tools.upsert_from_function( 163 func=func, 164 args_schema=tool_config["args_schema"], 165 tags=tool_config["tags"] 166 ) 167 168 # Get current agent tools 169 current_tools = client.agents.tools.list(agent_id=str(agent.id)) 170 tool_names = [t.name for t in current_tools] 171 172 # Check if already attached 173 if created_tool.name in tool_names: 174 table.add_row(tool_name, "Already Attached", tool_config["description"]) 175 else: 176 # Attach to agent 177 client.agents.tools.attach( 178 agent_id=str(agent.id), 179 tool_id=str(created_tool.id) 180 ) 181 table.add_row(tool_name, "✓ Attached", tool_config["description"]) 182 183 except Exception as e: 184 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"]) 185 logger.error(f"Error registering tool {tool_name}: {e}") 186 187 console.print(table) 188 189 except Exception as e: 190 console.print(f"[red]Error: {str(e)}[/red]") 191 logger.error(f"Fatal error: {e}") 192 193 194def list_available_x_tools(): 195 """List all available X tools.""" 196 table = Table(title="Available X Tools") 197 table.add_column("Tool Name", style="cyan") 198 table.add_column("Description") 199 table.add_column("Tags", style="dim") 200 201 for tool_config in X_TOOL_CONFIGS: 202 if tool_config["func"]: 203 table.add_row( 204 tool_config["func"].__name__, 205 tool_config["description"], 206 ", ".join(tool_config["tags"]) 207 ) 208 209 console.print(table) 210 211 212if __name__ == "__main__": 213 import argparse 214 215 parser = argparse.ArgumentParser(description="Register X tools with a Letta agent") 216 parser.add_argument("--agent-id", help=f"Agent ID (default: from config)") 217 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)") 218 parser.add_argument("--list", action="store_true", help="List available tools") 219 220 args = parser.parse_args() 221 222 if args.list: 223 list_available_x_tools() 224 else: 225 # Use config default if no agent specified 226 agent_id = args.agent_id if args.agent_id else letta_config['agent_id'] 227 console.print(f"\n[bold]Registering X tools for agent: {agent_id}[/bold]\n") 228 register_x_tools(agent_id, args.tools)