diff --git a/ductor_bot/messenger/telegram/app.py b/ductor_bot/messenger/telegram/app.py index cb2f22db..b7abe149 100644 --- a/ductor_bot/messenger/telegram/app.py +++ b/ductor_bot/messenger/telegram/app.py @@ -46,6 +46,7 @@ handle_command, handle_interrupt, handle_new_session, + merge_reply_context, strip_mention, ) from ductor_bot.messenger.telegram.media import ( @@ -1407,7 +1408,10 @@ async def _resolve_text(self, message: Message) -> str | None: ) if not message.text: return None - return strip_mention(message.text, self._bot_username) + text = strip_mention(message.text, self._bot_username) + reply = message.reply_to_message + reply_text = None if reply is None else (reply.text or reply.caption) + return merge_reply_context(text, reply_text) async def _handle_streaming( self, message: Message, key: SessionKey, text: str, *, thread_id: int | None = None diff --git a/ductor_bot/messenger/telegram/handlers.py b/ductor_bot/messenger/telegram/handlers.py index f87798db..fe3fd63e 100644 --- a/ductor_bot/messenger/telegram/handlers.py +++ b/ductor_bot/messenger/telegram/handlers.py @@ -203,3 +203,12 @@ def strip_mention(text: str, bot_username: str | None) -> str: stripped = (text[:idx] + text[idx + len(tag) :]).strip() return stripped or text return text + + +def merge_reply_context(text: str, reply_text: str | None) -> str: + """Combine the user's message with replied-to text when present.""" + message_text = text.strip() + quoted = (reply_text or "").strip() + if not quoted: + return message_text + return f"[REPLY TO]\n{quoted}\n\n[USER MESSAGE]\n{message_text}" diff --git a/tests/messenger/telegram/test_app.py b/tests/messenger/telegram/test_app.py index 1c00bfdb..c7df1bda 100644 --- a/tests/messenger/telegram/test_app.py +++ b/tests/messenger/telegram/test_app.py @@ -100,6 +100,10 @@ def _make_message( type(msg).message_id = PropertyMock(return_value=message_id) msg.text = text msg.answer = AsyncMock(return_value=msg) + msg.reply_to_message = None + msg.entities = None + msg.caption = None + msg.caption_entities = None user = MagicMock(spec=User) user.id = user_id @@ -501,7 +505,7 @@ async def test_returns_early_for_none_text(self) -> None: @patch("ductor_bot.messenger.telegram.app.strip_mention", return_value="clean text") async def test_strips_mention_from_text(self, mock_strip: MagicMock) -> None: tg_bot, _ = _make_tg_bot() - tg_bot.bot_instance_username = "testbot" + tg_bot._bot_username = "testbot" orch = _make_orchestrator() tg_bot._orchestrator = orch @@ -525,12 +529,30 @@ async def test_strips_mention_from_text(self, mock_strip: MagicMock) -> None: class TestResolveText: async def test_plain_text_message(self) -> None: tg_bot, _ = _make_tg_bot() - tg_bot.bot_instance_username = "mybot" + tg_bot._bot_username = "mybot" tg_bot._orchestrator = _make_orchestrator() msg = _make_message(text="Hello") result = await tg_bot._resolve_text(msg) assert result == "Hello" + async def test_reply_includes_replied_message_text(self) -> None: + tg_bot, _ = _make_tg_bot() + tg_bot._bot_username = "mybot" + tg_bot._orchestrator = _make_orchestrator() + + msg = _make_message(text="@mybot what do you think?", chat_type="group") + reply = MagicMock(spec=Message) + reply.text = "Original message text" + reply.caption = None + msg.reply_to_message = reply + + result = await tg_bot._resolve_text(msg) + + assert result == ( + "[REPLY TO]\nOriginal message text\n\n" + "[USER MESSAGE]\nwhat do you think?" + ) + async def test_none_when_no_text_and_no_media(self) -> None: tg_bot, _ = _make_tg_bot() tg_bot._orchestrator = _make_orchestrator()