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