a tool to help your Letta AI agents navigate bluesky
at main 18 kB view raw
1import os 2from typing import Optional, List, Dict, Any 3from atproto import Client, models 4 5 6def _normalize_datetime(date_str: Optional[str]) -> Optional[str]: 7 """ 8 Convert date string to ISO 8601 datetime format. 9 Accepts: "2024-01-15" or "2024-01-15T10:30:00Z" 10 Returns: "2024-01-15T00:00:00Z" or the original if already full datetime 11 """ 12 if not date_str: 13 return None 14 15 # If it's already a full datetime (contains 'T'), return as-is 16 if 'T' in date_str: 17 return date_str 18 19 # If it's just a date (YYYY-MM-DD), add time component 20 if len(date_str) == 10 and date_str.count('-') == 2: 21 return f"{date_str}T00:00:00Z" 22 23 # Return as-is if we can't parse it (let the API handle the error) 24 return date_str 25 26 27def search_bluesky( 28 query: str, 29 search_type: str = "posts", 30 author: Optional[str] = None, 31 mentions: Optional[str] = None, 32 hashtags: Optional[List[str]] = None, 33 domain: Optional[str] = None, 34 language: Optional[str] = None, 35 sort: str = "latest", 36 since: Optional[str] = None, 37 until: Optional[str] = None, 38 limit: int = 25 39) -> Dict[str, Any]: 40 """ 41 Search Bluesky for posts or users with comprehensive filtering and sorting options. 42 43 This tool allows you to search across Bluesky's content using keywords, phrases, and 44 multiple filter criteria. You can search for posts with specific hashtags, by particular 45 authors, mentioning certain users, linking to domains, or within date ranges. You can 46 also search for user accounts. The tool supports Lucene query syntax for advanced 47 searches with AND, OR, and NOT operators. 48 49 Use this tool when you need to find specific content on Bluesky, discover posts about 50 topics, find posts by or mentioning users, or search for user accounts. 51 52 Args: 53 query (str): The search keywords or phrases to find. This parameter is required and 54 cannot be empty. 55 56 - For simple searches: Use plain keywords like "artificial intelligence" 57 - For exact phrases: Use quotes like "climate change policy" 58 - For advanced searches: Use Lucene syntax with AND, OR, NOT operators 59 Example: "python AND (tutorial OR guide) NOT beginner" 60 61 The query is matched against post text content or user names/handles depending 62 on the search_type parameter. 63 64 search_type (str, optional): The type of content to search for. Defaults to "posts". 65 66 - "posts": Search through all Bluesky posts (status updates, replies, etc.) 67 - "users": Search for user accounts by handle or display name 68 69 This parameter determines which filters are available. Most filters only work 70 with "posts" search type. 71 72 author (Optional[str]): Filter posts to only those created by a specific user. 73 Only applies when search_type="posts". Defaults to None (search all users). 74 75 Provide the user's handle (e.g., "user.bsky.social") or their DID 76 (e.g., "did:plc:..."). This is useful for searching within a specific user's 77 posts. 78 79 mentions (Optional[str]): Filter posts to only those mentioning a specific user. 80 Only applies when search_type="posts". Defaults to None (no mention filter). 81 82 Provide the mentioned user's handle or DID. This finds posts that @mention 83 the specified user. 84 85 hashtags (Optional[List[str]]): Filter posts to only those containing specific hashtags. 86 Only applies when search_type="posts". Defaults to None (no hashtag filter). 87 88 IMPORTANT: Provide hashtag text WITHOUT the # symbol. 89 - Correct: ["python", "programming"] 90 - Incorrect: ["#python", "#programming"] 91 92 When multiple hashtags are provided, posts must contain ALL of them (AND logic). 93 94 domain (Optional[str]): Filter posts to only those containing links to a specific domain. 95 Only applies when search_type="posts". Defaults to None (no domain filter). 96 97 Provide just the domain name without protocol or path. 98 Examples: "nytimes.com", "github.com", "example.org" 99 100 language (Optional[str]): Filter posts to only those in a specific language. 101 Only applies when search_type="posts". Defaults to None (all languages). 102 103 Use ISO 639-1 language codes: 104 - "en" - English 105 - "es" - Spanish 106 - "ja" - Japanese 107 - "fr" - French 108 - "de" - German 109 - "pt" - Portuguese 110 111 sort (str, optional): Sort order for post results. Only applies when search_type="posts". 112 Defaults to "latest". 113 114 - "latest": Newest posts first (chronological order, most recent first) 115 - "top": Most popular posts first (by engagement metrics) 116 117 User searches are always sorted by relevance and ignore this parameter. 118 119 since (Optional[str]): Filter posts to only those created after this date/time. 120 Only applies when search_type="posts". Defaults to None (no start date). 121 122 Accepts two formats: 123 - Simple date: "2024-01-15" (automatically converts to midnight UTC) 124 - Full datetime: "2024-01-15T10:30:00Z" (ISO 8601 format) 125 126 until (Optional[str]): Filter posts to only those created before this date/time. 127 Only applies when search_type="posts". Defaults to None (no end date). 128 129 Accepts two formats: 130 - Simple date: "2024-12-31" (automatically converts to midnight UTC) 131 - Full datetime: "2024-12-31T23:59:59Z" (ISO 8601 format) 132 133 limit (int, optional): Maximum number of results to return. Defaults to 25. 134 135 Must be between 1 and 100. Higher limits take longer but provide more results. 136 Be mindful that very high limits may take significant time to process. 137 138 Returns: 139 Dict[str, Any]: A dictionary containing the search results with the following keys: 140 141 Common keys (all searches): 142 - status (str): Either "success" or "error" 143 - search_type (str): Either "posts" or "users" 144 - query (str): The search query that was executed 145 - result_count (int): Number of results found 146 147 For successful post searches: 148 - posts (List[Dict]): List of post objects, each containing: 149 - author (str): The handle of the post author 150 - authorDID (str): The DID of the post author 151 - uri (str): The AT Protocol URI of the post 152 - message (str): The text content of the post 153 - posted-datetime (str): When the post was created 154 - replies (int): Number of replies 155 - reposts (int): Number of reposts 156 - likes (int): Number of likes 157 - quotes (int): Number of quote posts 158 159 For successful user searches: 160 - users (List[Dict]): List of user objects, each containing: 161 - handle (str): The user's handle 162 - did (str): The user's DID 163 - display_name (str): The user's display name 164 - bio (str): The user's bio/description 165 - followers (int): Follower count 166 - following (int): Following count 167 - posts (int): Post count 168 - avatar_url (str): URL to avatar image (if present) 169 - profile_url (str): Link to view profile on bsky.app 170 171 For errors: 172 - message (str): Human-readable error description with guidance 173 174 Examples: 175 # Simple search for posts about AI 176 search_bluesky(query="artificial intelligence") 177 178 # Search for recent posts by a specific user 179 search_bluesky( 180 query="announcement", 181 author="bsky.app" 182 ) 183 184 # Find posts with specific hashtags (multiple tags = AND logic) 185 search_bluesky( 186 query="tutorial", 187 hashtags=["python", "programming"] 188 ) 189 190 # Search for popular posts mentioning someone 191 search_bluesky( 192 query="great work", 193 mentions="alice.bsky.social", 194 sort="top" 195 ) 196 197 # Find posts with links to a specific domain 198 search_bluesky( 199 query="article", 200 domain="nytimes.com" 201 ) 202 203 # Search posts in a specific language 204 search_bluesky( 205 query="news", 206 language="es" 207 ) 208 209 # Search posts within a date range 210 search_bluesky( 211 query="conference", 212 since="2024-01-01", 213 until="2024-12-31" 214 ) 215 216 # Search for user accounts 217 search_bluesky( 218 query="software engineer", 219 search_type="users" 220 ) 221 222 # Get top posts with more results 223 search_bluesky( 224 query="bluesky", 225 sort="top", 226 since="2024-10-28", 227 limit=50 228 ) 229 230 # Advanced search with Lucene syntax 231 search_bluesky( 232 query="python AND (tutorial OR guide) NOT beginner" 233 ) 234 """ 235 try: 236 # Validate query 237 if not query or len(query.strip()) == 0: 238 return { 239 "status": "error", 240 "message": "Error: The search query parameter is empty. To resolve this, provide keywords or " 241 "phrases to search for, such as 'artificial intelligence' or 'climate change'. " 242 "This is a common mistake and can be fixed by calling the tool again with a valid search query." 243 } 244 245 # Validate search_type 246 valid_search_types = ["posts", "users"] 247 if search_type not in valid_search_types: 248 return { 249 "status": "error", 250 "message": f"Error: The search_type '{search_type}' is not valid. To resolve this, use either 'posts' " 251 f"to search for posts or 'users' to search for user accounts. Feed search is not supported. " 252 f"This is a common parameter error that can be fixed by using one of the supported search types." 253 } 254 255 # Validate limit 256 if limit < 1 or limit > 100: 257 return { 258 "status": "error", 259 "message": f"Error: The limit parameter {limit} is out of range. To resolve this, choose a value between " 260 f"1 and 100. Lower limits return results faster, while higher limits provide more comprehensive " 261 f"results. This is a validation error that can be fixed by adjusting the limit parameter." 262 } 263 264 # Validate sort for posts 265 if search_type == "posts" and sort not in ["latest", "top"]: 266 return { 267 "status": "error", 268 "message": f"Error: The sort parameter '{sort}' is not valid for post searches. To resolve this, use " 269 f"'latest' for newest posts first (chronological) or 'top' for most popular posts first " 270 f"(by engagement). This is a parameter validation error that can be fixed by using a supported " 271 f"sort option." 272 } 273 274 # Validate hashtags format 275 if hashtags: 276 for tag in hashtags: 277 if tag.startswith('#'): 278 corrected = [t.lstrip('#') for t in hashtags] 279 return { 280 "status": "error", 281 "message": f"Error: The hashtag '{tag}' includes the # symbol, but hashtags should be provided " 282 f"without the # prefix. To resolve this, use {corrected} instead. This is a common " 283 f"formatting mistake that can be fixed by removing the # symbol from hashtag values." 284 } 285 286 # Get credentials 287 username = os.environ.get("BSKY_USERNAME") 288 password = os.environ.get("BSKY_APP_PASSWORD") 289 if not username or not password: 290 return { 291 "status": "error", 292 "message": "Error: Missing Bluesky authentication credentials. The BSKY_USERNAME and BSKY_APP_PASSWORD " 293 "environment variables are not set. To resolve this, ask the user to configure these environment " 294 "variables with valid Bluesky credentials. The user can generate an app password in Bluesky " 295 "Settings → App Passwords. This is a configuration issue that the user needs to address before " 296 "you can search Bluesky." 297 } 298 299 # Login to Bluesky 300 client = Client() 301 client.login(username, password) 302 303 if search_type == "posts": 304 # Normalize date formats to full ISO 8601 datetime 305 normalized_since = _normalize_datetime(since) 306 normalized_until = _normalize_datetime(until) 307 308 # Build search parameters 309 params = models.AppBskyFeedSearchPosts.Params( 310 q=query, 311 limit=min(limit, 100) 312 ) 313 314 # Add optional filters 315 if author: 316 params.author = author 317 if mentions: 318 params.mentions = mentions 319 if hashtags: 320 params.tag = hashtags 321 if domain: 322 params.domain = domain 323 if language: 324 params.lang = language 325 if sort: 326 params.sort = sort 327 if normalized_since: 328 params.since = normalized_since 329 if normalized_until: 330 params.until = normalized_until 331 332 # Execute search 333 try: 334 response = client.app.bsky.feed.search_posts(params) 335 except Exception as e: 336 return { 337 "status": "error", 338 "message": f"Error: Failed to search posts. The Bluesky API returned: {str(e)}. To resolve this, " 339 f"verify your search parameters are correct and try again. This type of error can occur due " 340 f"to invalid filter combinations, malformed date strings, temporary API issues, or network " 341 f"problems, and usually succeeds on retry." 342 } 343 344 # Transform results to compact format 345 posts = [] 346 for item in response.posts: 347 record = item.record 348 posts.append({ 349 "author": item.author.handle, 350 "authorDID": item.author.did, 351 "uri": item.uri, 352 "message": getattr(record, 'text', ''), 353 "posted-datetime": getattr(record, 'created_at', ''), 354 "replies": getattr(item, 'reply_count', 0), 355 "reposts": getattr(item, 'repost_count', 0), 356 "likes": getattr(item, 'like_count', 0), 357 "quotes": getattr(item, 'quote_count', 0) 358 }) 359 360 return { 361 "status": "success", 362 "search_type": "posts", 363 "query": query, 364 "result_count": len(posts), 365 "posts": posts 366 } 367 368 else: # search_type == "users" 369 # Build search parameters for users 370 params = models.AppBskyActorSearchActors.Params( 371 q=query, 372 limit=min(limit, 100) 373 ) 374 375 # Execute search 376 try: 377 response = client.app.bsky.actor.search_actors(params) 378 except Exception as e: 379 return { 380 "status": "error", 381 "message": f"Error: Failed to search users. The Bluesky API returned: {str(e)}. To resolve this, " 382 f"verify your search query is correct and try again. This type of error can occur due to " 383 f"temporary API issues or network problems, and usually succeeds on retry." 384 } 385 386 # Transform results to compact format 387 users = [] 388 for actor in response.actors: 389 users.append({ 390 "handle": actor.handle, 391 "did": actor.did, 392 "display_name": getattr(actor, 'display_name', '') or actor.handle, 393 "bio": getattr(actor, 'description', '') or '', 394 "followers": getattr(actor, 'followers_count', 0), 395 "following": getattr(actor, 'follows_count', 0), 396 "posts": getattr(actor, 'posts_count', 0), 397 "avatar_url": getattr(actor, 'avatar', None), 398 "profile_url": f"https://bsky.app/profile/{actor.handle}" 399 }) 400 401 return { 402 "status": "success", 403 "search_type": "users", 404 "query": query, 405 "result_count": len(users), 406 "users": users 407 } 408 409 except ImportError: 410 return { 411 "status": "error", 412 "message": "Error: The atproto Python package is not installed in the execution environment. " 413 "To resolve this, the system administrator needs to install it using 'pip install atproto'. " 414 "This is a dependency issue that prevents the tool from connecting to Bluesky. Once the " 415 "package is installed, this tool will work normally." 416 } 417 except Exception as e: 418 return { 419 "status": "error", 420 "message": f"Error: An unexpected issue occurred while searching Bluesky: {str(e)}. To resolve this, " 421 f"verify your credentials are correct, check your network connection, and ensure your search " 422 f"parameters are valid. This type of error is uncommon but can usually be resolved by retrying " 423 f"or adjusting the search parameters." 424 }