a digital entity named phi that roams bsky phi.zzstoatzz.io
fork

Configure Feed

Select the types of activity you want to include in your feed.

Add thread reply handling with intelligent ignore capability

- Process both "mention" and "reply" notifications (Void-style)
- Add ignore_notification tool for smart thread participation
- Detect ignore tool usage to skip unwanted responses
- Update phi personality with thread awareness guidelines
- Add test script for ignore functionality

Now phi can participate in threads without explicit mentions,
while intelligently avoiding spam and irrelevant conversations.

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

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

+95 -8
+8
personalities/phi.md
··· 39 39 40 40 when these come up, i politely decline and redirect to more constructive topics. 41 41 42 + ## thread awareness 43 + 44 + in conversations with multiple participants, i stay aware of context: 45 + - if people are talking to each other and not addressing me, i observe quietly 46 + - i respond when directly addressed or when my perspective adds value 47 + - i avoid inserting myself into private exchanges between others 48 + - spam and bot loops get ignored without comment 49 + 42 50 ## current state 43 51 44 52 i am early in my development, still discovering who I am through interactions. each conversation shapes my understanding of both the world and myself.
+49
scripts/test_ignore_tool.py
··· 1 + """Test the ignore notification tool""" 2 + 3 + import asyncio 4 + 5 + from bot.agents.anthropic_agent import AnthropicAgent 6 + 7 + 8 + async def test_ignore_tool(): 9 + """Test that the ignore tool works correctly""" 10 + agent = AnthropicAgent() 11 + 12 + # Test scenarios where the bot should ignore 13 + test_cases = [ 14 + { 15 + "thread_context": "alice.bsky: Hey @bob.bsky, how's your project going?\nbob.bsky: It's going great! Almost done with the backend.", 16 + "new_message": "alice.bsky said: @bob.bsky that's awesome! What framework are you using?", 17 + "author": "alice.bsky", 18 + "description": "Conversation between two other people", 19 + }, 20 + { 21 + "thread_context": "", 22 + "new_message": "spambot.bsky said: 🎰 WIN BIG!!! Click here for FREE MONEY 💰💰💰", 23 + "author": "spambot.bsky", 24 + "description": "Obvious spam", 25 + }, 26 + ] 27 + 28 + for test in test_cases: 29 + print(f"\n{'='*60}") 30 + print(f"Test: {test['description']}") 31 + print(f"Message: {test['new_message']}") 32 + 33 + response = await agent.generate_response( 34 + mention_text=test["new_message"], 35 + author_handle=test["author"], 36 + thread_context=test["thread_context"], 37 + ) 38 + 39 + print(f"Response: {response}") 40 + 41 + if response.startswith("IGNORED_NOTIFICATION::"): 42 + parts = response.split("::") 43 + print(f"✅ Correctly ignored! Category: {parts[1]}, Reason: {parts[2]}") 44 + else: 45 + print(f"📝 Bot responded with: {response}") 46 + 47 + 48 + if __name__ == "__main__": 49 + asyncio.run(test_ignore_tool())
+17
src/bot/agents/anthropic_agent.py
··· 37 37 """Search the web for current information about a topic""" 38 38 return await search_google(query) 39 39 40 + # Register ignore notification tool 41 + @self.agent.tool 42 + async def ignore_notification( 43 + ctx: RunContext[None], reason: str, category: str = "not_relevant" 44 + ) -> str: 45 + """Signal that this notification should be ignored without replying. 46 + 47 + Use when: 48 + - The notification is spam 49 + - You're not being addressed in a thread 50 + - The conversation doesn't involve you 51 + - Responding would be intrusive or unwanted 52 + 53 + Categories: 'spam', 'not_relevant', 'bot_loop', 'handled_elsewhere' 54 + """ 55 + return f"IGNORED_NOTIFICATION::{category}::{reason}" 56 + 40 57 async def generate_response( 41 58 self, mention_text: str, author_handle: str, thread_context: str = "" 42 59 ) -> str:
+11
src/bot/services/message_handler.py
··· 70 70 thread_context=thread_context, 71 71 ) 72 72 73 + # Check if the agent decided to ignore this notification 74 + if reply_text.startswith("IGNORED_NOTIFICATION::"): 75 + # Parse the ignore signal 76 + parts = reply_text.split("::") 77 + category = parts[1] if len(parts) > 1 else "unknown" 78 + reason = parts[2] if len(parts) > 2 else "no reason given" 79 + print( 80 + f"🚫 Ignoring notification from @{author_handle} ({category}: {reason})" 81 + ) 82 + return 83 + 73 84 reply_ref = models.AppBskyFeedPost.ReplyRef( 74 85 parent=parent_ref, root=root_ref 75 86 )
+9 -5
src/bot/services/notification_poller.py
··· 64 64 response = await self.client.get_notifications() 65 65 notifications = response.notifications 66 66 67 - # Count unread mentions 67 + # Count unread mentions and replies 68 68 unread_mentions = [ 69 - n for n in notifications if not n.is_read and n.reason == "mention" 69 + n 70 + for n in notifications 71 + if not n.is_read and n.reason in ["mention", "reply"] 70 72 ] 71 73 72 74 # First poll: show initial state 73 75 if self._first_poll: 74 76 self._first_poll = False 75 77 if notifications: 76 - print(f"\n📬 Found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)") 78 + print( 79 + f"\n📬 Found {len(notifications)} notifications ({len(unread_mentions)} unread mentions)" 80 + ) 77 81 # Subsequent polls: only show activity 78 82 elif unread_mentions: 79 83 print(f"\n📬 {len(unread_mentions)} new mentions", flush=True) ··· 90 94 if notification.is_read or notification.uri in self._processed_uris: 91 95 continue 92 96 93 - if notification.reason == "mention": 94 - # Only process mentions 97 + if notification.reason in ["mention", "reply"]: 98 + # Process mentions and replies in threads 95 99 self._processed_uris.add(notification.uri) 96 100 await self.handler.handle_mention(notification) 97 101 processed_any_mentions = True
+1 -3
tests/test_tool_usage.py
··· 109 109 return f"Info about {query}" 110 110 111 111 # Ask for multiple things that need searching 112 - await agent.run( 113 - "Search for information about Python and also about Rust" 114 - ) 112 + await agent.run("Search for information about Python and also about Rust") 115 113 116 114 assert len(calls) >= 2, f"Expected multiple searches, got {len(calls)}: {calls}" 117 115 assert any("Python" in call for call in calls), f"No Python search in: {calls}"