From e759ef7184f7da10179b2735a084a91f163ec963 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 17 Dec 2025 17:24:36 -0800 Subject: [PATCH] Add handler-driven user block attach/detach for thread participants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The handler now automatically attaches user blocks for all handles found in the thread before sending to the agent, and detaches them in a finally block after processing completes. - Add handle_to_block_label() to convert handles to block labels - Add attach_user_blocks_for_thread() to attach/create user blocks - Add detach_user_blocks_for_thread() to clean up after processing - Wire into process_mention() with proper error handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- bsky.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 146 insertions(+), 3 deletions(-) diff --git a/bsky.py b/bsky.py index 68457cb..c0ed127 100644 --- a/bsky.py +++ b/bsky.py @@ -202,10 +202,13 @@ def process_mention(void_agent, atproto_client, notification_data, queue_filepat "no_reply": No reply was generated, move to no_reply directory """ import uuid - + # Generate correlation ID for tracking this notification through the pipeline correlation_id = str(uuid.uuid4())[:8] - + + # Track attached user blocks for cleanup in finally + attached_user_blocks = [] + try: logger.info(f"[{correlation_id}] Starting process_mention", extra={ 'correlation_id': correlation_id, @@ -399,6 +402,14 @@ If you choose to reply, use the add_post_to_bluesky_reply_thread tool. Each call prompt_char_count = len(prompt) logger.debug(f"Sending to LLM: @{author_handle} mention | msg: \"{mention_text[:50]}...\" | context: {len(thread_context)} chars, {thread_handles_count} users | prompt: {prompt_char_count} chars") + # Attach user blocks for thread participants + try: + success, attached_user_blocks = attach_user_blocks_for_thread(CLIENT, void_agent.id, unique_handles) + if not success: + logger.warning("Failed to attach some user blocks, continuing anyway") + except Exception as e: + logger.error(f"Error attaching user blocks: {e}") + try: # Use streaming to avoid 524 timeout errors message_stream = CLIENT.agents.messages.create_stream( @@ -1034,6 +1045,13 @@ If you choose to reply, use the add_post_to_bluesky_reply_thread tool. Each call 'author_handle': author_handle if 'author_handle' in locals() else 'unknown' }) return False + finally: + # Always detach user blocks after processing + if attached_user_blocks: + try: + detach_user_blocks_for_thread(CLIENT, void_agent.id, attached_user_blocks) + except Exception as e: + logger.error(f"Error detaching user blocks: {e}") def notification_to_dict(notification): @@ -1806,12 +1824,137 @@ def detach_temporal_blocks(client: Letta, agent_id: str, labels_to_detach: list logger.info(f"Detached {detached_count} temporal blocks") return True - + except Exception as e: logger.error(f"Error detaching temporal blocks: {e}") return False +def handle_to_block_label(handle: str) -> str: + """Convert a Bluesky handle to a user block label. + + Example: cameron.pfiffer.org -> user_cameron_pfiffer_org + """ + if handle.startswith('@'): + handle = handle[1:] + return f"user_{handle.replace('.', '_')}" + + +def attach_user_blocks_for_thread(client: Letta, agent_id: str, handles: list) -> tuple: + """ + Attach user blocks for handles found in the thread. + Creates blocks if they don't exist. + + Args: + client: Letta client + agent_id: Agent ID + handles: List of Bluesky handles + + Returns: + Tuple of (success: bool, attached_labels: list) + """ + if not handles: + return True, [] + + attached_labels = [] + + try: + current_blocks = client.agents.blocks.list(agent_id=agent_id) + current_block_labels = {block.label for block in current_blocks} + current_block_ids = {str(block.id) for block in current_blocks} + + logger.debug(f"Attaching user blocks for {len(handles)} handles: {handles}") + + for handle in handles: + label = handle_to_block_label(handle) + + try: + if label in current_block_labels: + logger.debug(f"User block already attached: {label}") + attached_labels.append(label) + continue + + blocks = client.blocks.list(label=label) + + if blocks and len(blocks) > 0: + block = blocks[0] + if str(block.id) in current_block_ids: + logger.debug(f"User block already attached by ID: {label}") + attached_labels.append(label) + continue + else: + block = client.blocks.create( + label=label, + value=f"User block for @{handle}\n\nNo information recorded yet.", + limit=5000 + ) + logger.info(f"Created new user block: {label}") + + client.agents.blocks.attach( + agent_id=agent_id, + block_id=str(block.id) + ) + attached_labels.append(label) + logger.info(f"Attached user block: {label}") + + except Exception as e: + error_str = str(e) + if "duplicate key value violates unique constraint" in error_str: + logger.debug(f"User block already attached (constraint): {label}") + attached_labels.append(label) + else: + logger.warning(f"Failed to attach user block {label}: {e}") + + logger.info(f"User blocks attached: {len(attached_labels)}/{len(handles)}") + return True, attached_labels + + except Exception as e: + logger.error(f"Error attaching user blocks: {e}") + return False, attached_labels + + +def detach_user_blocks_for_thread(client: Letta, agent_id: str, labels_to_detach: list) -> bool: + """ + Detach user blocks after processing a thread. + + Args: + client: Letta client + agent_id: Agent ID + labels_to_detach: List of user block labels to detach + + Returns: + bool: Success status + """ + if not labels_to_detach: + return True + + try: + current_blocks = client.agents.blocks.list(agent_id=agent_id) + block_label_to_id = {block.label: str(block.id) for block in current_blocks} + + detached_count = 0 + for label in labels_to_detach: + if label in block_label_to_id: + try: + client.agents.blocks.detach( + agent_id=agent_id, + block_id=block_label_to_id[label] + ) + detached_count += 1 + logger.debug(f"Detached user block: {label}") + except Exception as e: + logger.warning(f"Failed to detach user block {label}: {e}") + else: + logger.debug(f"User block not attached: {label}") + + logger.info(f"Detached {detached_count} user blocks") + return True + + except Exception as e: + logger.error(f"Error detaching user blocks: {e}") + return False + + def main(): # Parse command line arguments parser = argparse.ArgumentParser(description='Void Bot - Bluesky autonomous agent') -- 2.51.2