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 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)