diff --git a/bsky.py b/bsky.py index cc08577..c130e26 100644 --- a/bsky.py +++ b/bsky.py @@ -95,7 +95,7 @@ def export_agent_state(client, agent): os.makedirs("agents", exist_ok=True) # Export agent data - logger.info(f"Exporting agent {agent.id}...") + logger.info(f"Exporting agent {agent.id}. This takes some time...") agent_data = client.agents.export_file(agent_id=agent.id) # Save timestamped archive copy @@ -231,10 +231,10 @@ def process_mention(void_agent, atproto_client, notification_data): raise # Get thread context as YAML string - logger.info("Converting thread to YAML string") + logger.debug("Converting thread to YAML string") try: thread_context = thread_to_yaml_string(thread) - logger.info(f"Thread context generated, length: {len(thread_context)} characters") + logger.debug(f"Thread context generated, length: {len(thread_context)} characters") # Create a more informative preview by extracting meaningful content lines = thread_context.split('\n') @@ -327,11 +327,79 @@ Use the bluesky_reply tool to send a response less than 300 characters.""" # Log condensed chunk info if hasattr(chunk, 'message_type'): if chunk.message_type == 'reasoning_message': - logger.info(f"🧠 Reasoning: {chunk.reasoning[:150]}...") + # Show full reasoning without truncation + logger.info(f"🧠 Reasoning: {chunk.reasoning}") elif chunk.message_type == 'tool_call_message': - logger.info(f"🔧 Tool call: {chunk.tool_call.name}({chunk.tool_call.arguments[:150]}...)") + # Parse tool arguments for better display + tool_name = chunk.tool_call.name + try: + args = json.loads(chunk.tool_call.arguments) + # Format based on tool type + if tool_name == 'bluesky_reply': + messages = args.get('messages', [args.get('message', '')]) + lang = args.get('lang', 'en-US') + if messages and isinstance(messages, list): + preview = messages[0][:100] + "..." if len(messages[0]) > 100 else messages[0] + msg_count = f" ({len(messages)} msgs)" if len(messages) > 1 else "" + logger.info(f"🔧 Tool call: {tool_name} → \"{preview}\"{msg_count} [lang: {lang}]") + else: + logger.info(f"🔧 Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)") + elif tool_name == 'archival_memory_search': + query = args.get('query', 'unknown') + logger.info(f"🔧 Tool call: {tool_name} → query: \"{query}\"") + elif tool_name == 'update_block': + label = args.get('label', 'unknown') + value_preview = str(args.get('value', ''))[:50] + "..." if len(str(args.get('value', ''))) > 50 else str(args.get('value', '')) + logger.info(f"🔧 Tool call: {tool_name} → {label}: \"{value_preview}\"") + else: + # Generic display for other tools + args_str = ', '.join(f"{k}={v}" for k, v in args.items() if k != 'request_heartbeat') + if len(args_str) > 150: + args_str = args_str[:150] + "..." + logger.info(f"🔧 Tool call: {tool_name}({args_str})") + except: + # Fallback to original format if parsing fails + logger.info(f"🔧 Tool call: {tool_name}({chunk.tool_call.arguments[:150]}...)") elif chunk.message_type == 'tool_return_message': - logger.info(f"📋 Tool result: {chunk.name} - {chunk.status}") + # Enhanced tool result logging + tool_name = chunk.name + status = chunk.status + + if status == 'success': + # Try to show meaningful result info based on tool type + if hasattr(chunk, 'tool_return') and chunk.tool_return: + result_str = str(chunk.tool_return) + if tool_name == 'archival_memory_search': + # Count number of results if it looks like a list + if result_str.startswith('[') and result_str.endswith(']'): + try: + results = json.loads(result_str) + logger.info(f"📋 Tool result: {tool_name} ✓ Found {len(results)} memory entries") + except: + logger.info(f"📋 Tool result: {tool_name} ✓ {result_str[:100]}...") + else: + logger.info(f"📋 Tool result: {tool_name} ✓ {result_str[:100]}...") + elif tool_name == 'bluesky_reply': + logger.info(f"📋 Tool result: {tool_name} ✓ Reply posted successfully") + elif tool_name == 'update_block': + logger.info(f"📋 Tool result: {tool_name} ✓ Memory block updated") + else: + # Generic success with preview + preview = result_str[:100] + "..." if len(result_str) > 100 else result_str + logger.info(f"📋 Tool result: {tool_name} ✓ {preview}") + else: + logger.info(f"📋 Tool result: {tool_name} ✓") + elif status == 'error': + # Show error details + error_preview = "" + if hasattr(chunk, 'tool_return') and chunk.tool_return: + error_str = str(chunk.tool_return) + error_preview = error_str[:100] + "..." if len(error_str) > 100 else error_str + logger.info(f"📋 Tool result: {tool_name} ✗ Error: {error_preview}") + else: + logger.info(f"📋 Tool result: {tool_name} ✗ Error occurred") + else: + logger.info(f"📋 Tool result: {tool_name} - {status}") elif chunk.message_type == 'assistant_message': logger.info(f"💬 Assistant: {chunk.content[:150]}...") else: diff --git a/letta b/letta index e2888dc..0f98d55 160000 --- a/letta +++ b/letta @@ -1 +1 @@ -Subproject commit e2888dc134832fc40c84d23733bc00f806a4b299 +Subproject commit 0f98d55bea8c9ee48bc3070b2e6e68740f0b38f6 diff --git a/register_tools.py b/register_tools.py index bf93310..ff8fa2c 100755 --- a/register_tools.py +++ b/register_tools.py @@ -13,7 +13,7 @@ from rich.table import Table from tools.search import search_bluesky_posts, SearchArgs from tools.post import create_new_bluesky_post, PostArgs from tools.feed import get_bluesky_feed, FeedArgs -from tools.blocks import attach_user_blocks, detach_user_blocks, AttachUserBlocksArgs, DetachUserBlocksArgs +from tools.blocks import attach_user_blocks, detach_user_blocks, user_note_append, AttachUserBlocksArgs, DetachUserBlocksArgs, UserNoteAppendArgs from tools.reply import bluesky_reply, ReplyArgs from tools.halt import halt_activity, HaltArgs @@ -55,6 +55,12 @@ TOOL_CONFIGS = [ "description": "Detach user-specific memory blocks from the agent. Blocks are preserved for later use.", "tags": ["memory", "blocks", "user"] }, + { + "func": user_note_append, + "args_schema": UserNoteAppendArgs, + "description": "Append a note to a user's memory block. Creates the block if it doesn't exist.", + "tags": ["memory", "blocks", "user", "append"] + }, { "func": bluesky_reply, "args_schema": ReplyArgs, diff --git a/tools/blocks.py b/tools/blocks.py index 7f6a648..99c6467 100644 --- a/tools/blocks.py +++ b/tools/blocks.py @@ -11,6 +11,22 @@ class DetachUserBlocksArgs(BaseModel): handles: List[str] = Field(..., description="List of user Bluesky handles (e.g., ['user1.bsky.social', 'user2.bsky.social'])") +class UserNoteAppendArgs(BaseModel): + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") + note: str = Field(..., description="Note to append to the user's memory block (e.g., '\\n- Cameron is a person')") + + +class UserNoteReplaceArgs(BaseModel): + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") + old_text: str = Field(..., description="Text to find and replace in the user's memory block") + new_text: str = Field(..., description="Text to replace the old_text with") + + +class UserNoteSetArgs(BaseModel): + handle: str = Field(..., description="User Bluesky handle (e.g., 'cameron.pfiffer.org')") + content: str = Field(..., description="Complete content to set for the user's memory block") + + def attach_user_blocks(handles: list, agent_state: "AgentState") -> str: """ @@ -140,3 +156,75 @@ def detach_user_blocks(handles: list, agent_state: "AgentState") -> str: raise Exception(f"Error detaching user blocks: {str(e)}") +def user_note_append(handle: str, note: str, agent_state: "AgentState") -> str: + """ + Append a note to a user's memory block. Creates the block if it doesn't exist. + + Args: + handle: User Bluesky handle (e.g., 'cameron.pfiffer.org') + note: Note to append to the user's memory block + agent_state: The agent state object containing agent information + + Returns: + String confirming the note was appended + """ + import os + import logging + from letta_client import Letta + + logger = logging.getLogger(__name__) + + try: + client = Letta(token=os.environ["LETTA_API_KEY"]) + + # Sanitize handle for block label + clean_handle = handle.lstrip('@').replace('.', '_').replace('-', '_').replace(' ', '_') + block_label = f"user_{clean_handle}" + + # Check if block exists + blocks = client.blocks.list(label=block_label) + + if blocks and len(blocks) > 0: + # Block exists, append to it + block = blocks[0] + current_value = block.value + new_value = current_value + note + + # Update the block + client.blocks.modify( + block_id=str(block.id), + value=new_value + ) + logger.info(f"Appended note to existing block: {block_label}") + return f"✓ Appended note to {handle}'s memory block" + + else: + # Block doesn't exist, create it with the note + initial_value = f"# User: {handle}\n\n{note}" + block = client.blocks.create( + label=block_label, + value=initial_value, + limit=5000 + ) + logger.info(f"Created new block with note: {block_label}") + + # Check if block needs to be attached to agent + current_blocks = client.agents.blocks.list(agent_id=str(agent_state.id)) + current_block_labels = {block.label for block in current_blocks} + + if block_label not in current_block_labels: + # Attach the new block to the agent + client.agents.blocks.attach( + agent_id=str(agent_state.id), + block_id=str(block.id) + ) + logger.info(f"Attached new block to agent: {block_label}") + return f"✓ Created and attached {handle}'s memory block with note" + else: + return f"✓ Created {handle}'s memory block with note" + + except Exception as e: + logger.error(f"Error appending note to user block: {e}") + raise Exception(f"Error appending note to user block: {str(e)}") + +