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
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.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs
17from tools.halt import halt_activity, HaltArgs
18from tools.thread import add_post_to_bluesky_reply_thread, ReplyThreadPostArgs
19from tools.ignore import ignore_notification, IgnoreNotificationArgs
20from tools.whitewind import create_whitewind_blog_post, WhitewindPostArgs
21from tools.ack import annotate_ack, AnnotateAckArgs
22from tools.webpage import fetch_webpage, WebpageArgs
23
24letta_config = get_letta_config()
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 # Note: attach_user_blocks is available on the server but not exposed to the agent
51 # to prevent the agent from managing its own memory blocks
52 {
53 "func": detach_user_blocks,
54 "args_schema": DetachUserBlocksArgs,
55 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
56 "tags": ["memory", "blocks", "user"]
57 },
58 {
59 "func": halt_activity,
60 "args_schema": HaltArgs,
61 "description": "Signal to halt all bot activity and terminate bsky.py",
62 "tags": ["control", "halt", "terminate"]
63 },
64 {
65 "func": add_post_to_bluesky_reply_thread,
66 "args_schema": ReplyThreadPostArgs,
67 "description": "Add a single post to the current Bluesky reply thread atomically",
68 "tags": ["bluesky", "reply", "thread", "atomic"]
69 },
70 {
71 "func": ignore_notification,
72 "args_schema": IgnoreNotificationArgs,
73 "description": "Explicitly ignore a notification without replying (useful for ignoring bot interactions)",
74 "tags": ["notification", "ignore", "control", "bot"]
75 },
76 {
77 "func": create_whitewind_blog_post,
78 "args_schema": WhitewindPostArgs,
79 "description": "Create a blog post on Whitewind with markdown support",
80 "tags": ["whitewind", "blog", "post", "markdown"]
81 },
82 {
83 "func": annotate_ack,
84 "args_schema": AnnotateAckArgs,
85 "description": "Add a note to the acknowledgment record for the current post interaction",
86 "tags": ["acknowledgment", "note", "annotation", "metadata"]
87 },
88 {
89 "func": fetch_webpage,
90 "args_schema": WebpageArgs,
91 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader",
92 "tags": ["web", "fetch", "webpage", "markdown", "jina"]
93 },
94]
95
96
97def register_tools(agent_id: str = None, tools: List[str] = None):
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 """
104 # Use agent ID from config if not provided
105 if agent_id is None:
106 agent_id = letta_config['agent_id']
107
108 try:
109 # Initialize Letta client with API key from config
110 client = Letta(token=letta_config['api_key'], timeout=letta_config['timeout'])
111
112 # Get the agent by ID
113 try:
114 agent = client.agents.retrieve(agent_id=agent_id)
115 except Exception as e:
116 console.print(f"[red]Error: Agent '{agent_id}' not found[/red]")
117 console.print(f"Error details: {e}")
118 return
119
120 # Filter tools if specific ones requested
121 tools_to_register = TOOL_CONFIGS
122 if tools:
123 tools_to_register = [t for t in TOOL_CONFIGS if t["func"].__name__ in tools]
124 if len(tools_to_register) != len(tools):
125 missing = set(tools) - {t["func"].__name__ for t in tools_to_register}
126 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]")
127
128 # Create results table
129 table = Table(title=f"Tool Registration for Agent '{agent.name}' ({agent_id})")
130 table.add_column("Tool", style="cyan")
131 table.add_column("Status", style="green")
132 table.add_column("Description")
133
134 # Register each tool
135 for tool_config in tools_to_register:
136 func = tool_config["func"]
137 tool_name = func.__name__
138
139 try:
140 # Create or update the tool using the standalone function
141 created_tool = client.tools.upsert_from_function(
142 func=func,
143 args_schema=tool_config["args_schema"],
144 tags=tool_config["tags"]
145 )
146
147 # Get current agent tools
148 current_tools = client.agents.tools.list(agent_id=str(agent.id))
149 tool_names = [t.name for t in current_tools]
150
151 # Check if already attached
152 if created_tool.name in tool_names:
153 table.add_row(tool_name, "Already Attached", tool_config["description"])
154 else:
155 # Attach to agent
156 client.agents.tools.attach(
157 agent_id=str(agent.id),
158 tool_id=str(created_tool.id)
159 )
160 table.add_row(tool_name, "✓ Attached", tool_config["description"])
161
162 except Exception as e:
163 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"])
164 logger.error(f"Error registering tool {tool_name}: {e}")
165
166 console.print(table)
167
168 except Exception as e:
169 console.print(f"[red]Error: {str(e)}[/red]")
170 logger.error(f"Fatal error: {e}")
171
172
173def list_available_tools():
174 """List all available tools."""
175 table = Table(title="Available Void Tools")
176 table.add_column("Tool Name", style="cyan")
177 table.add_column("Description")
178 table.add_column("Tags", style="dim")
179
180 for tool_config in TOOL_CONFIGS:
181 table.add_row(
182 tool_config["func"].__name__,
183 tool_config["description"],
184 ", ".join(tool_config["tags"])
185 )
186
187 console.print(table)
188
189
190if __name__ == "__main__":
191 import argparse
192
193 parser = argparse.ArgumentParser(description="Register Void tools with a Letta agent")
194 parser.add_argument("--agent-id", help=f"Agent ID (default: from config)")
195 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)")
196 parser.add_argument("--list", action="store_true", help="List available tools")
197
198 args = parser.parse_args()
199
200 if args.list:
201 list_available_tools()
202 else:
203 # Use config default if no agent specified
204 agent_id = args.agent_id if args.agent_id else letta_config['agent_id']
205 console.print(f"\n[bold]Registering tools for agent: {agent_id}[/bold]\n")
206 register_tools(agent_id, args.tools)