a digital person for bluesky
1#!/usr/bin/env python3
2"""Register all Void tools with a Letta agent."""
3import os
4import sys
5import logging
6from typing import List
7from letta_client import Letta
8from rich.console import Console
9from rich.table import Table
10from config_loader import get_letta_config, get_bluesky_config, get_config
11
12# Import standalone functions and their schemas
13from tools.search import search_bluesky_posts, SearchArgs
14from tools.post import create_new_bluesky_post, PostArgs
15from tools.feed import get_bluesky_feed, FeedArgs
16from tools.halt import halt_activity, HaltArgs
17from tools.thread import add_post_to_bluesky_reply_thread, ReplyThreadPostArgs
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
22from tools.flag_memory_deletion import flag_archival_memory_for_deletion, FlagArchivalMemoryForDeletionArgs
23
24logging.basicConfig(level=logging.INFO)
25logger = logging.getLogger(__name__)
26console = Console()
27
28
29# Tool configurations: function paired with its args_schema and metadata
30TOOL_CONFIGS = [
31 {
32 "func": search_bluesky_posts,
33 "args_schema": SearchArgs,
34 "description": "Search for posts on Bluesky matching the given criteria",
35 "tags": ["bluesky", "search", "posts"]
36 },
37 {
38 "func": create_new_bluesky_post,
39 "args_schema": PostArgs,
40 "description": "Create a new Bluesky post or thread",
41 "tags": ["bluesky", "post", "create", "thread"]
42 },
43 {
44 "func": get_bluesky_feed,
45 "args_schema": FeedArgs,
46 "description": "Retrieve a Bluesky feed (home timeline or custom feed)",
47 "tags": ["bluesky", "feed", "timeline"]
48 },
49 # Note: User block management tools (attach_user_blocks, detach_user_blocks, user_note_*)
50 # are available on the server but not exposed to the agent to prevent the agent from
51 # managing its own memory blocks. User blocks are managed by bsky.py automatically.
52 {
53 "func": halt_activity,
54 "args_schema": HaltArgs,
55 "description": "Signal to halt all bot activity and terminate bsky.py",
56 "tags": ["control", "halt", "terminate"]
57 },
58 {
59 "func": add_post_to_bluesky_reply_thread,
60 "args_schema": ReplyThreadPostArgs,
61 "description": "Add a single post to the current Bluesky reply thread atomically",
62 "tags": ["bluesky", "reply", "thread", "atomic"]
63 },
64 {
65 "func": ignore_notification,
66 "args_schema": IgnoreNotificationArgs,
67 "description": "Explicitly ignore a notification without replying (useful for ignoring bot interactions)",
68 "tags": ["notification", "ignore", "control", "bot"]
69 },
70 {
71 "func": create_whitewind_blog_post,
72 "args_schema": WhitewindPostArgs,
73 "description": "Create a blog post on Whitewind with markdown support",
74 "tags": ["whitewind", "blog", "post", "markdown"]
75 },
76 {
77 "func": annotate_ack,
78 "args_schema": AnnotateAckArgs,
79 "description": "Add a note to the acknowledgment record for the current post interaction",
80 "tags": ["acknowledgment", "note", "annotation", "metadata"]
81 },
82 {
83 "func": fetch_webpage,
84 "args_schema": WebpageArgs,
85 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader",
86 "tags": ["web", "fetch", "webpage", "markdown", "jina"]
87 },
88 {
89 "func": flag_archival_memory_for_deletion,
90 "args_schema": FlagArchivalMemoryForDeletionArgs,
91 "description": "Flag an archival memory for deletion based on its exact text content",
92 "tags": ["memory", "archival", "delete", "cleanup"]
93 },
94]
95
96
97def register_tools(agent_id: str = None, tools: List[str] = None, set_env: bool = True):
98 """Register tools with a Letta agent.
99
100 Args:
101 agent_id: ID of the agent to attach tools to. If None, uses config default.
102 tools: List of tool names to register. If None, registers all tools.
103 set_env: If True, set environment variables for tool execution. Defaults to True.
104 """
105 # Load config fresh (uses global config instance from get_config())
106 letta_config = get_letta_config()
107
108 # Use agent ID from config if not provided
109 if agent_id is None:
110 agent_id = letta_config['agent_id']
111
112 try:
113 # Initialize Letta client with API key and base_url from config
114 client_params = {
115 'token': letta_config['api_key'],
116 'timeout': letta_config['timeout']
117 }
118 if letta_config.get('base_url'):
119 client_params['base_url'] = letta_config['base_url']
120 client = Letta(**client_params)
121
122 # Get the agent by ID
123 try:
124 agent = client.agents.retrieve(agent_id=agent_id)
125 except Exception as e:
126 console.print(f"[red]Error: Agent '{agent_id}' not found[/red]")
127 console.print(f"Error details: {e}")
128 return
129
130 # Set environment variables for tool execution if requested
131 if set_env:
132 try:
133 bsky_config = get_bluesky_config()
134 env_vars = {
135 'BSKY_USERNAME': bsky_config['username'],
136 'BSKY_PASSWORD': bsky_config['password'],
137 'PDS_URI': bsky_config['pds_uri']
138 }
139
140 console.print(f"\n[bold cyan]Setting tool execution environment variables:[/bold cyan]")
141 console.print(f" BSKY_USERNAME: {env_vars['BSKY_USERNAME']}")
142 console.print(f" PDS_URI: {env_vars['PDS_URI']}")
143 console.print(f" BSKY_PASSWORD: {'*' * len(env_vars['BSKY_PASSWORD'])}\n")
144
145 # Modify agent with environment variables
146 client.agents.modify(
147 agent_id=agent_id,
148 tool_exec_environment_variables=env_vars
149 )
150
151 console.print("[green]✓ Environment variables set successfully[/green]\n")
152 except Exception as e:
153 console.print(f"[yellow]Warning: Failed to set environment variables: {e}[/yellow]\n")
154 logger.warning(f"Failed to set environment variables: {e}")
155
156 # Filter tools if specific ones requested
157 tools_to_register = TOOL_CONFIGS
158 if tools:
159 tools_to_register = [t for t in TOOL_CONFIGS if t["func"].__name__ in tools]
160 if len(tools_to_register) != len(tools):
161 missing = set(tools) - {t["func"].__name__ for t in tools_to_register}
162 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]")
163
164 # Create results table
165 table = Table(title=f"Tool Registration for Agent '{agent.name}' ({agent_id})")
166 table.add_column("Tool", style="cyan")
167 table.add_column("Status", style="green")
168 table.add_column("Description")
169
170 # Register each tool
171 for tool_config in tools_to_register:
172 func = tool_config["func"]
173 tool_name = func.__name__
174
175 try:
176 # Create or update the tool using the standalone function
177 created_tool = client.tools.upsert_from_function(
178 func=func,
179 args_schema=tool_config["args_schema"],
180 tags=tool_config["tags"]
181 )
182
183 # Get current agent tools
184 current_tools = client.agents.tools.list(agent_id=str(agent.id))
185 tool_names = [t.name for t in current_tools]
186
187 # Check if already attached
188 if created_tool.name in tool_names:
189 table.add_row(tool_name, "Already Attached", tool_config["description"])
190 else:
191 # Attach to agent
192 client.agents.tools.attach(
193 agent_id=str(agent.id),
194 tool_id=str(created_tool.id)
195 )
196 table.add_row(tool_name, "✓ Attached", tool_config["description"])
197
198 except Exception as e:
199 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"])
200 logger.error(f"Error registering tool {tool_name}: {e}")
201
202 console.print(table)
203
204 except Exception as e:
205 console.print(f"[red]Error: {str(e)}[/red]")
206 logger.error(f"Fatal error: {e}")
207
208
209def list_available_tools():
210 """List all available tools."""
211 table = Table(title="Available Void Tools")
212 table.add_column("Tool Name", style="cyan")
213 table.add_column("Description")
214 table.add_column("Tags", style="dim")
215
216 for tool_config in TOOL_CONFIGS:
217 table.add_row(
218 tool_config["func"].__name__,
219 tool_config["description"],
220 ", ".join(tool_config["tags"])
221 )
222
223 console.print(table)
224
225
226if __name__ == "__main__":
227 import argparse
228
229 parser = argparse.ArgumentParser(description="Register Void tools with a Letta agent")
230 parser.add_argument("--config", type=str, default='config.yaml', help="Path to config file (default: config.yaml)")
231 parser.add_argument("--agent-id", help=f"Agent ID (default: from config)")
232 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)")
233 parser.add_argument("--list", action="store_true", help="List available tools")
234 parser.add_argument("--no-env", action="store_true", help="Skip setting environment variables")
235
236 args = parser.parse_args()
237
238 # Initialize config with custom path (sets global config instance)
239 get_config(args.config)
240
241 if args.list:
242 list_available_tools()
243 else:
244 # Load config and get agent ID
245 letta_config = get_letta_config()
246 agent_id = args.agent_id if args.agent_id else letta_config['agent_id']
247 console.print(f"\n[bold]Registering tools for agent: {agent_id}[/bold]\n")
248 register_tools(agent_id, args.tools, set_env=not args.no_env)