a tool to help your Letta AI agents navigate bluesky

refactored update bluesky connection tool. now accepts both handle and did.

Changed files
+171 -19
tools
+171 -19
tools/bluesky/update_bluesky_connection.py
··· 1 1 import os 2 2 from typing import Dict, Literal 3 + from atproto import Client, models, AtUri 4 + 3 5 4 6 def update_bluesky_connection( 5 7 change_type: Literal["follow", "unfollow", "mute", "unmute", "block", "unblock"], 6 8 actor: str 7 9 ) -> Dict: 8 10 """ 9 - Update connection status with a Bluesky user (follow, unfollow, mute, unmute, block, unblock). 11 + Manage social connections with Bluesky users through follow, mute, and block actions. 12 + 13 + This tool allows you to update your relationship with other Bluesky users by following/ 14 + unfollowing them, muting/unmuting their content, or blocking/unblocking them. The tool 15 + intelligently checks the current connection state before taking action and reports whether 16 + changes were made. Use this tool to manage your social graph and control what content 17 + you see on Bluesky. 18 + 19 + Connection types explained: 20 + - Follow/Unfollow: Subscribe or unsubscribe from a user's posts in your feed 21 + - Mute/Unmute: Hide a user's posts and replies from your feed without unfollowing 22 + (they won't be notified). Muting is softer than blocking. 23 + - Block/Unblock: Prevent all interaction with a user bidirectionally. Blocking prevents 24 + them from seeing your content and you from seeing theirs. This is the strongest action. 10 25 11 26 Args: 12 - change_type (str): Type of connection change - "follow", "unfollow", "mute", "unmute", "block", or "unblock". 13 - actor (str): The actor identifier (DID like 'did:plc:...' or handle like 'user.bsky.social'). 27 + change_type (Literal): The type of connection change to make. Must be one of: 28 + 29 + - "follow": Subscribe to the user's posts. Their content will appear in your feed. 30 + The user will be notified that you followed them. 31 + 32 + - "unfollow": Unsubscribe from the user's posts. Their content will no longer appear 33 + in your feed. The user may notice you unfollowed them. 34 + 35 + - "mute": Hide the user's posts and replies from your feed without unfollowing them. 36 + This is private - the user is not notified. Use this to reduce noise while 37 + maintaining the connection. The user can still interact with your content. 38 + 39 + - "unmute": Resume seeing the user's posts and replies in your feed. Removes a 40 + previous mute. This is private - the user is not notified. 41 + 42 + - "block": Prevent all interaction with the user bidirectionally. They cannot see 43 + your posts or interact with them, and you cannot see theirs. This is the strongest 44 + moderation action. The user may infer they've been blocked. 45 + 46 + - "unblock": Remove a block and restore the ability for both of you to see and 47 + interact with each other's content. Does not automatically re-follow. 48 + 49 + This parameter is required and must be exactly one of the values listed above. 50 + 51 + actor (str): The identifier for the Bluesky user to update the connection with. 52 + This parameter is required and cannot be empty. 53 + 54 + Accepts two formats: 55 + - Handle: The user's full handle including domain suffix 56 + Examples: "alice.bsky.social", "nytimes.com", "user.example.org" 57 + - DID: The user's decentralized identifier starting with "did:plc:" 58 + Example: "did:plc:z72i7hdynmk6r22z27h6tvur" 59 + 60 + IMPORTANT: The tool automatically resolves handles to DIDs before performing 61 + any operations. If you provide a handle, the tool will first fetch the user's 62 + profile to get their DID, then use the DID for all connection operations. This 63 + ensures reliability and consistency. If you already have a DID, it will be used 64 + directly without additional lookups. 65 + 66 + You can obtain actor identifiers from tools like search_bluesky or 67 + get_bluesky_user_info. 14 68 15 69 Returns: 16 - Dict: Status and details of the connection change. 70 + Dict: A dictionary containing the operation result with the following keys: 71 + 72 + Common keys (all operations): 73 + - status (str): Either "success" or "error" 74 + - change_type (str): The type of change that was requested 75 + - actor (dict): Information about the target user with keys: 76 + - did (str): The user's DID 77 + - handle (str): The user's handle 78 + - display_name (str): The user's display name 79 + - message (str): Human-readable description of what happened 80 + - action_taken (bool): Whether a change was actually made (False if already 81 + in the desired state) 82 + 83 + For errors: 84 + - message (str): Human-readable error description with guidance 85 + 86 + Examples: 87 + # Follow a user 88 + update_bluesky_connection( 89 + change_type="follow", 90 + actor="alice.bsky.social" 91 + ) 92 + 93 + # Unfollow a user 94 + update_bluesky_connection( 95 + change_type="unfollow", 96 + actor="bob.bsky.social" 97 + ) 98 + 99 + # Mute a user to hide their content without unfollowing 100 + update_bluesky_connection( 101 + change_type="mute", 102 + actor="noisy.bsky.social" 103 + ) 104 + 105 + # Unmute a previously muted user 106 + update_bluesky_connection( 107 + change_type="unmute", 108 + actor="noisy.bsky.social" 109 + ) 110 + 111 + # Block a user (strongest moderation action) 112 + update_bluesky_connection( 113 + change_type="block", 114 + actor="spammer.bsky.social" 115 + ) 116 + 117 + # Unblock a previously blocked user 118 + update_bluesky_connection( 119 + change_type="unblock", 120 + actor="spammer.bsky.social" 121 + ) 122 + 123 + # Use DID instead of handle 124 + update_bluesky_connection( 125 + change_type="follow", 126 + actor="did:plc:z72i7hdynmk6r22z27h6tvur" 127 + ) 17 128 """ 18 129 try: 19 - from atproto import Client, models, AtUri 20 - 21 130 if not actor: 22 - raise ValueError("Actor identifier must be provided.") 131 + return { 132 + "status": "error", 133 + "message": "Error: The actor parameter is empty. To resolve this, provide a valid user identifier " 134 + "such as a handle (like 'alice.bsky.social') or DID (like 'did:plc:...'). You can obtain " 135 + "user identifiers from tools like search_bluesky or get_bluesky_user_info. This is a common " 136 + "mistake and can be fixed by calling the tool again with a valid actor." 137 + } 23 138 24 139 if change_type not in ["follow", "unfollow", "mute", "unmute", "block", "unblock"]: 25 - raise ValueError(f"Invalid change_type: {change_type}. Must be one of: follow, unfollow, mute, unmute, block, unblock.") 140 + return { 141 + "status": "error", 142 + "message": f"Error: The change_type '{change_type}' is not valid. To resolve this, use one of the " 143 + f"supported values: 'follow', 'unfollow', 'mute', 'unmute', 'block', or 'unblock'. Each " 144 + f"type manages a different aspect of your connection with the user. This is a parameter " 145 + f"validation error that can be fixed by using a supported change type." 146 + } 26 147 27 148 username = os.environ.get("BSKY_USERNAME") 28 149 password = os.environ.get("BSKY_APP_PASSWORD") 29 150 if not username or not password: 30 - raise EnvironmentError("BSKY_USERNAME and BSKY_APP_PASSWORD must be set.") 151 + return { 152 + "status": "error", 153 + "message": "Error: Missing Bluesky authentication credentials. The BSKY_USERNAME and BSKY_APP_PASSWORD " 154 + "environment variables are not set. To resolve this, ask the user to configure these environment " 155 + "variables with valid Bluesky credentials. This is a configuration issue that the user needs to " 156 + "address before you can manage connections on Bluesky." 157 + } 31 158 32 159 client = Client() 33 160 client.login(username, password) 34 161 35 - # Get user profile to verify they exist and get current status 36 - profile = client.get_profile(actor=actor) 162 + # Get user profile to verify they exist, resolve handle to DID, and get current status 163 + try: 164 + profile = client.get_profile(actor=actor) 165 + except Exception as e: 166 + return { 167 + "status": "error", 168 + "message": f"Error: Failed to find user with identifier '{actor}'. To resolve this, verify the handle " 169 + f"or DID is correct and that the user exists. Common issues include typos in the handle, " 170 + f"deleted or suspended accounts, or using an incomplete handle (missing the domain like " 171 + f"'.bsky.social'). You can use search_bluesky to find valid user identifiers. " 172 + f"API details: {str(e)}" 173 + } 174 + 175 + # Extract DID for all operations (ensures we always use DID regardless of input format) 176 + user_did = profile.did 37 177 38 178 # Check current viewer state 39 179 viewer = profile.viewer if profile.viewer else None ··· 48 188 } 49 189 } 50 190 51 - # Perform the requested action 191 + # Perform the requested action (always using DID) 52 192 if change_type == "follow": 53 193 if viewer and viewer.following: 54 194 result["message"] = f"Already following {profile.handle}." 55 195 result["action_taken"] = False 56 196 else: 57 - client.follow(actor) 197 + client.follow(user_did) 58 198 result["message"] = f"Successfully followed {profile.handle}." 59 199 result["action_taken"] = True 60 200 ··· 72 212 result["message"] = f"Already muted {profile.handle}." 73 213 result["action_taken"] = False 74 214 else: 75 - client.mute(actor) 215 + client.mute(user_did) 76 216 result["message"] = f"Successfully muted {profile.handle}." 77 217 result["action_taken"] = True 78 218 79 219 elif change_type == "unmute": 80 220 if viewer and viewer.muted: 81 - client.unmute(actor) 221 + client.unmute(user_did) 82 222 result["message"] = f"Successfully unmuted {profile.handle}." 83 223 result["action_taken"] = True 84 224 else: ··· 90 230 result["message"] = f"Already blocked {profile.handle}." 91 231 result["action_taken"] = False 92 232 else: 93 - # Create block record with proper structure 233 + # Create block record with proper structure (using resolved DID) 94 234 block_record = models.AppBskyGraphBlock.Record( 95 - subject=profile.did, 235 + subject=user_did, 96 236 created_at=client.get_current_time_iso() 97 237 ) 98 238 client.app.bsky.graph.block.create(client.me.did, block_record) ··· 113 253 return result 114 254 115 255 except ImportError: 116 - raise ImportError("atproto package not installed. Install with: pip install atproto") 256 + return { 257 + "status": "error", 258 + "message": "Error: The atproto Python package is not installed in the execution environment. " 259 + "To resolve this, the system administrator needs to install it using 'pip install atproto'. " 260 + "This is a dependency issue that prevents the tool from connecting to Bluesky. Once the " 261 + "package is installed, this tool will work normally." 262 + } 117 263 except Exception as e: 118 - raise RuntimeError(f"Error updating Bluesky connection: {e}") 264 + return { 265 + "status": "error", 266 + "message": f"Error: An unexpected issue occurred while updating the Bluesky connection: {str(e)}. " 267 + f"To resolve this, verify the actor identifier is correct and that the user exists. This type " 268 + f"of error can occur due to invalid actors, deleted accounts, temporary API issues, or network " 269 + f"problems, and usually succeeds on retry." 270 + }