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 from config
121 client = Letta(token=letta_config['api_key'], timeout=letta_config['timeout'])
122
123 # Get the agent by ID
124 try:
125 agent = client.agents.retrieve(agent_id=agent_id)
126 except Exception as e:
127 console.print(f"[red]Error: Agent '{agent_id}' not found[/red]")
128 console.print(f"Error details: {e}")
129 return
130
131 # Filter tools if specific ones requested
132 tools_to_register = X_TOOL_CONFIGS
133 if tools:
134 tools_to_register = [t for t in X_TOOL_CONFIGS if t["func"] and t["func"].__name__ in tools]
135 if len(tools_to_register) != len(tools):
136 registered_names = {t["func"].__name__ for t in tools_to_register if t["func"]}
137 missing = set(tools) - registered_names
138 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]")
139
140 # Create results table
141 table = Table(title=f"X Tool Registration for Agent '{agent.name}' ({agent_id})")
142 table.add_column("Tool", style="cyan")
143 table.add_column("Status", style="green")
144 table.add_column("Description")
145
146 # Register each tool
147 for tool_config in tools_to_register:
148 func = tool_config["func"]
149 if not func:
150 continue
151
152 tool_name = func.__name__
153
154 try:
155 # Create or update the tool using the standalone function
156 created_tool = client.tools.upsert_from_function(
157 func=func,
158 args_schema=tool_config["args_schema"],
159 tags=tool_config["tags"]
160 )
161
162 # Get current agent tools
163 current_tools = client.agents.tools.list(agent_id=str(agent.id))
164 tool_names = [t.name for t in current_tools]
165
166 # Check if already attached
167 if created_tool.name in tool_names:
168 table.add_row(tool_name, "Already Attached", tool_config["description"])
169 else:
170 # Attach to agent
171 client.agents.tools.attach(
172 agent_id=str(agent.id),
173 tool_id=str(created_tool.id)
174 )
175 table.add_row(tool_name, "✓ Attached", tool_config["description"])
176
177 except Exception as e:
178 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"])
179 logger.error(f"Error registering tool {tool_name}: {e}")
180
181 console.print(table)
182
183 except Exception as e:
184 console.print(f"[red]Error: {str(e)}[/red]")
185 logger.error(f"Fatal error: {e}")
186
187
188def list_available_x_tools():
189 """List all available X tools."""
190 table = Table(title="Available X Tools")
191 table.add_column("Tool Name", style="cyan")
192 table.add_column("Description")
193 table.add_column("Tags", style="dim")
194
195 for tool_config in X_TOOL_CONFIGS:
196 if tool_config["func"]:
197 table.add_row(
198 tool_config["func"].__name__,
199 tool_config["description"],
200 ", ".join(tool_config["tags"])
201 )
202
203 console.print(table)
204
205
206if __name__ == "__main__":
207 import argparse
208
209 parser = argparse.ArgumentParser(description="Register X tools with a Letta agent")
210 parser.add_argument("--agent-id", help=f"Agent ID (default: from config)")
211 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)")
212 parser.add_argument("--list", action="store_true", help="List available tools")
213
214 args = parser.parse_args()
215
216 if args.list:
217 list_available_x_tools()
218 else:
219 # Use config default if no agent specified
220 agent_id = args.agent_id if args.agent_id else letta_config['agent_id']
221 console.print(f"\n[bold]Registering X tools for agent: {agent_id}[/bold]\n")
222 register_x_tools(agent_id, args.tools)