diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index c68d74d..4d13162 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -134,11 +134,11 @@ class AgentLoop: await self._mcp_stack.__aenter__() await connect_mcp_servers(self._mcp_servers, self.tools, self._mcp_stack) - def _set_tool_context(self, channel: str, chat_id: str) -> None: + def _set_tool_context(self, channel: str, chat_id: str, message_id: str | None = None) -> None: """Update context for all tools that need routing info.""" if message_tool := self.tools.get("message"): if isinstance(message_tool, MessageTool): - message_tool.set_context(channel, chat_id) + message_tool.set_context(channel, chat_id, message_id) if spawn_tool := self.tools.get("spawn"): if isinstance(spawn_tool, SpawnTool): @@ -342,7 +342,7 @@ class AgentLoop: asyncio.create_task(_consolidate_and_unlock()) - self._set_tool_context(msg.channel, msg.chat_id) + self._set_tool_context(msg.channel, msg.chat_id, msg.metadata.get("message_id")) initial_messages = self.context.build_messages( history=session.get_history(max_messages=self.memory_window), current_message=msg.content, @@ -400,7 +400,7 @@ class AgentLoop: session_key = f"{origin_channel}:{origin_chat_id}" session = self.sessions.get_or_create(session_key) - self._set_tool_context(origin_channel, origin_chat_id) + self._set_tool_context(origin_channel, origin_chat_id, msg.metadata.get("message_id")) initial_messages = self.context.build_messages( history=session.get_history(max_messages=self.memory_window), current_message=msg.content, diff --git a/nanobot/agent/tools/message.py b/nanobot/agent/tools/message.py index 3853725..10947c4 100644 --- a/nanobot/agent/tools/message.py +++ b/nanobot/agent/tools/message.py @@ -13,16 +13,19 @@ class MessageTool(Tool): self, send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None, default_channel: str = "", - default_chat_id: str = "" + default_chat_id: str = "", + default_message_id: str | None = None ): self._send_callback = send_callback self._default_channel = default_channel self._default_chat_id = default_chat_id + self._default_message_id = default_message_id - def set_context(self, channel: str, chat_id: str) -> None: + def set_context(self, channel: str, chat_id: str, message_id: str | None = None) -> None: """Set the current message context.""" self._default_channel = channel self._default_chat_id = chat_id + self._default_message_id = message_id def set_send_callback(self, callback: Callable[[OutboundMessage], Awaitable[None]]) -> None: """Set the callback for sending messages.""" @@ -67,11 +70,13 @@ class MessageTool(Tool): content: str, channel: str | None = None, chat_id: str | None = None, + message_id: str | None = None, media: list[str] | None = None, **kwargs: Any ) -> str: channel = channel or self._default_channel chat_id = chat_id or self._default_chat_id + message_id = message_id or self._default_message_id if not channel or not chat_id: return "Error: No target channel/chat specified" @@ -83,7 +88,10 @@ class MessageTool(Tool): channel=channel, chat_id=chat_id, content=content, - media=media or [] + media=media or [], + metadata={ + "message_id": message_id, + } ) try: diff --git a/nanobot/channels/telegram.py b/nanobot/channels/telegram.py index 768e565..d29fdfd 100644 --- a/nanobot/channels/telegram.py +++ b/nanobot/channels/telegram.py @@ -5,7 +5,7 @@ from __future__ import annotations import asyncio import re from loguru import logger -from telegram import BotCommand, Update +from telegram import BotCommand, Update, ReplyParameters from telegram.ext import Application, CommandHandler, MessageHandler, filters, ContextTypes from telegram.request import HTTPXRequest @@ -224,6 +224,15 @@ class TelegramChannel(BaseChannel): logger.error("Invalid chat_id: {}", msg.chat_id) return + # Build reply parameters (Will reply to the message if it exists) + reply_to_message_id = msg.metadata.get("message_id") + reply_params = None + if reply_to_message_id: + reply_params = ReplyParameters( + message_id=reply_to_message_id, + allow_sending_without_reply=True + ) + # Send media files for media_path in (msg.media or []): try: @@ -235,22 +244,39 @@ class TelegramChannel(BaseChannel): }.get(media_type, self._app.bot.send_document) param = "photo" if media_type == "photo" else media_type if media_type in ("voice", "audio") else "document" with open(media_path, 'rb') as f: - await sender(chat_id=chat_id, **{param: f}) + await sender( + chat_id=chat_id, + **{param: f}, + reply_parameters=reply_params + ) except Exception as e: filename = media_path.rsplit("/", 1)[-1] logger.error("Failed to send media {}: {}", media_path, e) - await self._app.bot.send_message(chat_id=chat_id, text=f"[Failed to send: {filename}]") + await self._app.bot.send_message( + chat_id=chat_id, + text=f"[Failed to send: {filename}]", + reply_parameters=reply_params + ) # Send text content if msg.content and msg.content != "[empty message]": for chunk in _split_message(msg.content): try: html = _markdown_to_telegram_html(chunk) - await self._app.bot.send_message(chat_id=chat_id, text=html, parse_mode="HTML") + await self._app.bot.send_message( + chat_id=chat_id, + text=html, + parse_mode="HTML", + reply_parameters=reply_params + ) except Exception as e: logger.warning("HTML parse failed, falling back to plain text: {}", e) try: - await self._app.bot.send_message(chat_id=chat_id, text=chunk) + await self._app.bot.send_message( + chat_id=chat_id, + text=chunk, + reply_parameters=reply_params + ) except Exception as e2: logger.error("Error sending Telegram message: {}", e2)