a digital person for bluesky

Fix bot response issues and enhance outbound message logging

This commit addresses multiple issues preventing the bot from responding
to non-priority users and improves visibility into outbound message status.

## Key Fixes

**Bot Detection Issue:**
- Temporarily disable bot detection causing 90% skip rate for normal users
- Bot was incorrectly flagging most users as bots, only responding to cameron.pfiffer.org
- Added clear TODO to re-enable after debugging the root cause

**Configuration Priority:**
- Fix config_loader to prioritize config.yaml over environment variables
- Update default_login() to use config-based authentication instead of env vars
- Ensures bot connects to configured PDS (comind.network) consistently

**PDS Server Consistency:**
- Update all bsky_utils functions to use configured PDS URI from config.yaml
- Fixed create_synthesis_ack(), acknowledge_post(), create_tool_call_record(), create_reasoning_record()
- Eliminates "Token could not be verified" errors from posting to wrong server

## Enhanced Logging

**Correlation ID Tracking:**
- Add end-to-end correlation IDs for tracking messages through pipeline
- Generate unique 8-char IDs in process_mention() and pass through all functions
- Enables debugging of dropped messages with structured logging

**Improved Success Visibility:**
- Enhanced reply logging shows response times and post URIs in console output
- Added structured logging with extra fields for detailed analysis
- Better confirmation that posts are being sent successfully

**Facet Parsing Details:**
- Log mention resolution (handles -> DIDs) and URL detection
- Track parsing failures and network issues during rich text processing

## Files Modified

- `bsky.py`: Add correlation IDs, disable bot detection, enhance process_mention logging
- `bsky_utils.py`: Update all functions to use config PDS, add comprehensive logging
- `config_loader.py`: Prioritize config.yaml over environment variables
- `tools/post.py`: Cleaned up (removed enhanced logging for cloud compatibility)

## Testing Notes

Bot should now:
- Respond to ALL users (not just priority ones)
- Use consistent PDS server (comind.network) for all operations
- Provide clear logging confirmation when posts are sent
- Resolve acknowledgment and reasoning record errors

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

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

