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