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)