a digital person for bluesky
at 42e4d0055155dada2db9c31df06a3238decf3328 218 lines 8.2 kB view raw
1#!/usr/bin/env python3 2""" 3Add Bluesky posting 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_posting_tool") 16 17def create_posting_tool(client: Letta): 18 """Create the Bluesky posting tool using Letta SDK.""" 19 20 def post_to_bluesky(text: str) -> str: 21 """ 22 Post a message to Bluesky. 23 24 Args: 25 text: The text content of the post (required) 26 27 Returns: 28 Status message with the post URI if successful, error message if failed 29 """ 30 import os 31 import requests 32 import json 33 import re 34 from datetime import datetime, timezone 35 36 # Check character limit 37 if len(text) > 300: 38 raise ValueError(f"Post text exceeds 300 character limit ({len(text)} characters)") 39 40 try: 41 # Get credentials from environment 42 username = os.getenv("BSKY_USERNAME") 43 password = os.getenv("BSKY_PASSWORD") 44 pds_host = os.getenv("PDS_URI", "https://bsky.social") 45 46 if not username or not password: 47 return "Error: BSKY_USERNAME and BSKY_PASSWORD environment variables must be set" 48 49 # Create session 50 session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 51 session_data = { 52 "identifier": username, 53 "password": password 54 } 55 56 try: 57 session_response = requests.post(session_url, json=session_data, timeout=10) 58 session_response.raise_for_status() 59 session = session_response.json() 60 access_token = session.get("accessJwt") 61 user_did = session.get("did") 62 63 if not access_token or not user_did: 64 return "Error: Failed to get access token or DID from session" 65 except Exception as e: 66 return f"Error: Authentication failed. ({str(e)})" 67 68 # Helper function to parse mentions and create facets 69 def parse_mentions(text: str): 70 facets = [] 71 # Regex for mentions based on Bluesky handle syntax 72 mention_regex = rb"[$|\W](@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 73 text_bytes = text.encode("UTF-8") 74 75 for m in re.finditer(mention_regex, text_bytes): 76 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 77 78 # Resolve handle to DID 79 try: 80 resolve_resp = requests.get( 81 f"{pds_host}/xrpc/com.atproto.identity.resolveHandle", 82 params={"handle": handle}, 83 timeout=5 84 ) 85 if resolve_resp.status_code == 200: 86 did = resolve_resp.json()["did"] 87 facets.append({ 88 "index": { 89 "byteStart": m.start(1), 90 "byteEnd": m.end(1), 91 }, 92 "features": [{"$type": "app.bsky.richtext.facet#mention", "did": did}], 93 }) 94 except: 95 # If handle resolution fails, skip this mention 96 continue 97 98 return facets 99 100 # Helper function to parse URLs and create facets 101 def parse_urls(text: str): 102 facets = [] 103 # URL regex 104 url_regex = rb"[$|\W](https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*[-a-zA-Z0-9@%_\+~#//=])?)" 105 text_bytes = text.encode("UTF-8") 106 107 for m in re.finditer(url_regex, text_bytes): 108 url = m.group(1).decode("UTF-8") 109 facets.append({ 110 "index": { 111 "byteStart": m.start(1), 112 "byteEnd": m.end(1), 113 }, 114 "features": [ 115 { 116 "$type": "app.bsky.richtext.facet#link", 117 "uri": url, 118 } 119 ], 120 }) 121 122 return facets 123 124 125 # Build the post record 126 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") 127 128 post_record = { 129 "$type": "app.bsky.feed.post", 130 "text": text, 131 "createdAt": now, 132 } 133 134 # Add facets for mentions and links 135 facets = parse_mentions(text) + parse_urls(text) 136 if facets: 137 post_record["facets"] = facets 138 139 # Create the post 140 try: 141 create_record_url = f"{pds_host}/xrpc/com.atproto.repo.createRecord" 142 headers = {"Authorization": f"Bearer {access_token}"} 143 144 create_data = { 145 "repo": user_did, 146 "collection": "app.bsky.feed.post", 147 "record": post_record 148 } 149 150 post_response = requests.post(create_record_url, headers=headers, json=create_data, timeout=10) 151 post_response.raise_for_status() 152 result = post_response.json() 153 154 post_uri = result.get("uri") 155 return f"✅ Post created successfully! URI: {post_uri}" 156 157 except Exception as e: 158 return f"Error: Failed to create post. ({str(e)})" 159 160 except Exception as e: 161 error_msg = f"Error posting to Bluesky: {str(e)}" 162 return error_msg 163 164 # Create the tool using upsert 165 tool = client.tools.upsert_from_function( 166 func=post_to_bluesky, 167 tags=["bluesky", "post", "create"] 168 ) 169 170 logger.info(f"Created tool: {tool.name} (ID: {tool.id})") 171 return tool 172 173def add_posting_tool_to_void(): 174 """Add posting tool to the void agent.""" 175 176 # Create client 177 client = Letta(token=os.environ["LETTA_API_KEY"]) 178 179 logger.info("Adding posting tool to void agent...") 180 181 # Create the posting tool 182 posting_tool = create_posting_tool(client) 183 184 # Find the void agent 185 agents = client.agents.list(name="void") 186 if not agents: 187 print("❌ Void agent not found") 188 return 189 190 void_agent = agents[0] 191 192 # Get current tools 193 current_tools = client.agents.tools.list(agent_id=void_agent.id) 194 tool_names = [tool.name for tool in current_tools] 195 196 # Add posting tool if not already present 197 if posting_tool.name not in tool_names: 198 client.agents.tools.attach(agent_id=void_agent.id, tool_id=posting_tool.id) 199 logger.info(f"Added {posting_tool.name} to void agent") 200 print(f"✅ Added post_to_bluesky tool to void agent!") 201 print(f"\nVoid agent can now post to Bluesky:") 202 print(f" - Simple post: 'Post \"Hello world!\" to Bluesky'") 203 print(f" - With mentions: 'Post \"Thanks @cameron.pfiffer.org for the help!\"'") 204 print(f" - With links: 'Post \"Check out https://bsky.app\"'") 205 else: 206 logger.info(f"Tool {posting_tool.name} already attached to void agent") 207 print(f"✅ Posting tool already present on void agent") 208 209def main(): 210 """Main function.""" 211 try: 212 add_posting_tool_to_void() 213 except Exception as e: 214 logger.error(f"Error: {e}") 215 print(f"❌ Error: {e}") 216 217if __name__ == "__main__": 218 main()