a digital person for bluesky

Implement retry logic for multiple bluesky_reply tool calls

- Collect all bluesky_reply tool calls instead of stopping at first one
- Try each reply candidate until one succeeds or all fail
- Remove fallback to text messages - only use tool calls
- Add detailed logging for debugging which candidate worked/failed
- Improves reliability when Letta API timeouts occur

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Changed files
+35 -35
+35 -35
bsky.py
··· 368 368 logger.debug("Successfully received response from Letta API") 369 369 logger.debug(f"Number of messages in response: {len(message_response.messages) if hasattr(message_response, 'messages') else 'N/A'}") 370 370 371 - # Extract the reply text and language from the agent's response 372 - reply_text = "" 373 - reply_lang = "en-US" # Default language 371 + # Extract all bluesky_reply tool calls from the agent's response 372 + reply_candidates = [] 374 373 logger.debug(f"Processing {len(message_response.messages)} response messages...") 375 374 376 375 for i, message in enumerate(message_response.messages, 1): ··· 390 389 else: 391 390 logger.debug(f" {i}. {msg_type}: <no content>") 392 391 393 - # Check if this is a ToolCallMessage with bluesky_reply tool 392 + # Collect bluesky_reply tool calls 394 393 if hasattr(message, 'tool_call') and message.tool_call: 395 394 if message.tool_call.name == 'bluesky_reply': 396 - # Parse the JSON arguments to get the message and language 397 395 try: 398 396 args = json.loads(message.tool_call.arguments) 399 397 reply_text = args.get('message', '') 400 - reply_lang = args.get('lang', 'en-US') # Extract language or use default 401 - logger.info(f"Extracted reply from tool call: {reply_text[:50]}... (lang: {reply_lang})") 402 - break 398 + reply_lang = args.get('lang', 'en-US') 399 + if reply_text: # Only add if there's actual content 400 + reply_candidates.append((reply_text, reply_lang)) 401 + logger.info(f"Found bluesky_reply candidate: {reply_text[:50]}... (lang: {reply_lang})") 403 402 except json.JSONDecodeError as e: 404 403 logger.error(f"Failed to parse tool call arguments: {e}") 405 404 406 - # Fallback to text message if available 407 - elif hasattr(message, 'text') and message.text: 408 - reply_text = message.text 409 - break 410 - 411 - if reply_text: 412 - # Print the generated reply for testing 413 - print(f"\n=== GENERATED REPLY ===") 414 - print(f"To: @{author_handle}") 415 - print(f"Reply: {reply_text}") 416 - print(f"Language: {reply_lang}") 417 - print(f"======================\n") 405 + if reply_candidates: 406 + logger.info(f"Found {len(reply_candidates)} bluesky_reply candidates, trying each until one succeeds...") 407 + 408 + for i, (reply_text, reply_lang) in enumerate(reply_candidates, 1): 409 + # Print the generated reply for testing 410 + print(f"\n=== GENERATED REPLY {i}/{len(reply_candidates)} ===") 411 + print(f"To: @{author_handle}") 412 + print(f"Reply: {reply_text}") 413 + print(f"Language: {reply_lang}") 414 + print(f"======================\n") 418 415 419 - # Send the reply with language 420 - logger.info(f"Sending reply: {reply_text[:50]}... (lang: {reply_lang})") 421 - response = bsky_utils.reply_to_notification( 422 - client=atproto_client, 423 - notification=notification_data, 424 - reply_text=reply_text, 425 - lang=reply_lang 426 - ) 416 + # Send the reply with language 417 + logger.info(f"Trying reply {i}/{len(reply_candidates)}: {reply_text[:50]}... (lang: {reply_lang})") 418 + response = bsky_utils.reply_to_notification( 419 + client=atproto_client, 420 + notification=notification_data, 421 + reply_text=reply_text, 422 + lang=reply_lang 423 + ) 427 424 428 - if response: 429 - logger.info(f"Successfully replied to @{author_handle}") 430 - return True 431 - else: 432 - logger.error(f"Failed to send reply to @{author_handle}") 433 - return False 425 + if response: 426 + logger.info(f"Successfully replied to @{author_handle} with candidate {i}") 427 + return True 428 + else: 429 + logger.warning(f"Failed to send reply candidate {i} to @{author_handle}, trying next...") 430 + 431 + # If we get here, all candidates failed 432 + logger.error(f"All {len(reply_candidates)} reply candidates failed for @{author_handle}") 433 + return False 434 434 else: 435 - logger.warning(f"No reply generated for mention from @{author_handle}, removing notification from queue") 435 + logger.warning(f"No bluesky_reply tool calls found for mention from @{author_handle}, removing notification from queue") 436 436 return True 437 437 438 438 except Exception as e: