a tool to help your Letta AI agents navigate bluesky

Update consent verification with new duplicate reply check

Changed files
+111 -8
tools
+111 -8
tools/bluesky/create_bluesky_post.py
··· 68 68 69 69 70 70 def _check_is_self(agent_did: str, target_did: str) -> bool: 71 - """Check 1: Self-Post Check (Free).""" 71 + """Check 2: Self-Post Check (Free).""" 72 72 return agent_did == target_did 73 73 74 74 ··· 126 126 raise 127 127 128 128 129 + def _check_already_replied(client, agent_did: str, agent_handle: str, reply_to_uri: str) -> None: 130 + """ 131 + Check 1: Duplicate Reply Prevention (Cheap - 1 API call). 132 + 133 + Prevents agents from replying multiple times to the same message. 134 + This check runs FIRST to block duplicates in ALL scenarios, including: 135 + - Replies to the agent's own posts 136 + - Replies to posts that mention the agent 137 + - Any other reply scenario 138 + 139 + Only checks direct replies, not deeper thread responses. 140 + 141 + When duplicates are found, provides detailed information about existing 142 + replies including URIs and content to help agents continue the conversation 143 + appropriately. 144 + 145 + Args: 146 + client: Authenticated Bluesky client 147 + agent_did: The agent's DID 148 + agent_handle: The agent's handle (username) 149 + reply_to_uri: URI of the post being replied to 150 + 151 + Raises: 152 + Exception: If agent has already replied directly to this message, 153 + with details about the existing reply(ies) 154 + """ 155 + try: 156 + # Fetch post with only direct replies (depth=1) 157 + response = client.app.bsky.feed.get_post_thread({ 158 + 'uri': reply_to_uri, 159 + 'depth': 1, # Only direct replies 160 + 'parentHeight': 0 # Don't fetch parents (not needed) 161 + }) 162 + 163 + # Validate response structure 164 + if not hasattr(response, 'thread'): 165 + return # Can't verify, proceed 166 + 167 + thread = response.thread 168 + if not hasattr(thread, 'replies') or not thread.replies: 169 + return # No replies yet, proceed 170 + 171 + # Collect all replies by this agent 172 + agent_replies = [] 173 + for reply in thread.replies: 174 + # Validate reply structure 175 + if not hasattr(reply, 'post'): 176 + continue 177 + if not hasattr(reply.post, 'author'): 178 + continue 179 + 180 + # Found agent's reply 181 + if reply.post.author.did == agent_did: 182 + agent_replies.append(reply) 183 + 184 + # If no duplicates found, proceed 185 + if not agent_replies: 186 + return 187 + 188 + # Get the most recent reply (last in list) 189 + # Note: Agents may have multiple replies if this issue happened before 190 + most_recent = agent_replies[-1] 191 + reply_post = most_recent.post 192 + reply_text = reply_post.record.text if hasattr(reply_post.record, 'text') else "[text unavailable]" 193 + reply_uri = reply_post.uri 194 + 195 + # Extract rkey from URI for web URL 196 + # URI format: at://did:plc:xyz/app.bsky.feed.post/rkey 197 + rkey = reply_uri.split('/')[-1] 198 + reply_url = f"https://bsky.app/profile/{agent_handle}/post/{rkey}" 199 + 200 + # Handle multiple replies case 201 + count_msg = "" 202 + if len(agent_replies) > 1: 203 + count_msg = f"\n\nNote: You have {len(agent_replies)} direct replies to this message. The most recent one is shown above." 204 + 205 + # Construct detailed error message 206 + error_msg = ( 207 + f"Message not sent: You have already replied directly to this message.{count_msg}\n\n" 208 + f"Your previous reply:\n\"{reply_text}\"\n\n" 209 + f"Reply URI: {reply_uri}\n" 210 + f"Web link: {reply_url}\n\n" 211 + f"Suggestions:\n" 212 + f"1. If you want to add more to your existing reply, use the URI above to continue that thread.\n" 213 + f"2. Make sure you're not repeating yourself - check what you already said before adding more.\n" 214 + f"3. Consider replying to one of the responses to your reply instead.\n" 215 + f"4. If you have something new to say, start a new top-level message with additional context." 216 + ) 217 + 218 + raise Exception(error_msg) 219 + 220 + except Exception as e: 221 + # If it's our duplicate reply exception, raise it 222 + if "already replied" in str(e): 223 + raise e 224 + # For other errors, re-raise to be caught by main error handler 225 + raise 226 + 227 + 129 228 def _check_thread_participation(client, agent_did: str, agent_handle: str, reply_to_uri: str) -> bool: 130 - """Check 3 & 4: Thread Participation and Mention Check (Expensive).""" 229 + """Check 5: Thread Participation and Mention Check (Expensive).""" 131 230 try: 132 231 # Fetch the thread 133 232 # depth=100 should be sufficient for most contexts, or we can walk up manually if needed. ··· 193 292 Raises Exception with specific message if consent denied or verification fails. 194 293 """ 195 294 try: 196 - # Check 1: Self-Post 295 + # Check 1: Duplicate Reply Prevention 296 + # This check must run BEFORE any early returns to prevent duplicates in all scenarios 297 + _check_already_replied(client, agent_did, agent_handle, reply_to_uri) 298 + 299 + # Check 2: Self-Post 197 300 if _check_is_self(agent_did, target_did): 198 301 return True 199 302 200 - # Check 2: Mention Check (Free/Cheap) 303 + # Check 3: Mention Check (Free/Cheap) 201 304 # If the post we are replying to mentions us, we can reply. 202 305 if parent_post_record: 203 306 # Check facets for mention ··· 206 309 for feature in facet.features: 207 310 if hasattr(feature, 'did') and feature.did == agent_did: 208 311 return True 209 - 312 + 210 313 # Fallback: Check text for handle 211 314 if hasattr(parent_post_record, 'text') and f"@{agent_handle}" in parent_post_record.text: 212 315 return True 213 316 214 - # Check 3: Follow Check 317 + # Check 4: Follow Check 215 318 # Rule: Target must follow agent. 216 - # Rule 3B: If root author is different from target, Root must ALSO follow agent. 319 + # Rule 4B: If root author is different from target, Root must ALSO follow agent. 217 320 218 321 target_follows = _check_follows(client, agent_did, target_did) 219 322 ··· 229 332 ) 230 333 return True 231 334 232 - # Check 4: Thread Participation 335 + # Check 5: Thread Participation 233 336 # This requires fetching the thread (Expensive) 234 337 if _check_thread_participation(client, agent_did, agent_handle, reply_to_uri): 235 338 return True