a digital person for bluesky
at main 253 lines 10 kB view raw
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 blog_post_create, BlogPostCreateArgs 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 23from tools.get_record import get_atproto_record, GetRecordArgs 24 25logging.basicConfig(level=logging.INFO) 26logger = logging.getLogger(__name__) 27console = Console() 28 29 30# Tool configurations: function paired with its args_schema and metadata 31TOOL_CONFIGS = [ 32 { 33 "func": search_bluesky_posts, 34 "args_schema": SearchArgs, 35 "description": "Search for posts on Bluesky matching the given criteria", 36 "tags": ["bluesky", "search", "posts"] 37 }, 38 { 39 "func": create_new_bluesky_post, 40 "args_schema": PostArgs, 41 "description": "Create a new Bluesky post or thread", 42 "tags": ["bluesky", "post", "create", "thread"] 43 }, 44 { 45 "func": get_bluesky_feed, 46 "args_schema": FeedArgs, 47 "description": "Retrieve a Bluesky feed (home timeline or custom feed)", 48 "tags": ["bluesky", "feed", "timeline"] 49 }, 50 { 51 "func": halt_activity, 52 "args_schema": HaltArgs, 53 "description": "Signal to halt all bot activity and terminate bsky.py", 54 "tags": ["control", "halt", "terminate"] 55 }, 56 { 57 "func": add_post_to_bluesky_reply_thread, 58 "args_schema": ReplyThreadPostArgs, 59 "description": "Add a single post to the current Bluesky reply thread atomically", 60 "tags": ["bluesky", "reply", "thread", "atomic"] 61 }, 62 { 63 "func": ignore_notification, 64 "args_schema": IgnoreNotificationArgs, 65 "description": "Explicitly ignore a notification without replying (useful for ignoring bot interactions)", 66 "tags": ["notification", "ignore", "control", "bot"] 67 }, 68 { 69 "func": blog_post_create, 70 "args_schema": BlogPostCreateArgs, 71 "description": "Create a blog post on Greengale (served at greengale.app) with markdown support", 72 "tags": ["greengale", "blog", "post", "markdown"] 73 }, 74 { 75 "func": annotate_ack, 76 "args_schema": AnnotateAckArgs, 77 "description": "Add a note to the acknowledgment record for the current post interaction", 78 "tags": ["acknowledgment", "note", "annotation", "metadata"] 79 }, 80 { 81 "func": fetch_webpage, 82 "args_schema": WebpageArgs, 83 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader", 84 "tags": ["web", "fetch", "webpage", "markdown", "jina"] 85 }, 86 { 87 "func": flag_archival_memory_for_deletion, 88 "args_schema": FlagArchivalMemoryForDeletionArgs, 89 "description": "Flag an archival memory for deletion based on its exact text content", 90 "tags": ["memory", "archival", "delete", "cleanup"] 91 }, 92 { 93 "func": get_atproto_record, 94 "args_schema": GetRecordArgs, 95 "description": "Retrieve any ATProto record by URI or repo/collection/rkey (posts, profiles, follows, likes, etc.)", 96 "tags": ["atproto", "record", "fetch", "bluesky", "generic"] 97 }, 98] 99 100 101def register_tools(agent_id: str = None, tools: List[str] = None, set_env: bool = True): 102 """Register tools with a Letta agent. 103 104 Args: 105 agent_id: ID of the agent to attach tools to. If None, uses config default. 106 tools: List of tool names to register. If None, registers all tools. 107 set_env: If True, set environment variables for tool execution. Defaults to True. 108 """ 109 # Load config fresh (uses global config instance from get_config()) 110 letta_config = get_letta_config() 111 112 # Use agent ID from config if not provided 113 if agent_id is None: 114 agent_id = letta_config['agent_id'] 115 116 try: 117 # Initialize Letta client with API key and base_url from config 118 client_params = { 119 'api_key': letta_config['api_key'], # v1.0: token → api_key 120 'timeout': letta_config['timeout'] 121 } 122 if letta_config.get('base_url'): 123 client_params['base_url'] = letta_config['base_url'] 124 client = Letta(**client_params) 125 126 # Get the agent by ID 127 try: 128 agent = client.agents.retrieve(agent_id=agent_id) 129 except Exception as e: 130 console.print(f"[red]Error: Agent '{agent_id}' not found[/red]") 131 console.print(f"Error details: {e}") 132 return 133 134 # Set environment variables for tool execution if requested 135 if set_env: 136 try: 137 bsky_config = get_bluesky_config() 138 env_vars = { 139 'BSKY_USERNAME': bsky_config['username'], 140 'BSKY_PASSWORD': bsky_config['password'], 141 'PDS_URI': bsky_config['pds_uri'] 142 } 143 144 console.print(f"\n[bold cyan]Setting tool execution environment variables:[/bold cyan]") 145 console.print(f" BSKY_USERNAME: {env_vars['BSKY_USERNAME']}") 146 console.print(f" PDS_URI: {env_vars['PDS_URI']}") 147 console.print(f" BSKY_PASSWORD: {'*' * len(env_vars['BSKY_PASSWORD'])}\n") 148 149 # Update agent with environment variables (v1.0: modify → update) 150 client.agents.update( 151 agent_id=agent_id, 152 tool_exec_environment_variables=env_vars 153 ) 154 155 console.print("[green]✓ Environment variables set successfully[/green]\n") 156 except Exception as e: 157 console.print(f"[yellow]Warning: Failed to set environment variables: {e}[/yellow]\n") 158 logger.warning(f"Failed to set environment variables: {e}") 159 160 # Filter tools if specific ones requested 161 tools_to_register = TOOL_CONFIGS 162 if tools: 163 tools_to_register = [t for t in TOOL_CONFIGS if t["func"].__name__ in tools] 164 if len(tools_to_register) != len(tools): 165 missing = set(tools) - {t["func"].__name__ for t in tools_to_register} 166 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]") 167 168 # Create results table 169 table = Table(title=f"Tool Registration for Agent '{agent.name}' ({agent_id})") 170 table.add_column("Tool", style="cyan") 171 table.add_column("Status", style="green") 172 table.add_column("Description") 173 174 # Register each tool 175 for tool_config in tools_to_register: 176 func = tool_config["func"] 177 tool_name = func.__name__ 178 179 try: 180 # Create or update the tool using the standalone function 181 created_tool = client.tools.upsert_from_function( 182 func=func, 183 args_schema=tool_config["args_schema"], 184 tags=tool_config["tags"] 185 ) 186 187 # Get current agent tools (v1.0: list returns page object) 188 current_tools_page = client.agents.tools.list(agent_id=str(agent.id)) 189 current_tools = current_tools_page.items if hasattr(current_tools_page, 'items') else current_tools_page 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_tools(): 215 """List all available tools.""" 216 table = Table(title="Available Void 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 TOOL_CONFIGS: 222 table.add_row( 223 tool_config["func"].__name__, 224 tool_config["description"], 225 ", ".join(tool_config["tags"]) 226 ) 227 228 console.print(table) 229 230 231if __name__ == "__main__": 232 import argparse 233 234 parser = argparse.ArgumentParser(description="Register Void tools with a Letta agent") 235 parser.add_argument("--config", type=str, default='configs/config.yaml', help="Path to config file (default: configs/config.yaml)") 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 parser.add_argument("--no-env", action="store_true", help="Skip setting environment variables") 240 241 args = parser.parse_args() 242 243 # Initialize config with custom path (sets global config instance) 244 get_config(args.config) 245 246 if args.list: 247 list_available_tools() 248 else: 249 # Load config and get agent ID 250 letta_config = get_letta_config() 251 agent_id = args.agent_id if args.agent_id else letta_config['agent_id'] 252 console.print(f"\n[bold]Registering tools for agent: {agent_id}[/bold]\n") 253 register_tools(agent_id, args.tools, set_env=not args.no_env)