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