a tool to help your Letta AI agents navigate bluesky

refactored update bluesky profile tool

Changed files
+157 -31
tools
+157 -31
tools/bluesky/update_bluesky_profile.py
··· 2 2 3 3 import os 4 4 from typing import Optional 5 + from atproto import Client, models 6 + 5 7 6 8 def update_bluesky_profile(display_name: Optional[str] = None, bio: Optional[str] = None) -> dict: 7 9 """ 8 - Safely update the user's Bluesky profile display name and/or bio 9 - without losing other fields (avatar, banner, etc.). 10 + Update the authenticated user's Bluesky profile display name and/or bio. 11 + 12 + This tool safely updates profile information while automatically preserving all other 13 + profile data (avatar, banner, pinned post, etc.). You can confidently update just the 14 + display name, just the bio, or both together - the tool handles preserving unchanged 15 + fields for you. 16 + 17 + The tool uses a safe fetch-merge-write pattern: it retrieves the current profile, 18 + merges your updates with existing data, then saves the complete record. This ensures 19 + no data loss when updating individual fields. 20 + 21 + Use this tool when you need to update the user's display name, bio, or both. Updates 22 + are immediate and visible on the user's profile. 10 23 11 24 Args: 12 - display_name: The new display name for the profile (max 64 characters). Optional 13 - bio: The new bio/description for the profile (max 256 characters). Optional 25 + display_name (Optional[str]): The new display name for the profile (max 64 characters). This is the prominent name shown on posts and the profile page. Can include Unicode characters, emoji, and special characters. Use empty string to remove the display name. If None, the current display name is preserved unchanged. Defaults to None. 26 + bio (Optional[str]): The new bio/description for the profile (max 256 characters). This appears below the display name and profile picture. Can include line breaks, emoji, and special characters. Supports plain text only (no markdown). Use empty string to remove the bio. If None, the current bio is preserved unchanged. Defaults to None. At least one of display_name or bio must be provided (not both None). 14 27 15 28 Returns: 16 - A dictionary containing the status, success message, updated fields, and profile URL 29 + dict: A dictionary containing the operation result with the following keys: 30 + 31 + On success: 32 + - status (str): "success" 33 + - message (str): Confirmation message 34 + - updated_fields (dict): The fields that were changed with their new values: 35 + - displayName (str): New display name if updated 36 + - description (str): New bio if updated 37 + - profile_url (str): URL to view the updated profile on bsky.app 38 + 39 + On error: 40 + - status (str): "error" 41 + - message (str): Human-readable error description with resolution guidance 42 + 43 + Examples: 44 + # Update only the display name 45 + update_bluesky_profile(display_name="Alice Johnson") 46 + 47 + # Update only the bio 48 + update_bluesky_profile(bio="Software engineer interested in AI") 49 + 50 + # Update both display name and bio 51 + update_bluesky_profile( 52 + display_name="Dr. Alice Johnson", 53 + bio="Researcher in distributed systems and AI" 54 + ) 55 + 56 + # Remove display name (use empty string) 57 + update_bluesky_profile(display_name="") 58 + 59 + # Update bio with multiple lines and emoji 60 + update_bluesky_profile( 61 + bio="🚀 Building the decentralized web\\nPreviously @BigTech\\n📍 San Francisco" 62 + ) 17 63 """ 18 64 try: 19 - from atproto import Client, models 20 - 21 65 # Validate input 22 66 if display_name is None and bio is None: 23 - raise ValueError("At least one of display_name or bio must be provided.") 67 + return { 68 + "status": "error", 69 + "message": "Error: No fields provided for update. To resolve this, provide at least one field to update: " 70 + "either display_name, bio, or both. You cannot call this tool with both parameters as None. " 71 + "This is a common mistake and can be fixed by specifying which field(s) you want to update." 72 + } 73 + 24 74 if display_name is not None and len(display_name) > 64: 25 - raise ValueError("Display name exceeds 64 characters.") 75 + return { 76 + "status": "error", 77 + "message": f"Error: The display_name is too long (current length: {len(display_name)} characters). " 78 + f"To resolve this, shorten it to 64 characters or less (you need to remove " 79 + f"{len(display_name) - 64} characters). This is a Bluesky platform limitation that applies " 80 + f"to all users. Consider making the name more concise." 81 + } 82 + 26 83 if bio is not None and len(bio) > 256: 27 - raise ValueError("Bio exceeds 256 characters.") 84 + return { 85 + "status": "error", 86 + "message": f"Error: The bio is too long (current length: {len(bio)} characters). To resolve this, " 87 + f"shorten it to 256 characters or less (you need to remove {len(bio) - 256} characters). " 88 + f"This is a Bluesky platform limitation that applies to all users. Consider making the " 89 + f"bio more concise or breaking it into shorter sentences." 90 + } 28 91 29 92 username = os.environ.get("BSKY_USERNAME") 30 93 password = os.environ.get("BSKY_APP_PASSWORD") 31 94 if not username or not password: 32 - raise EnvironmentError("BSKY_USERNAME and BSKY_APP_PASSWORD environment variables must be set.") 95 + return { 96 + "status": "error", 97 + "message": "Error: Missing Bluesky authentication credentials. The BSKY_USERNAME and BSKY_APP_PASSWORD " 98 + "environment variables are not set. To resolve this, ask the user to configure these environment " 99 + "variables with valid Bluesky credentials. This is a configuration issue that the user needs to " 100 + "address before you can update their profile." 101 + } 33 102 34 103 # Initialize client and login 35 104 client = Client() 36 105 client.login(username, password) 37 106 38 - # ✅ Use the proper parameter model for get_record 39 - params = models.ComAtprotoRepoGetRecord.Params( 40 - repo=client.me.did, 41 - collection="app.bsky.actor.profile", 42 - rkey="self" 43 - ) 44 - record = client.com.atproto.repo.get_record(params) 45 - current_record = record.value 107 + # STEP 1A: Fetch current profile via get_profile (reliable for display name and bio) 108 + try: 109 + current_profile = client.get_profile(actor=client.me.did) 110 + except Exception as e: 111 + return { 112 + "status": "error", 113 + "message": f"Error: Failed to fetch current profile via AppView. To resolve this, verify you are authenticated " 114 + f"and try again. This error can occur due to authentication issues or temporary API problems. " 115 + f"API details: {str(e)}" 116 + } 117 + 118 + # Extract current display name and bio from profile 119 + current_display_name = getattr(current_profile, 'display_name', None) or "" 120 + current_bio = getattr(current_profile, 'description', None) or "" 121 + 122 + # STEP 1B: Fetch the complete raw profile record (for avatar, banner, etc.) 123 + # This gets ALL fields from the repository that we need to preserve 124 + try: 125 + params = models.ComAtprotoRepoGetRecord.Params( 126 + repo=client.me.did, 127 + collection="app.bsky.actor.profile", 128 + rkey="self" 129 + ) 130 + record = client.com.atproto.repo.get_record(params) 131 + current_record = record.value 132 + except Exception as e: 133 + return { 134 + "status": "error", 135 + "message": f"Error: Failed to fetch current profile record. To resolve this, verify you are authenticated " 136 + f"and try again. This error can occur due to authentication issues or temporary API problems. " 137 + f"API details: {str(e)}" 138 + } 46 139 47 140 # Convert model to dict if needed 48 141 if not isinstance(current_record, dict): ··· 51 144 except Exception: 52 145 current_record = current_record.__dict__ 53 146 54 - # Merge updates 147 + # STEP 2: Merge the updates into the existing record 148 + # Use values from get_profile for fields not being updated (most reliable source) 149 + # Start with the raw record to preserve avatar, banner, etc. 55 150 updated_record = dict(current_record) 151 + 152 + # For display name: use provided value, or fallback to current from get_profile 56 153 if display_name is not None: 57 154 updated_record["displayName"] = display_name 155 + else: 156 + # Ensure we keep the current display name from get_profile 157 + updated_record["displayName"] = current_display_name 158 + 159 + # For bio: use provided value, or fallback to current from get_profile 58 160 if bio is not None: 59 161 updated_record["description"] = bio 162 + else: 163 + # Ensure we keep the current bio from get_profile 164 + updated_record["description"] = current_bio 60 165 61 - # Ensure type field exists 166 + # Ensure the record type field exists (required by AT Protocol) 62 167 updated_record["$type"] = "app.bsky.actor.profile" 63 168 64 - # ✅ Put updated record back using correct model 65 - put_data = models.ComAtprotoRepoPutRecord.Data( 66 - repo=client.me.did, 67 - collection="app.bsky.actor.profile", 68 - rkey="self", 69 - record=updated_record 70 - ) 71 - client.com.atproto.repo.put_record(put_data) 169 + # STEP 3: Write the complete updated record back to Bluesky 170 + # AT Protocol requires sending the entire record, not just changed fields 171 + try: 172 + put_data = models.ComAtprotoRepoPutRecord.Data( 173 + repo=client.me.did, 174 + collection="app.bsky.actor.profile", 175 + rkey="self", 176 + record=updated_record 177 + ) 178 + client.com.atproto.repo.put_record(put_data) 179 + except Exception as e: 180 + return { 181 + "status": "error", 182 + "message": f"Error: Failed to update profile on Bluesky. To resolve this, verify your credentials are valid " 183 + f"and try again. This error can occur due to authentication issues, network problems, or temporary " 184 + f"API issues, and usually succeeds on retry. API details: {str(e)}" 185 + } 72 186 73 187 # Build structured success response 74 188 updates = {} ··· 85 199 } 86 200 87 201 except ImportError: 88 - raise ImportError("atproto package not installed. Install it with: pip install atproto") 202 + return { 203 + "status": "error", 204 + "message": "Error: The atproto Python package is not installed in the execution environment. " 205 + "To resolve this, the system administrator needs to install it using 'pip install atproto'. " 206 + "This is a dependency issue that prevents the tool from connecting to Bluesky. Once the " 207 + "package is installed, this tool will work normally." 208 + } 89 209 except Exception as e: 90 - raise RuntimeError(f"Error updating Bluesky profile: {e}") 210 + return { 211 + "status": "error", 212 + "message": f"Error: An unexpected issue occurred while updating the profile: {str(e)}. To resolve this, " 213 + f"verify your credentials are correct, ensure the display_name and bio meet length requirements, " 214 + f"and try again. This type of error is uncommon but can usually be resolved by retrying or " 215 + f"checking that your parameters are valid." 216 + }