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 dotenv import load_dotenv
8from letta_client import Letta
9from rich.console import Console
10from rich.table import Table
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.reply import reply_to_bluesky_post, ReplyArgs
16from tools.feed import get_bluesky_feed, FeedArgs
17from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs
18
19load_dotenv()
20logging.basicConfig(level=logging.INFO)
21logger = logging.getLogger(__name__)
22console = Console()
23
24
25# Tool configurations: function paired with its args_schema and metadata
26TOOL_CONFIGS = [
27 {
28 "func": search_bluesky_posts,
29 "args_schema": SearchArgs,
30 "description": "Search for posts on Bluesky matching the given criteria",
31 "tags": ["bluesky", "search", "posts"]
32 },
33 {
34 "func": create_new_bluesky_post,
35 "args_schema": PostArgs,
36 "description": "Create a NEW standalone post on Bluesky. DO NOT use for replies.",
37 "tags": ["bluesky", "post", "create", "new"]
38 },
39 {
40 "func": reply_to_bluesky_post,
41 "args_schema": ReplyArgs,
42 "description": "Reply to an existing Bluesky post. Requires the URI of the post being replied to.",
43 "tags": ["bluesky", "reply", "thread", "conversation"]
44 },
45 {
46 "func": get_bluesky_feed,
47 "args_schema": FeedArgs,
48 "description": "Retrieve a Bluesky feed (home timeline or custom feed)",
49 "tags": ["bluesky", "feed", "timeline"]
50 },
51 {
52 "func": attach_user_blocks,
53 "args_schema": AttachUserBlocksArgs,
54 "description": "Attach user-specific memory blocks to the agent. Creates blocks if they don't exist.",
55 "tags": ["memory", "blocks", "user"]
56 },
57 {
58 "func": detach_user_blocks,
59 "args_schema": DetachUserBlocksArgs,
60 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
61 "tags": ["memory", "blocks", "user"]
62 },
63 # {
64 # "func": update_user_blocks,
65 # "args_schema": UpdateUserBlockArgs,
66 # "description": "Update the content of user-specific memory blocks",
67 # "tags": ["memory", "blocks", "user"]
68 # },
69]
70
71
72def register_tools(agent_name: str = "void", tools: List[str] = None):
73 """Register tools with a Letta agent.
74
75 Args:
76 agent_name: Name of the agent to attach tools to
77 tools: List of tool names to register. If None, registers all tools.
78 """
79 try:
80 # Initialize Letta client with API key
81 client = Letta(token=os.environ["LETTA_API_KEY"])
82
83 # Find the agent
84 agents = client.agents.list()
85 agent = None
86 for a in agents:
87 if a.name == agent_name:
88 agent = a
89 break
90
91 if not agent:
92 console.print(f"[red]Error: Agent '{agent_name}' not found[/red]")
93 console.print("\nAvailable agents:")
94 for a in agents:
95 console.print(f" - {a.name}")
96 return
97
98 # Filter tools if specific ones requested
99 tools_to_register = TOOL_CONFIGS
100 if tools:
101 tools_to_register = [t for t in TOOL_CONFIGS if t["func"].__name__ in tools]
102 if len(tools_to_register) != len(tools):
103 missing = set(tools) - {t["func"].__name__ for t in tools_to_register}
104 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]")
105
106 # Create results table
107 table = Table(title=f"Tool Registration for Agent '{agent_name}'")
108 table.add_column("Tool", style="cyan")
109 table.add_column("Status", style="green")
110 table.add_column("Description")
111
112 # Register each tool
113 for tool_config in tools_to_register:
114 func = tool_config["func"]
115 tool_name = func.__name__
116
117 try:
118 # Create or update the tool using the standalone function
119 created_tool = client.tools.upsert_from_function(
120 func=func,
121 args_schema=tool_config["args_schema"],
122 tags=tool_config["tags"]
123 )
124
125 # Get current agent tools
126 current_tools = client.agents.tools.list(agent_id=str(agent.id))
127 tool_names = [t.name for t in current_tools]
128
129 # Check if already attached
130 if created_tool.name in tool_names:
131 table.add_row(tool_name, "Already Attached", tool_config["description"])
132 else:
133 # Attach to agent
134 client.agents.tools.attach(
135 agent_id=str(agent.id),
136 tool_id=str(created_tool.id)
137 )
138 table.add_row(tool_name, "✓ Attached", tool_config["description"])
139
140 except Exception as e:
141 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"])
142 logger.error(f"Error registering tool {tool_name}: {e}")
143
144 console.print(table)
145
146 except Exception as e:
147 console.print(f"[red]Error: {str(e)}[/red]")
148 logger.error(f"Fatal error: {e}")
149
150
151def list_available_tools():
152 """List all available tools."""
153 table = Table(title="Available Void Tools")
154 table.add_column("Tool Name", style="cyan")
155 table.add_column("Description")
156 table.add_column("Tags", style="dim")
157
158 for tool_config in TOOL_CONFIGS:
159 table.add_row(
160 tool_config["func"].__name__,
161 tool_config["description"],
162 ", ".join(tool_config["tags"])
163 )
164
165 console.print(table)
166
167
168if __name__ == "__main__":
169 import argparse
170
171 parser = argparse.ArgumentParser(description="Register Void tools with a Letta agent")
172 parser.add_argument("agent", nargs="?", default="void", help="Agent name (default: void)")
173 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)")
174 parser.add_argument("--list", action="store_true", help="List available tools")
175
176 args = parser.parse_args()
177
178 if args.list:
179 list_available_tools()
180 else:
181 console.print(f"\n[bold]Registering tools for agent: {args.agent}[/bold]\n")
182 register_tools(args.agent, args.tools)