a digital person for bluesky

Enhance tool call logging with detailed information

- Show full reasoning messages without truncation
- Display bluesky_reply messages with content preview and language
- Show archival_memory_search queries clearly
- Display update_block operations with block name and value preview
- Enhanced tool result logging with success/error indicators
- Add specific formatting for known tool types
- Provide meaningful summaries for tool operations

This makes the bot's operation much more transparent and easier to debug.

Changed files
+169 -7
tools
+74 -6
bsky.py
··· 95 os.makedirs("agents", exist_ok=True) 96 97 # Export agent data 98 - logger.info(f"Exporting agent {agent.id}...") 99 agent_data = client.agents.export_file(agent_id=agent.id) 100 101 # Save timestamped archive copy ··· 231 raise 232 233 # Get thread context as YAML string 234 - logger.info("Converting thread to YAML string") 235 try: 236 thread_context = thread_to_yaml_string(thread) 237 - logger.info(f"Thread context generated, length: {len(thread_context)} characters") 238 239 # Create a more informative preview by extracting meaningful content 240 lines = thread_context.split('\n') ··· 327 # Log condensed chunk info 328 if hasattr(chunk, 'message_type'): 329 if chunk.message_type == 'reasoning_message': 330 - logger.info(f"๐Ÿง  Reasoning: {chunk.reasoning[:150]}...") 331 elif chunk.message_type == 'tool_call_message': 332 - logger.info(f"๐Ÿ”ง Tool call: {chunk.tool_call.name}({chunk.tool_call.arguments[:150]}...)") 333 elif chunk.message_type == 'tool_return_message': 334 - logger.info(f"๐Ÿ“‹ Tool result: {chunk.name} - {chunk.status}") 335 elif chunk.message_type == 'assistant_message': 336 logger.info(f"๐Ÿ’ฌ Assistant: {chunk.content[:150]}...") 337 else:
··· 95 os.makedirs("agents", exist_ok=True) 96 97 # Export agent data 98 + logger.info(f"Exporting agent {agent.id}. This takes some time...") 99 agent_data = client.agents.export_file(agent_id=agent.id) 100 101 # Save timestamped archive copy ··· 231 raise 232 233 # Get thread context as YAML string 234 + logger.debug("Converting thread to YAML string") 235 try: 236 thread_context = thread_to_yaml_string(thread) 237 + logger.debug(f"Thread context generated, length: {len(thread_context)} characters") 238 239 # Create a more informative preview by extracting meaningful content 240 lines = thread_context.split('\n') ··· 327 # Log condensed chunk info 328 if hasattr(chunk, 'message_type'): 329 if chunk.message_type == 'reasoning_message': 330 + # Show full reasoning without truncation 331 + logger.info(f"๐Ÿง  Reasoning: {chunk.reasoning}") 332 elif chunk.message_type == 'tool_call_message': 333 + # Parse tool arguments for better display 334 + tool_name = chunk.tool_call.name 335 + try: 336 + args = json.loads(chunk.tool_call.arguments) 337 + # Format based on tool type 338 + if tool_name == 'bluesky_reply': 339 + messages = args.get('messages', [args.get('message', '')]) 340 + lang = args.get('lang', 'en-US') 341 + if messages and isinstance(messages, list): 342 + preview = messages[0][:100] + "..." if len(messages[0]) > 100 else messages[0] 343 + msg_count = f" ({len(messages)} msgs)" if len(messages) > 1 else "" 344 + logger.info(f"๐Ÿ”ง Tool call: {tool_name} โ†’ \"{preview}\"{msg_count} [lang: {lang}]") 345 + else: 346 + logger.info(f"๐Ÿ”ง Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)") 347 + elif tool_name == 'archival_memory_search': 348 + query = args.get('query', 'unknown') 349 + logger.info(f"๐Ÿ”ง Tool call: {tool_name} โ†’ query: \"{query}\"") 350 + elif tool_name == 'update_block': 351 + label = args.get('label', 'unknown') 352 + value_preview = str(args.get('value', ''))[:50] + "..." if len(str(args.get('value', ''))) > 50 else str(args.get('value', '')) 353 + logger.info(f"๐Ÿ”ง Tool call: {tool_name} โ†’ {label}: \"{value_preview}\"") 354 + else: 355 + # Generic display for other tools 356 + args_str = ', '.join(f"{k}={v}" for k, v in args.items() if k != 'request_heartbeat') 357 + if len(args_str) > 150: 358 + args_str = args_str[:150] + "..." 359 + logger.info(f"๐Ÿ”ง Tool call: {tool_name}({args_str})") 360 + except: 361 + # Fallback to original format if parsing fails 362 + logger.info(f"๐Ÿ”ง Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)") 363 elif chunk.message_type == 'tool_return_message': 364 + # Enhanced tool result logging 365 + tool_name = chunk.name 366 + status = chunk.status 367 + 368 + if status == 'success': 369 + # Try to show meaningful result info based on tool type 370 + if hasattr(chunk, 'tool_return') and chunk.tool_return: 371 + result_str = str(chunk.tool_return) 372 + if tool_name == 'archival_memory_search': 373 + # Count number of results if it looks like a list 374 + if result_str.startswith('[') and result_str.endswith(']'): 375 + try: 376 + results = json.loads(result_str) 377 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ Found {len(results)} memory entries") 378 + except: 379 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ {result_str[:100]}...") 380 + else: 381 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ {result_str[:100]}...") 382 + elif tool_name == 'bluesky_reply': 383 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ Reply posted successfully") 384 + elif tool_name == 'update_block': 385 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ Memory block updated") 386 + else: 387 + # Generic success with preview 388 + preview = result_str[:100] + "..." if len(result_str) > 100 else result_str 389 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“ {preview}") 390 + else: 391 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ“") 392 + elif status == 'error': 393 + # Show error details 394 + error_preview = "" 395 + if hasattr(chunk, 'tool_return') and chunk.tool_return: 396 + error_str = str(chunk.tool_return) 397 + error_preview = error_str[:100] + "..." if len(error_str) > 100 else error_str 398 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ— Error: {error_preview}") 399 + else: 400 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} โœ— Error occurred") 401 + else: 402 + logger.info(f"๐Ÿ“‹ Tool result: {tool_name} - {status}") 403 elif chunk.message_type == 'assistant_message': 404 logger.info(f"๐Ÿ’ฌ Assistant: {chunk.content[:150]}...") 405 else:
+7 -1
register_tools.py
··· 13 from tools.search import search_bluesky_posts, SearchArgs 14 from tools.post import create_new_bluesky_post, PostArgs 15 from tools.feed import get_bluesky_feed, FeedArgs 16 - from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs 17 from tools.reply import bluesky_reply, ReplyArgs 18 from tools.halt import halt_activity, HaltArgs 19 ··· 54 "args_schema": DetachUserBlocksArgs, 55 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", 56 "tags": ["memory", "blocks", "user"] 57 }, 58 { 59 "func": bluesky_reply,
··· 13 from tools.search import search_bluesky_posts, SearchArgs 14 from tools.post import create_new_bluesky_post, PostArgs 15 from tools.feed import get_bluesky_feed, FeedArgs 16 + from tools.blocks import attach_user_blocks, detach_user_blocks, user_note_append, AttachUserBlocksArgs, DetachUserBlocksArgs, UserNoteAppendArgs 17 from tools.reply import bluesky_reply, ReplyArgs 18 from tools.halt import halt_activity, HaltArgs 19 ··· 54 "args_schema": DetachUserBlocksArgs, 55 "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", 56 "tags": ["memory", "blocks", "user"] 57 + }, 58 + { 59 + "func": user_note_append, 60 + "args_schema": UserNoteAppendArgs, 61 + "description": "Append a note to a user's memory block. Creates the block if it doesn't exist.", 62 + "tags": ["memory", "blocks", "user", "append"] 63 }, 64 { 65 "func": bluesky_reply,
+88
tools/blocks.py
··· 11 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 12 13 14 15 def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 16 """ ··· 140 raise Exception(f"Error detaching user blocks: {str(e)}") 141 142
··· 11 handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") 12 13 14 + class UserNoteAppendArgs(BaseModel): 15 + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 16 + note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')") 17 + 18 + 19 + class UserNoteReplaceArgs(BaseModel): 20 + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 21 + old_text: str = Field(..., description="Text to find and replace in the user's memory block") 22 + new_text: str = Field(..., description="Text to replace the old_text with") 23 + 24 + 25 + class UserNoteSetArgs(BaseModel): 26 + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") 27 + content: str = Field(..., description="Complete content to set for the user's memory block") 28 + 29 + 30 31 def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: 32 """ ··· 156 raise Exception(f"Error detaching user blocks: {str(e)}") 157 158 159 + def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str: 160 + """ 161 + Append a note to a user's memory block. Creates the block if it doesn't exist. 162 + 163 + Args: 164 + handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') 165 + note: Note to append to the user's memory block 166 + agent_state: The agent state object containing agent information 167 + 168 + Returns: 169 + String confirming the note was appended 170 + """ 171 + import os 172 + import logging 173 + from letta_client import Letta 174 + 175 + logger = logging.getLogger(__name__) 176 + 177 + try: 178 + client = Letta(token=os.environ["LETTA_API_KEY"]) 179 + 180 + # Sanitize handle for block label 181 + clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') 182 + block_label = f"user_{clean_handle}" 183 + 184 + # Check if block exists 185 + blocks = client.blocks.list(label=block_label) 186 + 187 + if blocks and len(blocks) > 0: 188 + # Block exists, append to it 189 + block = blocks[0] 190 + current_value = block.value 191 + new_value = current_value + note 192 + 193 + # Update the block 194 + client.blocks.modify( 195 + block_id=str(block.id), 196 + value=new_value 197 + ) 198 + logger.info(f"Appended note to existing block: {block_label}") 199 + return f"โœ“ Appended note to {handle}'s memory block" 200 + 201 + else: 202 + # Block doesn't exist, create it with the note 203 + initial_value = f"# User: {handle}\n\n{note}" 204 + block = client.blocks.create( 205 + label=block_label, 206 + value=initial_value, 207 + limit=5000 208 + ) 209 + logger.info(f"Created new block with note: {block_label}") 210 + 211 + # Check if block needs to be attached to agent 212 + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) 213 + current_block_labels = {block.label for block in current_blocks} 214 + 215 + if block_label not in current_block_labels: 216 + # Attach the new block to the agent 217 + client.agents.blocks.attach( 218 + agent_id=str(agent_state.id), 219 + block_id=str(block.id) 220 + ) 221 + logger.info(f"Attached new block to agent: {block_label}") 222 + return f"โœ“ Created and attached {handle}'s memory block with note" 223 + else: 224 + return f"โœ“ Created {handle}'s memory block with note" 225 + 226 + except Exception as e: 227 + logger.error(f"Error appending note to user block: {e}") 228 + raise Exception(f"Error appending note to user block: {str(e)}") 229 + 230 +