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)