a digital person for bluesky
1#!/usr/bin/env python3
2"""
3Add Bluesky feed retrieval tool to the main void agent.
4"""
5
6import os
7import logging
8from letta_client import Letta
9
10# Configure logging
11logging.basicConfig(
12 level=logging.INFO,
13 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
14)
15logger = logging.getLogger("add_feed_tool")
16
17def create_feed_tool(client: Letta):
18 """Create the Bluesky feed retrieval tool using Letta SDK."""
19
20 def get_bluesky_feed(feed_uri: str = None, max_posts: int = 25) -> str:
21 """
22 Retrieve a Bluesky feed. If no feed_uri provided, gets the authenticated user's home timeline.
23
24 Args:
25 feed_uri: The AT-URI of the feed to retrieve (optional - defaults to home timeline)
26 max_posts: Maximum number of posts to return (default: 25, max: 100)
27
28 Returns:
29 YAML-formatted feed data with posts and metadata
30 """
31 import os
32 import requests
33 import json
34 import yaml
35 from datetime import datetime
36
37 try:
38 # Get credentials from environment
39 username = os.getenv("BSKY_USERNAME")
40 password = os.getenv("BSKY_PASSWORD")
41 pds_host = os.getenv("PDS_URI", "https://bsky.social")
42
43 if not username or not password:
44 return "Error: BSKY_USERNAME and BSKY_PASSWORD environment variables must be set"
45
46 # Create session
47 session_url = f"{pds_host}/xrpc/com.atproto.server.createSession"
48 session_data = {
49 "identifier": username,
50 "password": password
51 }
52
53 try:
54 session_response = requests.post(session_url, json=session_data, timeout=10)
55 session_response.raise_for_status()
56 session = session_response.json()
57 access_token = session.get("accessJwt")
58
59 if not access_token:
60 return "Error: Failed to get access token from session"
61 except Exception as e:
62 return f"Error: Authentication failed. ({str(e)})"
63
64 # Build feed parameters
65 params = {
66 "limit": min(max_posts, 100)
67 }
68
69 # Determine which endpoint to use
70 if feed_uri:
71 # Use getFeed for custom feeds
72 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getFeed"
73 params["feed"] = feed_uri
74 feed_type = "custom_feed"
75 else:
76 # Use getTimeline for home feed
77 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getTimeline"
78 feed_type = "home_timeline"
79
80 # Make authenticated feed request
81 try:
82 headers = {"Authorization": f"Bearer {access_token}"}
83 feed_response = requests.get(feed_url, params=params, headers=headers, timeout=10)
84 feed_response.raise_for_status()
85 feed_data = feed_response.json()
86 except Exception as e:
87 feed_identifier = feed_uri if feed_uri else "home timeline"
88 return f"Error: Failed to retrieve feed '{feed_identifier}'. ({str(e)})"
89
90 # Build feed results structure
91 results_data = {
92 "feed_data": {
93 "feed_type": feed_type,
94 "feed_uri": feed_uri if feed_uri else "home_timeline",
95 "timestamp": datetime.now().isoformat(),
96 "parameters": {
97 "max_posts": max_posts,
98 "user": username
99 },
100 "results": feed_data
101 }
102 }
103
104 # Convert to YAML directly without field stripping complications
105 # This avoids the JSON parsing errors we had before
106 return yaml.dump(results_data, default_flow_style=False, allow_unicode=True)
107
108 except Exception as e:
109 error_msg = f"Error retrieving feed: {str(e)}"
110 return error_msg
111
112 # Create the tool using upsert
113 tool = client.tools.upsert_from_function(
114 func=get_bluesky_feed,
115 tags=["bluesky", "feed", "timeline"]
116 )
117
118 logger.info(f"Created tool: {tool.name} (ID: {tool.id})")
119 return tool
120
121def add_feed_tool_to_void():
122 """Add feed tool to the void agent."""
123
124 # Create client
125 client = Letta(token=os.environ["LETTA_API_KEY"])
126
127 logger.info("Adding feed tool to void agent...")
128
129 # Create the feed tool
130 feed_tool = create_feed_tool(client)
131
132 # Find the void agent
133 agents = client.agents.list(name="void")
134 if not agents:
135 print("❌ Void agent not found")
136 return
137
138 void_agent = agents[0]
139
140 # Get current tools
141 current_tools = client.agents.tools.list(agent_id=void_agent.id)
142 tool_names = [tool.name for tool in current_tools]
143
144 # Add feed tool if not already present
145 if feed_tool.name not in tool_names:
146 client.agents.tools.attach(agent_id=void_agent.id, tool_id=feed_tool.id)
147 logger.info(f"Added {feed_tool.name} to void agent")
148 print(f"✅ Added get_bluesky_feed tool to void agent!")
149 print(f"\nVoid agent can now retrieve Bluesky feeds:")
150 print(f" - Home timeline: 'Show me my home feed'")
151 print(f" - Custom feed: 'Get posts from at://did:plc:xxx/app.bsky.feed.generator/xxx'")
152 print(f" - Limited posts: 'Show me the latest 10 posts from my timeline'")
153 else:
154 logger.info(f"Tool {feed_tool.name} already attached to void agent")
155 print(f"✅ Feed tool already present on void agent")
156
157def main():
158 """Main function."""
159 try:
160 add_feed_tool_to_void()
161 except Exception as e:
162 logger.error(f"Error: {e}")
163 print(f"❌ Error: {e}")
164
165if __name__ == "__main__":
166 main()