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.feed import get_bluesky_feed, FeedArgs
16from tools.blocks import attach_user_blocks, detach_user_blocks, user_note_append, user_note_replace, user_note_set, user_note_view, AttachUserBlocksArgs, DetachUserBlocksArgs, UserNoteAppendArgs, UserNoteReplaceArgs, UserNoteSetArgs, UserNoteViewArgs
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
24load_dotenv()
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": attach_user_blocks,
52 "args_schema": AttachUserBlocksArgs,
53 "description": "Attach user-specific memory blocks to the agent. Creates blocks if they don't exist.",
54 "tags": ["memory", "blocks", "user"]
55 },
56 {
57 "func": detach_user_blocks,
58 "args_schema": DetachUserBlocksArgs,
59 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.",
60 "tags": ["memory", "blocks", "user"]
61 },
62 {
63 "func": user_note_append,
64 "args_schema": UserNoteAppendArgs,
65 "description": "Append a note to a user's memory block. Creates the block if it doesn't exist.",
66 "tags": ["memory", "blocks", "user", "append"]
67 },
68 {
69 "func": user_note_replace,
70 "args_schema": UserNoteReplaceArgs,
71 "description": "Replace text in a user's memory block.",
72 "tags": ["memory", "blocks", "user", "replace"]
73 },
74 {
75 "func": user_note_set,
76 "args_schema": UserNoteSetArgs,
77 "description": "Set the complete content of a user's memory block.",
78 "tags": ["memory", "blocks", "user", "set"]
79 },
80 {
81 "func": user_note_view,
82 "args_schema": UserNoteViewArgs,
83 "description": "View the content of a user's memory block.",
84 "tags": ["memory", "blocks", "user", "view"]
85 },
86 {
87 "func": halt_activity,
88 "args_schema": HaltArgs,
89 "description": "Signal to halt all bot activity and terminate bsky.py",
90 "tags": ["control", "halt", "terminate"]
91 },
92 {
93 "func": add_post_to_bluesky_reply_thread,
94 "args_schema": ReplyThreadPostArgs,
95 "description": "Add a single post to the current Bluesky reply thread atomically",
96 "tags": ["bluesky", "reply", "thread", "atomic"]
97 },
98 {
99 "func": ignore_notification,
100 "args_schema": IgnoreNotificationArgs,
101 "description": "Explicitly ignore a notification without replying (useful for ignoring bot interactions)",
102 "tags": ["notification", "ignore", "control", "bot"]
103 },
104 {
105 "func": create_whitewind_blog_post,
106 "args_schema": WhitewindPostArgs,
107 "description": "Create a blog post on Whitewind with markdown support",
108 "tags": ["whitewind", "blog", "post", "markdown"]
109 },
110 {
111 "func": annotate_ack,
112 "args_schema": AnnotateAckArgs,
113 "description": "Add a note to the acknowledgment record for the current post interaction",
114 "tags": ["acknowledgment", "note", "annotation", "metadata"]
115 },
116 {
117 "func": fetch_webpage,
118 "args_schema": WebpageArgs,
119 "description": "Fetch a webpage and convert it to markdown/text format using Jina AI reader",
120 "tags": ["web", "fetch", "webpage", "markdown", "jina"]
121 },
122]
123
124
125def register_tools(agent_name: str = "void", tools: List[str] = None):
126 """Register tools with a Letta agent.
127
128 Args:
129 agent_name: Name of the agent to attach tools to
130 tools: List of tool names to register. If None, registers all tools.
131 """
132 try:
133 # Initialize Letta client with API key
134 client = Letta(token=os.environ["LETTA_API_KEY"])
135
136 # Find the agent
137 agents = client.agents.list()
138 agent = None
139 for a in agents:
140 if a.name == agent_name:
141 agent = a
142 break
143
144 if not agent:
145 console.print(f"[red]Error: Agent '{agent_name}' not found[/red]")
146 console.print("\nAvailable agents:")
147 for a in agents:
148 console.print(f" - {a.name}")
149 return
150
151 # Filter tools if specific ones requested
152 tools_to_register = TOOL_CONFIGS
153 if tools:
154 tools_to_register = [t for t in TOOL_CONFIGS if t["func"].__name__ in tools]
155 if len(tools_to_register) != len(tools):
156 missing = set(tools) - {t["func"].__name__ for t in tools_to_register}
157 console.print(f"[yellow]Warning: Unknown tools: {missing}[/yellow]")
158
159 # Create results table
160 table = Table(title=f"Tool Registration for Agent '{agent_name}'")
161 table.add_column("Tool", style="cyan")
162 table.add_column("Status", style="green")
163 table.add_column("Description")
164
165 # Register each tool
166 for tool_config in tools_to_register:
167 func = tool_config["func"]
168 tool_name = func.__name__
169
170 try:
171 # Create or update the tool using the standalone function
172 created_tool = client.tools.upsert_from_function(
173 func=func,
174 args_schema=tool_config["args_schema"],
175 tags=tool_config["tags"]
176 )
177
178 # Get current agent tools
179 current_tools = client.agents.tools.list(agent_id=str(agent.id))
180 tool_names = [t.name for t in current_tools]
181
182 # Check if already attached
183 if created_tool.name in tool_names:
184 table.add_row(tool_name, "Already Attached", tool_config["description"])
185 else:
186 # Attach to agent
187 client.agents.tools.attach(
188 agent_id=str(agent.id),
189 tool_id=str(created_tool.id)
190 )
191 table.add_row(tool_name, "✓ Attached", tool_config["description"])
192
193 except Exception as e:
194 table.add_row(tool_name, f"✗ Error: {str(e)}", tool_config["description"])
195 logger.error(f"Error registering tool {tool_name}: {e}")
196
197 console.print(table)
198
199 except Exception as e:
200 console.print(f"[red]Error: {str(e)}[/red]")
201 logger.error(f"Fatal error: {e}")
202
203
204def list_available_tools():
205 """List all available tools."""
206 table = Table(title="Available Void Tools")
207 table.add_column("Tool Name", style="cyan")
208 table.add_column("Description")
209 table.add_column("Tags", style="dim")
210
211 for tool_config in TOOL_CONFIGS:
212 table.add_row(
213 tool_config["func"].__name__,
214 tool_config["description"],
215 ", ".join(tool_config["tags"])
216 )
217
218 console.print(table)
219
220
221if __name__ == "__main__":
222 import argparse
223
224 parser = argparse.ArgumentParser(description="Register Void tools with a Letta agent")
225 parser.add_argument("agent", nargs="?", default="void", help="Agent name (default: void)")
226 parser.add_argument("--tools", nargs="+", help="Specific tools to register (default: all)")
227 parser.add_argument("--list", action="store_true", help="List available tools")
228
229 args = parser.parse_args()
230
231 if args.list:
232 list_available_tools()
233 else:
234 console.print(f"\n[bold]Registering tools for agent: {args.agent}[/bold]\n")
235 register_tools(args.agent, args.tools)