a digital person for bluesky

Add language support for Bluesky replies

- Updated bluesky_reply tool to accept optional lang parameter (defaults to en-US)
- Modified bsky.py to extract language from tool call and pass to reply function
- Updated bsky_utils.py reply functions to accept and use langs parameter
- Posts are now tagged with the specified language code for better filtering

This allows Void to post in different languages by specifying codes like
'es', 'ja', 'th', etc. when using the bluesky_reply tool.

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

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

Changed files
+31 -15
tools
+10 -6
bsky.py
··· 368 logger.debug("Successfully received response from Letta API") 369 logger.debug(f"Number of messages in response: {len(message_response.messages) if hasattr(message_response, 'messages') else 'N/A'}") 370 371 - # Extract the reply text from the agent's response 372 reply_text = "" 373 logger.debug(f"Processing {len(message_response.messages)} response messages...") 374 375 for i, message in enumerate(message_response.messages, 1): ··· 392 # Check if this is a ToolCallMessage with bluesky_reply tool 393 if hasattr(message, 'tool_call') and message.tool_call: 394 if message.tool_call.name == 'bluesky_reply': 395 - # Parse the JSON arguments to get the message 396 try: 397 args = json.loads(message.tool_call.arguments) 398 reply_text = args.get('message', '') 399 - logger.info(f"Extracted reply from tool call: {reply_text[:50]}...") 400 break 401 except json.JSONDecodeError as e: 402 logger.error(f"Failed to parse tool call arguments: {e}") ··· 411 print(f"\n=== GENERATED REPLY ===") 412 print(f"To: @{author_handle}") 413 print(f"Reply: {reply_text}") 414 print(f"======================\n") 415 416 - # Send the reply 417 - logger.info(f"Sending reply: {reply_text[:50]}...") 418 response = bsky_utils.reply_to_notification( 419 client=atproto_client, 420 notification=notification_data, 421 - reply_text=reply_text 422 ) 423 424 if response:
··· 368 logger.debug("Successfully received response from Letta API") 369 logger.debug(f"Number of messages in response: {len(message_response.messages) if hasattr(message_response, 'messages') else 'N/A'}") 370 371 + # Extract the reply text and language from the agent's response 372 reply_text = "" 373 + reply_lang = "en-US" # Default language 374 logger.debug(f"Processing {len(message_response.messages)} response messages...") 375 376 for i, message in enumerate(message_response.messages, 1): ··· 393 # Check if this is a ToolCallMessage with bluesky_reply tool 394 if hasattr(message, 'tool_call') and message.tool_call: 395 if message.tool_call.name == 'bluesky_reply': 396 + # Parse the JSON arguments to get the message and language 397 try: 398 args = json.loads(message.tool_call.arguments) 399 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 403 except json.JSONDecodeError as e: 404 logger.error(f"Failed to parse tool call arguments: {e}") ··· 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") 418 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 ) 427 428 if response:
+12 -6
bsky_utils.py
··· 253 254 return init_client(username, password) 255 256 - 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) -> Dict[str, Any]: 257 """ 258 Reply to a post on Bluesky with rich text support. 259 ··· 264 reply_to_cid: The CID of the post being replied to (parent) 265 root_uri: The URI of the root post (if replying to a reply). If None, uses reply_to_uri 266 root_cid: The CID of the root post (if replying to a reply). If None, uses reply_to_cid 267 268 Returns: 269 The response from sending the post ··· 331 response = client.send_post( 332 text=text, 333 reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 334 - facets=facets 335 ) 336 else: 337 response = client.send_post( 338 text=text, 339 - reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref) 340 ) 341 342 logger.info(f"Reply sent successfully: {response.uri}") ··· 362 return None 363 364 365 - def reply_to_notification(client: Client, notification: Any, reply_text: str) -> Optional[Dict[str, Any]]: 366 """ 367 Reply to a notification (mention or reply). 368 ··· 370 client: Authenticated Bluesky client 371 notification: The notification object from list_notifications 372 reply_text: The text to reply with 373 374 Returns: 375 The response from sending the reply or None if failed ··· 417 reply_to_uri=post_uri, 418 reply_to_cid=post_cid, 419 root_uri=root_uri, 420 - root_cid=root_cid 421 ) 422 else: 423 # If we can't get thread data, just reply directly ··· 425 client=client, 426 text=reply_text, 427 reply_to_uri=post_uri, 428 - reply_to_cid=post_cid 429 ) 430 431 except Exception as e:
··· 253 254 return init_client(username, password) 255 256 + 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]: 257 """ 258 Reply to a post on Bluesky with rich text support. 259 ··· 264 reply_to_cid: The CID of the post being replied to (parent) 265 root_uri: The URI of the root post (if replying to a reply). If None, uses reply_to_uri 266 root_cid: The CID of the root post (if replying to a reply). If None, uses reply_to_cid 267 + lang: Language code for the post (e.g., 'en-US', 'es', 'ja') 268 269 Returns: 270 The response from sending the post ··· 332 response = client.send_post( 333 text=text, 334 reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 335 + facets=facets, 336 + langs=[lang] if lang else None 337 ) 338 else: 339 response = client.send_post( 340 text=text, 341 + reply_to=models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref), 342 + langs=[lang] if lang else None 343 ) 344 345 logger.info(f"Reply sent successfully: {response.uri}") ··· 365 return None 366 367 368 + def reply_to_notification(client: Client, notification: Any, reply_text: str, lang: str = "en-US") -> Optional[Dict[str, Any]]: 369 """ 370 Reply to a notification (mention or reply). 371 ··· 373 client: Authenticated Bluesky client 374 notification: The notification object from list_notifications 375 reply_text: The text to reply with 376 + lang: Language code for the post (defaults to "en-US") 377 378 Returns: 379 The response from sending the reply or None if failed ··· 421 reply_to_uri=post_uri, 422 reply_to_cid=post_cid, 423 root_uri=root_uri, 424 + root_cid=root_cid, 425 + lang=lang 426 ) 427 else: 428 # If we can't get thread data, just reply directly ··· 430 client=client, 431 text=reply_text, 432 reply_to_uri=post_uri, 433 + reply_to_cid=post_cid, 434 + lang=lang 435 ) 436 437 except Exception as e:
+9 -3
tools/reply.py
··· 1 """Reply tool for Bluesky - a simple tool for the Letta agent to indicate a reply.""" 2 from pydantic import BaseModel, Field, validator 3 4 ··· 7 ..., 8 description="The reply message text (max 300 characters)" 9 ) 10 11 @validator('message') 12 def validate_message_length(cls, v): ··· 15 return v 16 17 18 - def bluesky_reply(message: str) -> str: 19 """ 20 This is a simple function that returns a string. MUST be less than 300 characters. 21 22 Args: 23 message: The reply text (max 300 characters) 24 25 Returns: 26 - Confirmation message 27 28 Raises: 29 Exception: If message exceeds 300 characters ··· 31 if len(message) > 300: 32 raise Exception(f"Message cannot be longer than 300 characters (current: {len(message)} characters)") 33 34 - return 'Reply sent'
··· 1 """Reply tool for Bluesky - a simple tool for the Letta agent to indicate a reply.""" 2 + from typing import Optional 3 from pydantic import BaseModel, Field, validator 4 5 ··· 8 ..., 9 description="The reply message text (max 300 characters)" 10 ) 11 + lang: Optional[str] = Field( 12 + default="en-US", 13 + description="Language code for the post (e.g., 'en-US', 'es', 'ja', 'th'). Defaults to 'en-US'" 14 + ) 15 16 @validator('message') 17 def validate_message_length(cls, v): ··· 20 return v 21 22 23 + def bluesky_reply(message: str, lang: str = "en-US") -> str: 24 """ 25 This is a simple function that returns a string. MUST be less than 300 characters. 26 27 Args: 28 message: The reply text (max 300 characters) 29 + lang: Language code for the post (e.g., 'en-US', 'es', 'ja', 'th'). Defaults to 'en-US' 30 31 Returns: 32 + Confirmation message with language info 33 34 Raises: 35 Exception: If message exceeds 300 characters ··· 37 if len(message) > 300: 38 raise Exception(f"Message cannot be longer than 300 characters (current: {len(message)} characters)") 39 40 + return f'Reply sent (language: {lang})'