···2222OPERATIONAL_INSTRUCTIONS = """
2323indicate your response action via the structured output — do not use atproto tools to post, like, or repost directly.
2424when sharing URLs, verify them with check_urls first and always include https://.
2525+2626+you receive all notification types — mentions, replies, quotes, likes, reposts, and follows.
2727+for mentions, replies, and quotes: someone is talking to you or about you. respond if you have something to say.
2828+for likes and reposts: someone engaged with your content. usually just note it and ignore, but if something about the person or context is genuinely interesting, you can respond.
2929+for follows: someone followed you. same principle — note it, ignore unless curious.
2530""".strip()
26312732
+69-7
src/bot/services/message_handler.py
···2020 self.agent = PhiAgent()
21212222 async def handle_notification(self, notification):
2323- """Process any notification type."""
2323+ """Process any notification through the agent."""
2424 reason = notification.reason
2525 author_handle = notification.author.handle
2626···2828 if reason in ("mention", "reply", "quote"):
2929 await self._handle_post(notification)
3030 elif reason in ("like", "repost"):
3131- # someone engaged with phi's content — log for awareness
3232- logger.info(f"{reason} from @{author_handle}")
3333- bot_status.record_mention()
3131+ await self._handle_engagement(notification)
3432 elif reason == "follow":
3535- logger.info(f"followed by @{author_handle}")
3636- bot_status.record_mention()
3333+ await self._handle_follow(notification)
3734 else:
3835 logger.debug(f"notification type '{reason}' from @{author_handle}")
3936 except Exception as e:
4037 logger.exception(f"notification handling error: {e}")
4138 bot_status.record_error()
42394040+ async def _handle_engagement(self, notification):
4141+ """Process a like or repost — someone engaged with phi's content."""
4242+ reason = notification.reason
4343+ author_handle = notification.author.handle
4444+ post_uri = notification.uri
4545+4646+ # Fetch phi's post that was liked/reposted
4747+ posts = await self.client.get_posts([post_uri])
4848+ if not posts.posts:
4949+ logger.warning(f"could not find post {post_uri}")
5050+ return
5151+5252+ post = posts.posts[0]
5353+ post_text = post.record.text if hasattr(post.record, "text") else ""
5454+5555+ bot_status.record_mention()
5656+5757+ mention_text = f"[notification: @{author_handle} {reason}d your post]\nyour post: {post_text}"
5858+5959+ response = await self.agent.process_mention(
6060+ mention_text=mention_text,
6161+ author_handle=author_handle,
6262+ thread_context="",
6363+ )
6464+6565+ if response.action == "ignore":
6666+ logger.info(f"ignoring {reason} from @{author_handle}: {response.reason}")
6767+ elif response.action == "reply" and response.text:
6868+ # reply to phi's own post as a follow-up
6969+ parent_ref = models.ComAtprotoRepoStrongRef.Main(uri=post_uri, cid=post.cid)
7070+ if hasattr(post.record, "reply") and post.record.reply:
7171+ root_ref = post.record.reply.root
7272+ else:
7373+ root_ref = parent_ref
7474+ reply_ref = models.AppBskyFeedPost.ReplyRef(parent=parent_ref, root=root_ref)
7575+ await self.client.create_post(response.text, reply_to=reply_ref)
7676+ bot_status.record_response()
7777+ logger.info(f"replied on {reason} from @{author_handle}: {response.text[:80]}")
7878+ else:
7979+ logger.info(f"{response.action} on {reason} from @{author_handle}")
8080+ bot_status.record_response()
8181+8282+ async def _handle_follow(self, notification):
8383+ """Process a follow notification."""
8484+ author_handle = notification.author.handle
8585+8686+ bot_status.record_mention()
8787+8888+ mention_text = f"[notification: @{author_handle} followed you]"
8989+9090+ response = await self.agent.process_mention(
9191+ mention_text=mention_text,
9292+ author_handle=author_handle,
9393+ thread_context="",
9494+ )
9595+9696+ if response.action == "ignore":
9797+ logger.info(f"ignoring follow from @{author_handle}: {response.reason}")
9898+ elif response.action == "reply" and response.text:
9999+ # post as a top-level post since there's no thread to reply to
100100+ await self.client.create_post(response.text)
101101+ bot_status.record_response()
102102+ logger.info(f"posted on follow from @{author_handle}: {response.text[:80]}")
103103+ else:
104104+ logger.info(f"{response.action} on follow from @{author_handle}")
105105+43106 async def _handle_post(self, notification):
44107 """Process a mention, reply, or quote notification."""
45108 post_uri = notification.uri
···115178 return
116179117180 elif response.action == "reply" and response.text:
118118- # Post reply
119181 reply_ref = models.AppBskyFeedPost.ReplyRef(
120182 parent=parent_ref, root=root_ref
121183 )