a digital person for bluesky
at main 6.5 kB view raw
1"""Feed tool for retrieving Bluesky feeds.""" 2from pydantic import BaseModel, Field 3from typing import Optional 4 5 6class FeedArgs(BaseModel): 7 feed_name: Optional[str] = Field(None, description="Named feed preset. Available feeds: 'home' (timeline), 'discover' (what's hot), 'ai-for-grownups', 'atmosphere', 'void-cafe'. If not provided, returns home timeline") 8 max_posts: int = Field(default=25, description="Maximum number of posts to retrieve (max 100)") 9 10 11def get_bluesky_feed(feed_name: str = None, max_posts: int = 25) -> str: 12 """ 13 Retrieve a Bluesky feed. 14 15 Args: 16 feed_name: Named feed preset - available options: 'home', 'discover', 'ai-for-grownups', 'atmosphere', 'void-cafe' 17 max_posts: Maximum number of posts to retrieve (max 100) 18 19 Returns: 20 YAML-formatted feed data with posts and metadata 21 """ 22 import os 23 import yaml 24 import requests 25 26 try: 27 # Predefined feed mappings (must be inside function for sandboxing) 28 feed_presets = { 29 "home": None, # Home timeline (default) 30 "discover": "at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/whats-hot", 31 "ai-for-grownups": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/ai-for-grownups", 32 "atmosphere": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/the-atmosphere", 33 "void-cafe": "at://did:plc:gfrmhdmjvxn2sjedzboeudef/app.bsky.feed.generator/void-cafe" 34 } 35 36 # Validate inputs 37 max_posts = min(max_posts, 100) 38 39 # Resolve feed URI from name 40 if feed_name: 41 # Handle case where agent passes 'FeedName.discover' instead of 'discover' 42 if '.' in feed_name and feed_name.startswith('FeedName.'): 43 feed_name = feed_name.split('.', 1)[1] 44 45 # Look up named preset 46 if feed_name not in feed_presets: 47 available_feeds = list(feed_presets.keys()) 48 raise Exception(f"Invalid feed name '{feed_name}'. Available feeds: {available_feeds}") 49 resolved_feed_uri = feed_presets[feed_name] 50 feed_display_name = feed_name 51 else: 52 # Default to home timeline 53 resolved_feed_uri = None 54 feed_display_name = "home" 55 56 # Get credentials from environment 57 username = os.getenv("BSKY_USERNAME") 58 password = os.getenv("BSKY_PASSWORD") 59 pds_host = os.getenv("PDS_URI", "https://bsky.social") 60 61 if not username or not password: 62 raise Exception("BSKY_USERNAME and BSKY_PASSWORD environment variables must be set") 63 64 # Create session 65 session_url = f"{pds_host}/xrpc/com.atproto.server.createSession" 66 session_data = { 67 "identifier": username, 68 "password": password 69 } 70 71 try: 72 session_response = requests.post(session_url, json=session_data, timeout=10) 73 session_response.raise_for_status() 74 session = session_response.json() 75 access_token = session.get("accessJwt") 76 77 if not access_token: 78 raise Exception("Failed to get access token from session") 79 except Exception as e: 80 raise Exception(f"Authentication failed. ({str(e)})") 81 82 # Get feed 83 headers = {"Authorization": f"Bearer {access_token}"} 84 85 if resolved_feed_uri: 86 # Custom feed 87 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getFeed" 88 params = { 89 "feed": resolved_feed_uri, 90 "limit": max_posts 91 } 92 feed_type = "custom" 93 else: 94 # Home timeline 95 feed_url = f"{pds_host}/xrpc/app.bsky.feed.getTimeline" 96 params = { 97 "limit": max_posts 98 } 99 feed_type = "home" 100 101 try: 102 response = requests.get(feed_url, headers=headers, params=params, timeout=10) 103 response.raise_for_status() 104 feed_data = response.json() 105 except Exception as e: 106 raise Exception(f"Failed to get feed. ({str(e)})") 107 108 # Format posts 109 posts = [] 110 for item in feed_data.get("feed", []): 111 post = item.get("post", {}) 112 author = post.get("author", {}) 113 record = post.get("record", {}) 114 115 post_data = { 116 "author": { 117 "handle": author.get("handle", ""), 118 "display_name": author.get("displayName", ""), 119 }, 120 "text": record.get("text", ""), 121 "created_at": record.get("createdAt", ""), 122 "uri": post.get("uri", ""), 123 "cid": post.get("cid", ""), 124 "like_count": post.get("likeCount", 0), 125 "repost_count": post.get("repostCount", 0), 126 "reply_count": post.get("replyCount", 0), 127 } 128 129 # Add repost info if present 130 if "reason" in item and item["reason"]: 131 reason = item["reason"] 132 if reason.get("$type") == "app.bsky.feed.defs#reasonRepost": 133 by = reason.get("by", {}) 134 post_data["reposted_by"] = { 135 "handle": by.get("handle", ""), 136 "display_name": by.get("displayName", ""), 137 } 138 139 # Add reply info if present 140 if "reply" in record and record["reply"]: 141 parent = record["reply"].get("parent", {}) 142 post_data["reply_to"] = { 143 "uri": parent.get("uri", ""), 144 "cid": parent.get("cid", ""), 145 } 146 147 posts.append(post_data) 148 149 # Format response 150 feed_result = { 151 "feed": { 152 "type": feed_type, 153 "name": feed_display_name, 154 "post_count": len(posts), 155 "posts": posts 156 } 157 } 158 159 if resolved_feed_uri: 160 feed_result["feed"]["uri"] = resolved_feed_uri 161 162 return yaml.dump(feed_result, default_flow_style=False, sort_keys=False) 163 164 except Exception as e: 165 raise Exception(f"Error retrieving feed: {str(e)}")