From 6c6ec67c1e9098d4fdbe74a742280b815d5cd1f1 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Tue, 8 Jul 2025 07:53:43 -0700 Subject: [PATCH] Add ignore_notification tool for explicit notification filtering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements a new tool that allows the agent to explicitly ignore notifications without creating a reply. This is particularly useful for filtering out bot interactions or spam. Key features: - New ignore_notification tool with reason and category tracking - Conflict detection if agent calls both reply and ignore tools - Ignored notifications are deleted from queue (not moved to no_reply) - Proper logging and tracking of ignored notifications The tool helps the agent make deliberate decisions about which notifications to engage with, improving interaction quality. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- bsky.py | 43 +++++++++++++++++++++++++++++++++++++++++-- register_tools.py | 7 +++++++ tools/ignore.py | 29 +++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 tools/ignore.py diff --git a/bsky.py b/bsky.py index 71f7c0d..c9feb9a 100644 --- a/bsky.py +++ b/bsky.py @@ -491,11 +491,27 @@ To reply, use the add_post_to_bluesky_reply_thread tool. Call it multiple times logger.debug(f"Processing {len(message_response.messages)} response messages...") # First pass: collect tool return statuses + ignored_notification = False + ignore_reason = "" + ignore_category = "" + for message in message_response.messages: if hasattr(message, 'tool_call_id') and hasattr(message, 'status') and hasattr(message, 'name'): if message.name == 'add_post_to_bluesky_reply_thread': tool_call_results[message.tool_call_id] = message.status logger.debug(f"Tool result: {message.tool_call_id} -> {message.status}") + elif message.name == 'ignore_notification': + # Check if the tool was successful + if hasattr(message, 'tool_return') and message.status == 'success': + # Parse the return value to extract category and reason + result_str = str(message.tool_return) + if 'IGNORED_NOTIFICATION::' in result_str: + parts = result_str.split('::') + if len(parts) >= 3: + ignore_category = parts[1] + ignore_reason = parts[2] + ignored_notification = True + logger.info(f"🚫 Notification ignored - Category: {ignore_category}, Reason: {ignore_reason}") elif message.name == 'bluesky_reply': logger.error("❌ DEPRECATED TOOL DETECTED: bluesky_reply is no longer supported!") logger.error("Please use add_post_to_bluesky_reply_thread instead.") @@ -584,6 +600,14 @@ To reply, use the add_post_to_bluesky_reply_thread tool. Call it multiple times else: logger.warning(f"⚠️ Skipping add_post_to_bluesky_reply_thread tool call with unknown status: {tool_status}") + # Check for conflicting tool calls + if reply_candidates and ignored_notification: + logger.error(f"⚠️ CONFLICT: Agent called both add_post_to_bluesky_reply_thread and ignore_notification!") + logger.error(f"Reply candidates: {len(reply_candidates)}, Ignore reason: {ignore_reason}") + logger.warning("Item will be left in queue for manual review") + # Return False to keep in queue + return False + if reply_candidates: # Aggregate reply posts into a thread reply_messages = [] @@ -642,8 +666,13 @@ To reply, use the add_post_to_bluesky_reply_thread tool. Call it multiple times logger.error(f"Failed to send reply to @{author_handle}") return False else: - logger.warning(f"No add_post_to_bluesky_reply_thread tool calls found for mention from @{author_handle}, moving to no_reply folder") - return "no_reply" + # Check if notification was explicitly ignored + if ignored_notification: + logger.info(f"Notification from @{author_handle} was explicitly ignored (category: {ignore_category})") + return "ignored" + else: + logger.warning(f"No add_post_to_bluesky_reply_thread tool calls found for mention from @{author_handle}, moving to no_reply folder") + return "no_reply" except Exception as e: logger.error(f"Error processing mention: {e}") @@ -846,6 +875,16 @@ def load_and_process_queued_notifications(void_agent, atproto_client, testing_mo processed_uris.add(notif_data['uri']) save_processed_notifications(processed_uris) + elif success == "ignored": # Special case for explicitly ignored notifications + # For ignored notifications, we just delete them (not move to no_reply) + filepath.unlink() + logger.info(f"🚫 Deleted ignored notification: {filepath.name}") + + # Also mark as processed to avoid retrying + processed_uris = load_processed_notifications() + processed_uris.add(notif_data['uri']) + save_processed_notifications(processed_uris) + else: logger.warning(f"⚠️ Failed to process {filepath.name}, keeping in queue for retry") diff --git a/register_tools.py b/register_tools.py index 0d4fb3d..b8c184b 100755 --- a/register_tools.py +++ b/register_tools.py @@ -16,6 +16,7 @@ from tools.feed import get_bluesky_feed, FeedArgs from tools.blocks import attach_user_blocks, detach_user_blocks, user_note_append, user_note_replace, user_note_set, user_note_view, AttachUserBlocksArgs, DetachUserBlocksArgs, UserNoteAppendArgs, UserNoteReplaceArgs, UserNoteSetArgs, UserNoteViewArgs from tools.halt import halt_activity, HaltArgs from tools.thread import add_post_to_bluesky_reply_thread, ReplyThreadPostArgs +from tools.ignore import ignore_notification, IgnoreNotificationArgs load_dotenv() logging.basicConfig(level=logging.INFO) @@ -91,6 +92,12 @@ TOOL_CONFIGS = [ "description": "Add a single post to the current Bluesky reply thread atomically", "tags": ["bluesky", "reply", "thread", "atomic"] }, + { + "func": ignore_notification, + "args_schema": IgnoreNotificationArgs, + "description": "Explicitly ignore a notification without replying (useful for ignoring bot interactions)", + "tags": ["notification", "ignore", "control", "bot"] + }, ] diff --git a/tools/ignore.py b/tools/ignore.py new file mode 100644 index 0000000..f392e3c --- /dev/null +++ b/tools/ignore.py @@ -0,0 +1,29 @@ +"""Ignore notification tool for Bluesky.""" +from pydantic import BaseModel, Field +from typing import Optional + + +class IgnoreNotificationArgs(BaseModel): + reason: str = Field(..., description="Reason for ignoring this notification") + category: Optional[str] = Field( + default="bot", + description="Category of ignored notification (e.g., 'bot', 'spam', 'not_relevant', 'handled_elsewhere')" + ) + + +def ignore_notification(reason: str, category: str = "bot") -> str: + """ + Signal that the current notification should be ignored without a reply. + + This tool allows the agent to explicitly mark a notification as ignored + rather than having it default to the no_reply folder. This is particularly + useful for ignoring interactions from bots or spam accounts. + + Args: + reason: Reason for ignoring this notification + category: Category of ignored notification (default: 'bot') + + Returns: + Confirmation message + """ + return f"IGNORED_NOTIFICATION::{category}::{reason}" \ No newline at end of file -- 2.51.2