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)