a tool to help your Letta AI agents navigate bluesky

refactored fetch bluesky posts tool

+77 -12
+77 -12
tools/bluesky/fetch_bluesky_posts.py
··· 11 11 Fetch posts from a Bluesky feed (user, following, or custom feed) with automatic pagination 12 12 and transform into compact agent-friendly format. 13 13 14 + IMPORTANT: The feed_type determines which feed to fetch from. 15 + ✅ Correct: feed_type="user", actor="alice.bsky.social" 16 + ✅ Correct: feed_type="following" 17 + ✅ Correct: feed_type="custom", feed_id="at://did:plc:xyz/app.bsky.feed.generator/abc" 18 + ❌ Wrong: feed_type="user" (without actor parameter) 19 + ❌ Wrong: feed_type="custom" (without feed_id parameter) 20 + ❌ Wrong: feed_type="timeline" (not a valid type) 21 + ❌ Wrong: limit=150 (exceeds maximum of 100) 22 + 14 23 Args: 15 - feed_type: Type of feed to fetch. Must be one of: "user", "following", or "custom" 16 - actor: The handle or DID of the user. Required when feed_type is "user" 17 - feed_id: The AT URI of the custom feed. Required when feed_type is "custom" 18 - limit: Maximum number of posts to retrieve (1-100). Default is 25 24 + feed_type (str): Type of feed to fetch. Must be "user", "following", or "custom". 25 + - "user": Fetch posts from a specific user's profile 26 + - "following": Fetch posts from accounts you follow (your timeline) 27 + - "custom": Fetch posts from a custom algorithm feed 28 + actor (Optional[str]): The handle (e.g., "alice.bsky.social") or DID of the user. 29 + Required when feed_type is "user". Leave as None for other feed types. 30 + feed_id (Optional[str]): The AT URI of the custom feed (e.g., "at://did:plc:.../app.bsky.feed.generator/..."). 31 + Required when feed_type is "custom". Leave as None for other feed types. 32 + limit (int): Maximum number of posts to retrieve. Must be between 1-100. Default is 25. 33 + Higher limits may take longer but return more posts. 19 34 20 35 Returns: 21 - A dictionary containing the status, feed metadata, and a list of compact post objects 36 + Dict: On success, returns dict with "status"="success", feed metadata, and "posts" list. 37 + On error, returns dict with "status"="error" and "message" describing the issue. 38 + 39 + What you can do: 40 + ✓ Fetch posts from any user's profile by handle or DID 41 + ✓ Fetch your following timeline 42 + ✓ Fetch posts from custom algorithm feeds 43 + ✓ Retrieve 1-100 posts with automatic pagination 44 + ✓ Get compact post data with author, message, URI, and engagement metrics 45 + ✓ Access post URIs for use with other tools (like, repost, reply, etc.) 46 + 47 + Examples: 48 + Fetch posts from a specific user: 49 + fetch_bluesky_posts(feed_type="user", actor="bsky.app", limit=10) 50 + 51 + Fetch your following timeline: 52 + fetch_bluesky_posts(feed_type="following", limit=50) 53 + 54 + Fetch from a custom feed: 55 + fetch_bluesky_posts( 56 + feed_type="custom", 57 + feed_id="at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 58 + limit=25 59 + ) 60 + 61 + Get recent posts from a user by handle: 62 + fetch_bluesky_posts(feed_type="user", actor="alice.bsky.social") 63 + 64 + Get posts from a user by DID: 65 + fetch_bluesky_posts(feed_type="user", actor="did:plc:abc123xyz") 22 66 """ 23 67 try: 24 68 from atproto import Client, models 25 69 26 70 if limit < 1 or limit > 100: 27 - raise ValueError("Limit must be between 1 and 100.") 71 + return { 72 + "status": "error", 73 + "message": f"Limit must be between 1 and 100, but received {limit}. Adjust the limit and try again." 74 + } 28 75 29 76 username = os.environ.get("BSKY_USERNAME") 30 77 password = os.environ.get("BSKY_APP_PASSWORD") 31 78 if not username or not password: 32 - raise EnvironmentError("BSKY_USERNAME and BSKY_APP_PASSWORD must be set.") 79 + return { 80 + "status": "error", 81 + "message": "Environment variables BSKY_USERNAME and BSKY_APP_PASSWORD are not set. Set these variables with your Bluesky credentials." 82 + } 33 83 34 84 client = Client() 35 85 client.login(username, password) ··· 43 93 # Fetch posts using your working logic 44 94 if feed_type == "user": 45 95 if not actor: 46 - raise ValueError("Actor must be specified for user feed.") 96 + return { 97 + "status": "error", 98 + "message": "Actor must be specified for user feed. Provide a handle like 'alice.bsky.social' or a DID." 99 + } 47 100 params = models.AppBskyFeedGetAuthorFeed.Params( 48 101 actor=actor, 49 102 limit=min(50, remaining), ··· 58 111 resp = client.app.bsky.feed.get_timeline(params) 59 112 elif feed_type == "custom": 60 113 if not feed_id: 61 - raise ValueError("feed_id must be specified for custom feed.") 114 + return { 115 + "status": "error", 116 + "message": "feed_id must be specified for custom feed. Provide an AT URI like 'at://did:plc:.../app.bsky.feed.generator/...'." 117 + } 62 118 params = models.AppBskyFeedGetFeed.Params( 63 119 feed=feed_id, 64 120 limit=min(50, remaining), ··· 66 122 ) 67 123 resp = client.app.bsky.feed.get_feed(params) 68 124 else: 69 - raise ValueError(f"Unsupported feed_type: {feed_type}") 125 + return { 126 + "status": "error", 127 + "message": f"Unsupported feed_type: '{feed_type}'. Must be 'user', 'following', or 'custom'." 128 + } 70 129 71 130 # Append raw posts from current page 72 131 posts.extend([item.model_dump() for item in resp.feed]) ··· 108 167 } 109 168 110 169 except ImportError: 111 - raise ImportError("atproto package not installed. Install with: pip install atproto") 170 + return { 171 + "status": "error", 172 + "message": "The atproto package is not installed. Install it with: pip install atproto" 173 + } 112 174 except Exception as e: 113 - raise RuntimeError(f"Error fetching Bluesky posts: {e}") 175 + return { 176 + "status": "error", 177 + "message": f"Failed to fetch Bluesky posts: {str(e)}. Check the error details and try again." 178 + }