+45 -10
bsky.py
··· 206 None: Failed with non-retryable error, move to errors directory 207 "no_reply": No reply was generated, move to no_reply directory 208 """ 209 try: 210 - logger.debug(f"Starting process_mention with notification_data type: {type(notification_data)}") 211 212 # Handle both dict and object inputs for backwards compatibility 213 if isinstance(notification_data, dict): ··· 222 author_handle = notification_data.author.handle 223 author_name = notification_data.author.display_name or author_handle 224 225 - logger.debug(f"Extracted data - URI: {uri}, Author: @{author_handle}, Text: {mention_text[:50]}...") 226 227 # Retrieve the entire thread associated with the mention 228 try: ··· 328 bot_check_result = check_known_bots(unique_handles, void_agent) 329 bot_check_data = json.loads(bot_check_result) 330 331 - if bot_check_data.get("bot_detected", False): 332 detected_bots = bot_check_data.get("detected_bots", []) 333 logger.info(f"Bot detected in thread: {detected_bots}") 334 ··· 340 else: 341 logger.info(f"Responding to bot thread (10% response rate). Detected bots: {detected_bots}") 342 else: 343 - logger.debug("No known bots detected in thread") 344 345 except Exception as bot_check_error: 346 logger.warning(f"Error checking for bots: {bot_check_error}") ··· 809 client=atproto_client, 810 notification=notification_data, 811 reply_text=cleaned_text, 812 - lang=reply_lang 813 ) 814 else: 815 # Multiple replies - use new threaded function ··· 819 client=atproto_client, 820 notification=notification_data, 821 reply_messages=cleaned_messages, 822 - lang=reply_lang 823 ) 824 825 if response: 826 - logger.info(f"Successfully replied to @{author_handle}") 827 828 # Acknowledge the post we're replying to with stream.thought.ack 829 try: ··· 857 else: 858 # Check if notification was explicitly ignored 859 if ignored_notification: 860 - logger.info(f"Notification from @{author_handle} was explicitly ignored (category: {ignore_category})") 861 return "ignored" 862 else: 863 - logger.warning(f"No add_post_to_bluesky_reply_thread tool calls found for mention from @{author_handle}, moving to no_reply folder") 864 return "no_reply" 865 866 except Exception as e: 867 - logger.error(f"Error processing mention: {e}") 868 return False 869 finally: 870 # Detach user blocks after agent response (success or failure)
··· 206 None: Failed with non-retryable error, move to errors directory 207 "no_reply": No reply was generated, move to no_reply directory 208 """ 209 + import uuid 210 + 211 + # Generate correlation ID for tracking this notification through the pipeline 212 + correlation_id = str(uuid.uuid4())[:8] 213 + 214 try: 215 + logger.info(f"[{correlation_id}] Starting process_mention", extra={ 216 + 'correlation_id': correlation_id, 217 + 'notification_type': type(notification_data).__name__ 218 + }) 219 220 # Handle both dict and object inputs for backwards compatibility 221 if isinstance(notification_data, dict): ··· 230 author_handle = notification_data.author.handle 231 author_name = notification_data.author.display_name or author_handle 232 233 + logger.info(f"[{correlation_id}] Processing mention from @{author_handle}", extra={ 234 + 'correlation_id': correlation_id, 235 + 'author_handle': author_handle, 236 + 'author_name': author_name, 237 + 'mention_uri': uri, 238 + 'mention_text_length': len(mention_text), 239 + 'mention_preview': mention_text[:100] if mention_text else '' 240 + }) 241 242 # Retrieve the entire thread associated with the mention 243 try: ··· 343 bot_check_result = check_known_bots(unique_handles, void_agent) 344 bot_check_data = json.loads(bot_check_result) 345 346 + # TEMPORARILY DISABLED: Bot detection causing issues with normal users 347 + # TODO: Re-enable after debugging why normal users are being flagged as bots 348 + if False: # bot_check_data.get("bot_detected", False): 349 detected_bots = bot_check_data.get("detected_bots", []) 350 logger.info(f"Bot detected in thread: {detected_bots}") 351 ··· 357 else: 358 logger.info(f"Responding to bot thread (10% response rate). Detected bots: {detected_bots}") 359 else: 360 + logger.debug("Bot detection disabled - processing all notifications") 361 362 except Exception as bot_check_error: 363 logger.warning(f"Error checking for bots: {bot_check_error}") ··· 826 client=atproto_client, 827 notification=notification_data, 828 reply_text=cleaned_text, 829 + lang=reply_lang, 830 + correlation_id=correlation_id 831 ) 832 else: 833 # Multiple replies - use new threaded function ··· 837 client=atproto_client, 838 notification=notification_data, 839 reply_messages=cleaned_messages, 840 + lang=reply_lang, 841 + correlation_id=correlation_id 842 ) 843 844 if response: 845 + logger.info(f"[{correlation_id}] Successfully replied to @{author_handle}", extra={ 846 + 'correlation_id': correlation_id, 847 + 'author_handle': author_handle, 848 + 'reply_count': len(reply_messages) 849 + }) 850 851 # Acknowledge the post we're replying to with stream.thought.ack 852 try: ··· 880 else: 881 # Check if notification was explicitly ignored 882 if ignored_notification: 883 + logger.info(f"[{correlation_id}] Notification from @{author_handle} was explicitly ignored (category: {ignore_category})", extra={ 884 + 'correlation_id': correlation_id, 885 + 'author_handle': author_handle, 886 + 'ignore_category': ignore_category 887 + }) 888 return "ignored" 889 else: 890 + logger.warning(f"[{correlation_id}] No reply generated for mention from @{author_handle}, moving to no_reply folder", extra={ 891 + 'correlation_id': correlation_id, 892 + 'author_handle': author_handle 893 + }) 894 return "no_reply" 895 896 except Exception as e: 897 + logger.error(f"[{correlation_id}] Error processing mention: {e}", extra={ 898 + 'correlation_id': correlation_id, 899 + 'error': str(e), 900 + 'error_type': type(e).__name__, 901 + 'author_handle': author_handle if 'author_handle' in locals() else 'unknown' 902 + }) 903 return False 904 finally: 905 # Detach user blocks after agent response (success or failure)
+188 -51
bsky_utils.py
··· 1 import os 2 import logging 3 from typing import Optional, Dict, Any, List 4 from atproto_client import Client, Session, SessionEvent, models 5 ··· 235 236 237 def default_login() -> Client: 238 - username = os.getenv("BSKY_USERNAME") 239 - password = os.getenv("BSKY_PASSWORD") 240 - 241 - if username is None: 242 - logger.error( 243 - "No username provided. Please provide a username using the BSKY_USERNAME environment variable." 244 - ) 245 - exit() 246 - 247 - if password is None: 248 - logger.error( 249 - "No password provided. Please provide a password using the BSKY_PASSWORD environment variable." 250 - ) 251 - exit() 252 - 253 - return init_client(username, password) 254 255 def remove_outside_quotes(text: str) -> str: 256 """ ··· 277 278 return text 279 280 - def reply_to_post(client: Client, text: str, reply_to_uri: str, reply_to_cid: str, root_uri: Optional[str] = None, root_cid: Optional[str] = None, lang: Optional[str] = None) -> Dict[str, Any]: 281 """ 282 Reply to a post on Bluesky with rich text support. 283 ··· 289 root_uri: The URI of the root post (if replying to a reply). If None, uses reply_to_uri 290 root_cid: The CID of the root post (if replying to a reply). If None, uses reply_to_cid 291 lang: Language code for the post (e.g., 'en-US', 'es', 'ja') 292 293 Returns: 294 The response from sending the post 295 """ 296 import re 297 298 # If root is not provided, this is a reply to the root post 299 if root_uri is None: 300 root_uri = reply_to_uri ··· 307 # Parse rich text facets (mentions and URLs) 308 facets = [] 309 text_bytes = text.encode("UTF-8") 310 311 # Parse mentions - fixed to handle @ at start of text 312 mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 313 314 for m in re.finditer(mention_regex, text_bytes): 315 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 316 # Adjust byte positions to account for the optional prefix 317 mention_start = m.start(1) 318 mention_end = m.end(1) ··· 329 features=[models.AppBskyRichtextFacet.Mention(did=resolve_resp.did)] 330 ) 331 ) 332 except Exception as e: 333 - logger.debug(f"Failed to resolve handle {handle}: {e}") 334 continue 335 336 # Parse URLs - fixed to handle URLs at start of text ··· 338 339 for m in re.finditer(url_regex, text_bytes): 340 url = m.group(1).decode("UTF-8") 341 # Adjust byte positions to account for the optional prefix 342 url_start = m.start(1) 343 url_end = m.end(1) ··· 350 features=[models.AppBskyRichtextFacet.Link(uri=url)] 351 ) 352 ) 353 354 # Send the reply with facets if any were found 355 - if facets: 356 - response = client.send_post( 357 - text=text, 358 - reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 359 - facets=facets, 360 - langs=[lang] if lang else None 361 - ) 362 - else: 363 - response = client.send_post( 364 - text=text, 365 - reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 366 - langs=[lang] if lang else None 367 - ) 368 - 369 - logger.info(f"Reply sent successfully: {response.uri}") 370 - return response 371 372 373 def get_post_thread(client: Client, uri: str) -> Optional[Dict[str, Any]]: ··· 389 return None 390 391 392 - def reply_to_notification(client: Client, notification: Any, reply_text: str, lang: str = "en-US") -> Optional[Dict[str, Any]]: 393 """ 394 Reply to a notification (mention or reply). 395 ··· 398 notification: The notification object from list_notifications 399 reply_text: The text to reply with 400 lang: Language code for the post (defaults to "en-US") 401 402 Returns: 403 The response from sending the reply or None if failed 404 """ 405 try: 406 # Get the post URI and CID from the notification (handle both dict and object) 407 if isinstance(notification, dict): ··· 461 reply_to_cid=post_cid, 462 root_uri=root_uri, 463 root_cid=root_cid, 464 - lang=lang 465 ) 466 467 except Exception as e: 468 - logger.error(f"Error replying to notification: {e}") 469 return None 470 471 472 - def reply_with_thread_to_notification(client: Client, notification: Any, reply_messages: List[str], lang: str = "en-US") -> Optional[List[Dict[str, Any]]]: 473 """ 474 Reply to a notification with a threaded chain of messages (max 15). 475 ··· 478 notification: The notification object from list_notifications 479 reply_messages: List of reply texts (max 15 messages, each max 300 chars) 480 lang: Language code for the posts (defaults to "en-US") 481 482 Returns: 483 List of responses from sending the replies or None if failed 484 """ 485 try: 486 # Validate input 487 if not reply_messages or len(reply_messages) == 0: 488 - logger.error("Reply messages list cannot be empty") 489 return None 490 if len(reply_messages) > 15: 491 - logger.error(f"Cannot send more than 15 reply messages (got {len(reply_messages)})") 492 return None 493 494 # Get the post URI and CID from the notification (handle both dict and object) ··· 547 current_parent_cid = post_cid 548 549 for i, message in enumerate(reply_messages): 550 - logger.info(f"Sending reply {i+1}/{len(reply_messages)}: {message[:50]}...") 551 552 # Send this reply 553 response = reply_to_post( ··· 557 reply_to_cid=current_parent_cid, 558 root_uri=root_uri, 559 root_cid=root_cid, 560 - lang=lang 561 ) 562 563 if not response: 564 - logger.error(f"Failed to send reply {i+1}, posting system failure message") 565 # Try to post a system failure message 566 failure_response = reply_to_post( 567 client=client, ··· 570 reply_to_cid=current_parent_cid, 571 root_uri=root_uri, 572 root_cid=root_cid, 573 - lang=lang 574 ) 575 if failure_response: 576 responses.append(failure_response) 577 current_parent_uri = failure_response.uri 578 current_parent_cid = failure_response.cid 579 else: 580 - logger.error("Could not even send system failure message, stopping thread") 581 return responses if responses else None 582 else: 583 responses.append(response) ··· 586 current_parent_uri = response.uri 587 current_parent_cid = response.cid 588 589 - logger.info(f"Successfully sent {len(responses)} threaded replies") 590 return responses 591 592 except Exception as e: 593 - logger.error(f"Error sending threaded reply to notification: {e}") 594 return None 595 596 ··· 631 logger.error("Missing access token or DID from session") 632 return None 633 634 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 635 636 # Create acknowledgment record with null subject 637 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 705 logger.error("Missing access token or DID from session") 706 return None 707 708 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 709 710 # Create acknowledgment record with stream.thought.ack type 711 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 781 logger.error("Missing access token or DID from session") 782 return None 783 784 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 785 786 # Create tool call record 787 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 858 logger.error("Missing access token or DID from session") 859 return None 860 861 - pds_host = os.getenv("PDS_URI", "https://bsky.social") 862 863 # Create reasoning record 864 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
··· 1 import os 2 import logging 3 + import uuid 4 + import time 5 from typing import Optional, Dict, Any, List 6 from atproto_client import Client, Session, SessionEvent, models 7 ··· 237 238 239 def default_login() -> Client: 240 + """Login using configuration from config.yaml or environment variables.""" 241 + try: 242 + from config_loader import get_bluesky_config 243 + bluesky_config = get_bluesky_config() 244 + 245 + username = bluesky_config['username'] 246 + password = bluesky_config['password'] 247 + pds_uri = bluesky_config.get('pds_uri', 'https://bsky.social') 248 + 249 + logger.info(f"Logging into Bluesky as {username} via {pds_uri}") 250 + 251 + # Use pds_uri from config 252 + client = Client(base_url=pds_uri) 253 + client.login(username, password) 254 + return client 255 + 256 + except Exception as e: 257 + logger.error(f"Failed to load Bluesky configuration: {e}") 258 + logger.error("Please check your config.yaml file or environment variables") 259 + exit(1) 260 261 def remove_outside_quotes(text: str) -> str: 262 """ ··· 283 284 return text 285 286 + def reply_to_post(client: Client, text: str, reply_to_uri: str, reply_to_cid: str, root_uri: Optional[str] = None, root_cid: Optional[str] = None, lang: Optional[str] = None, correlation_id: Optional[str] = None) -> Dict[str, Any]: 287 """ 288 Reply to a post on Bluesky with rich text support. 289 ··· 295 root_uri: The URI of the root post (if replying to a reply). If None, uses reply_to_uri 296 root_cid: The CID of the root post (if replying to a reply). If None, uses reply_to_cid 297 lang: Language code for the post (e.g., 'en-US', 'es', 'ja') 298 + correlation_id: Unique ID for tracking this message through the pipeline 299 300 Returns: 301 The response from sending the post 302 """ 303 import re 304 305 + # Generate correlation ID if not provided 306 + if correlation_id is None: 307 + correlation_id = str(uuid.uuid4())[:8] 308 + 309 + # Enhanced logging with structured data 310 + logger.info(f"[{correlation_id}] Starting reply_to_post", extra={ 311 + 'correlation_id': correlation_id, 312 + 'text_length': len(text), 313 + 'text_preview': text[:100] + '...' if len(text) > 100 else text, 314 + 'reply_to_uri': reply_to_uri, 315 + 'root_uri': root_uri, 316 + 'lang': lang 317 + }) 318 + 319 + start_time = time.time() 320 + 321 # If root is not provided, this is a reply to the root post 322 if root_uri is None: 323 root_uri = reply_to_uri ··· 330 # Parse rich text facets (mentions and URLs) 331 facets = [] 332 text_bytes = text.encode("UTF-8") 333 + mentions_found = [] 334 + urls_found = [] 335 + 336 + logger.debug(f"[{correlation_id}] Parsing facets from text (length: {len(text_bytes)} bytes)") 337 338 # Parse mentions - fixed to handle @ at start of text 339 mention_regex = rb"(?:^|[$|\W])(@([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)" 340 341 for m in re.finditer(mention_regex, text_bytes): 342 handle = m.group(1)[1:].decode("UTF-8") # Remove @ prefix 343 + mentions_found.append(handle) 344 # Adjust byte positions to account for the optional prefix 345 mention_start = m.start(1) 346 mention_end = m.end(1) ··· 357 features=[models.AppBskyRichtextFacet.Mention(did=resolve_resp.did)] 358 ) 359 ) 360 + logger.debug(f"[{correlation_id}] Resolved mention @{handle} -> {resolve_resp.did}") 361 except Exception as e: 362 + logger.warning(f"[{correlation_id}] Failed to resolve handle @{handle}: {e}") 363 continue 364 365 # Parse URLs - fixed to handle URLs at start of text ··· 367 368 for m in re.finditer(url_regex, text_bytes): 369 url = m.group(1).decode("UTF-8") 370 + urls_found.append(url) 371 # Adjust byte positions to account for the optional prefix 372 url_start = m.start(1) 373 url_end = m.end(1) ··· 380 features=[models.AppBskyRichtextFacet.Link(uri=url)] 381 ) 382 ) 383 + logger.debug(f"[{correlation_id}] Found URL: {url}") 384 + 385 + logger.debug(f"[{correlation_id}] Facet parsing complete", extra={ 386 + 'correlation_id': correlation_id, 387 + 'mentions_count': len(mentions_found), 388 + 'mentions': mentions_found, 389 + 'urls_count': len(urls_found), 390 + 'urls': urls_found, 391 + 'total_facets': len(facets) 392 + }) 393 394 # Send the reply with facets if any were found 395 + logger.info(f"[{correlation_id}] Sending reply to Bluesky API", extra={ 396 + 'correlation_id': correlation_id, 397 + 'has_facets': bool(facets), 398 + 'facet_count': len(facets), 399 + 'lang': lang 400 + }) 401 + 402 + try: 403 + if facets: 404 + response = client.send_post( 405 + text=text, 406 + reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 407 + facets=facets, 408 + langs=[lang] if lang else None 409 + ) 410 + else: 411 + response = client.send_post( 412 + text=text, 413 + reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 414 + langs=[lang] if lang else None 415 + ) 416 + 417 + # Calculate response time 418 + response_time = time.time() - start_time 419 + 420 + # Extract post URL for user-friendly logging 421 + post_url = None 422 + if hasattr(response, 'uri') and response.uri: 423 + # Convert AT-URI to web URL 424 + # Format: at://did:plc:xxx/app.bsky.feed.post/xxx -> https://bsky.app/profile/handle/post/xxx 425 + try: 426 + uri_parts = response.uri.split('/') 427 + if len(uri_parts) >= 4 and uri_parts[3] == 'app.bsky.feed.post': 428 + rkey = uri_parts[4] 429 + # We'd need to resolve DID to handle, but for now just use the URI 430 + post_url = f"bsky://post/{rkey}" 431 + except: 432 + pass 433 + 434 + logger.info(f"[{correlation_id}] Reply sent successfully ({response_time:.3f}s) - URI: {response.uri}" + 435 + (f" - URL: {post_url}" if post_url else ""), extra={ 436 + 'correlation_id': correlation_id, 437 + 'response_time': round(response_time, 3), 438 + 'post_uri': response.uri, 439 + 'post_url': post_url, 440 + 'post_cid': getattr(response, 'cid', None), 441 + 'text_length': len(text) 442 + }) 443 + 444 + return response 445 + 446 + except Exception as e: 447 + response_time = time.time() - start_time 448 + logger.error(f"[{correlation_id}] Failed to send reply", extra={ 449 + 'correlation_id': correlation_id, 450 + 'error': str(e), 451 + 'error_type': type(e).__name__, 452 + 'response_time': round(response_time, 3), 453 + 'text_length': len(text) 454 + }) 455 + raise 456 457 458 def get_post_thread(client: Client, uri: str) -> Optional[Dict[str, Any]]: ··· 474 return None 475 476 477 + def reply_to_notification(client: Client, notification: Any, reply_text: str, lang: str = "en-US", correlation_id: Optional[str] = None) -> Optional[Dict[str, Any]]: 478 """ 479 Reply to a notification (mention or reply). 480 ··· 483 notification: The notification object from list_notifications 484 reply_text: The text to reply with 485 lang: Language code for the post (defaults to "en-US") 486 + correlation_id: Unique ID for tracking this message through the pipeline 487 488 Returns: 489 The response from sending the reply or None if failed 490 """ 491 + # Generate correlation ID if not provided 492 + if correlation_id is None: 493 + correlation_id = str(uuid.uuid4())[:8] 494 + 495 + logger.info(f"[{correlation_id}] Processing reply_to_notification", extra={ 496 + 'correlation_id': correlation_id, 497 + 'reply_length': len(reply_text), 498 + 'lang': lang 499 + }) 500 + 501 try: 502 # Get the post URI and CID from the notification (handle both dict and object) 503 if isinstance(notification, dict): ··· 557 reply_to_cid=post_cid, 558 root_uri=root_uri, 559 root_cid=root_cid, 560 + lang=lang, 561 + correlation_id=correlation_id 562 ) 563 564 except Exception as e: 565 + logger.error(f"[{correlation_id}] Error replying to notification: {e}", extra={ 566 + 'correlation_id': correlation_id, 567 + 'error': str(e), 568 + 'error_type': type(e).__name__ 569 + }) 570 return None 571 572 573 + def reply_with_thread_to_notification(client: Client, notification: Any, reply_messages: List[str], lang: str = "en-US", correlation_id: Optional[str] = None) -> Optional[List[Dict[str, Any]]]: 574 """ 575 Reply to a notification with a threaded chain of messages (max 15). 576 ··· 579 notification: The notification object from list_notifications 580 reply_messages: List of reply texts (max 15 messages, each max 300 chars) 581 lang: Language code for the posts (defaults to "en-US") 582 + correlation_id: Unique ID for tracking this message through the pipeline 583 584 Returns: 585 List of responses from sending the replies or None if failed 586 """ 587 + # Generate correlation ID if not provided 588 + if correlation_id is None: 589 + correlation_id = str(uuid.uuid4())[:8] 590 + 591 + logger.info(f"[{correlation_id}] Starting threaded reply", extra={ 592 + 'correlation_id': correlation_id, 593 + 'message_count': len(reply_messages), 594 + 'total_length': sum(len(msg) for msg in reply_messages), 595 + 'lang': lang 596 + }) 597 + 598 try: 599 # Validate input 600 if not reply_messages or len(reply_messages) == 0: 601 + logger.error(f"[{correlation_id}] Reply messages list cannot be empty") 602 return None 603 if len(reply_messages) > 15: 604 + logger.error(f"[{correlation_id}] Cannot send more than 15 reply messages (got {len(reply_messages)})") 605 return None 606 607 # Get the post URI and CID from the notification (handle both dict and object) ··· 660 current_parent_cid = post_cid 661 662 for i, message in enumerate(reply_messages): 663 + thread_correlation_id = f"{correlation_id}-{i+1}" 664 + logger.info(f"[{thread_correlation_id}] Sending reply {i+1}/{len(reply_messages)}: {message[:50]}...") 665 666 # Send this reply 667 response = reply_to_post( ··· 671 reply_to_cid=current_parent_cid, 672 root_uri=root_uri, 673 root_cid=root_cid, 674 + lang=lang, 675 + correlation_id=thread_correlation_id 676 ) 677 678 if not response: 679 + logger.error(f"[{thread_correlation_id}] Failed to send reply {i+1}, posting system failure message") 680 # Try to post a system failure message 681 failure_response = reply_to_post( 682 client=client, ··· 685 reply_to_cid=current_parent_cid, 686 root_uri=root_uri, 687 root_cid=root_cid, 688 + lang=lang, 689 + correlation_id=f"{thread_correlation_id}-FAIL" 690 ) 691 if failure_response: 692 responses.append(failure_response) 693 current_parent_uri = failure_response.uri 694 current_parent_cid = failure_response.cid 695 else: 696 + logger.error(f"[{thread_correlation_id}] Could not even send system failure message, stopping thread") 697 return responses if responses else None 698 else: 699 responses.append(response) ··· 702 current_parent_uri = response.uri 703 current_parent_cid = response.cid 704 705 + logger.info(f"[{correlation_id}] Successfully sent {len(responses)} threaded replies", extra={ 706 + 'correlation_id': correlation_id, 707 + 'replies_sent': len(responses), 708 + 'replies_requested': len(reply_messages) 709 + }) 710 return responses 711 712 except Exception as e: 713 + logger.error(f"[{correlation_id}] Error sending threaded reply to notification: {e}", extra={ 714 + 'correlation_id': correlation_id, 715 + 'error': str(e), 716 + 'error_type': type(e).__name__, 717 + 'message_count': len(reply_messages) 718 + }) 719 return None 720 721 ··· 756 logger.error("Missing access token or DID from session") 757 return None 758 759 + # Get PDS URI from config instead of environment variables 760 + from config_loader import get_bluesky_config 761 + bluesky_config = get_bluesky_config() 762 + pds_host = bluesky_config['pds_uri'] 763 764 # Create acknowledgment record with null subject 765 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 833 logger.error("Missing access token or DID from session") 834 return None 835 836 + # Get PDS URI from config instead of environment variables 837 + from config_loader import get_bluesky_config 838 + bluesky_config = get_bluesky_config() 839 + pds_host = bluesky_config['pds_uri'] 840 841 # Create acknowledgment record with stream.thought.ack type 842 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 912 logger.error("Missing access token or DID from session") 913 return None 914 915 + # Get PDS URI from config instead of environment variables 916 + from config_loader import get_bluesky_config 917 + bluesky_config = get_bluesky_config() 918 + pds_host = bluesky_config['pds_uri'] 919 920 # Create tool call record 921 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") ··· 992 logger.error("Missing access token or DID from session") 993 return None 994 995 + # Get PDS URI from config instead of environment variables 996 + from config_loader import get_bluesky_config 997 + bluesky_config = get_bluesky_config() 998 + pds_host = bluesky_config['pds_uri'] 999 1000 # Create reasoning record 1001 now = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z")
+4 -4
config_loader.py
··· 179 } 180 181 def get_bluesky_config() -> Dict[str, Any]: 182 - """Get Bluesky configuration.""" 183 config = get_config() 184 return { 185 - 'username': config.get_required('bluesky.username', 'BSKY_USERNAME'), 186 - 'password': config.get_required('bluesky.password', 'BSKY_PASSWORD'), 187 - 'pds_uri': config.get_with_env('bluesky.pds_uri', 'PDS_URI', 'https://bsky.social'), 188 } 189 190 def get_bot_config() -> Dict[str, Any]:
··· 179 } 180 181 def get_bluesky_config() -> Dict[str, Any]: 182 + """Get Bluesky configuration, prioritizing config.yaml over environment variables.""" 183 config = get_config() 184 return { 185 + 'username': config.get_required('bluesky.username'), 186 + 'password': config.get_required('bluesky.password'), 187 + 'pds_uri': config.get('bluesky.pds_uri', 'https://bsky.social'), 188 } 189 190 def get_bot_config() -> Dict[str, Any]: