Send Telegram intermediate responses separately
Anmol Malik committed
May 28, 2026 at 21:24 UTC
8bc81c7a6a5030f4899f3e4810480915b0950c3c
4 files changed
+104
-2
plugins/_telegram_integration/README.md
+1
-1
@@ -35,7 +35,7 @@ This plugin connects one or more Telegram bots to Agent Zero. Each bot runs inde
35
- **Reply delivery**
36
- Streams tool progress and response text through real Telegram messages updated with `editMessageText`.
37
- Tool progress and the AI response are separate messages; only the AI response replies to the user message.
38
- - `tool_execute_after` intercepts the `response` tool — sends inline progress for `break_loop=false`.
38
+ - `tool_execute_after` intercepts the `response` tool — sends `break_loop=false` updates as separate intermediate Telegram messages.
39
- `process_chain_end` auto-sends the final response, with retry logic on failure.
40
- **Formatting**
41
- Converts Markdown output to Telegram-compatible HTML (bold, italic, strikethrough, code, links, blockquotes, lists).
plugins/_telegram_integration/extensions/python/tool_execute_after/_50_telegram_response.py
+7
-1
@@ -47,6 +47,7 @@ class TelegramResponseIntercept(Extension):
47
async def _send_inline(self, context, tool, response: Response):
48
ensure_dependencies()
49
from plugins._telegram_integration.helpers.handler import send_telegram_reply
50
+ from plugins._telegram_integration.helpers import draft_stream
51
52
agent = self.agent
53
assert agent is not None
@@ -55,7 +56,12 @@ class TelegramResponseIntercept(Extension):
56
attachments = context.data.pop(CTX_TG_ATTACHMENTS, [])
57
keyboard = context.data.pop(CTX_TG_KEYBOARD, None)
58
58
- error = await send_telegram_reply(context, text, attachments or None, keyboard)
59
+ if attachments:
60
+ error = await send_telegram_reply(context, text, attachments or None, keyboard)
61
+ elif await draft_stream.send_intermediate_response(context, text, keyboard):
62
+ error = None
63
+ else:
64
+ error = "Telegram intermediate update was not sent"
65
66
if error:
67
result = agent.read_prompt("fw.telegram.update_error.md", error=error)
plugins/_telegram_integration/helpers/draft_stream.py
+26
@@ -84,6 +84,32 @@ async def update_response(context: AgentContext, response_text: str) -> None:
84
context.data[CTX_TG_RESPONSE_LAST_UPDATE] = now
85
86
87
+async def send_intermediate_response(
88
+ context: AgentContext,
89
+ response_text: str,
90
+ keyboard: list[list[dict]] | None = None,
91
+) -> bool:
92
+ html = _format_response(response_text)
93
+ if not html:
94
+ return False
95
+ bot = _bot_instance(context)
96
+ chat_id = context.data.get(CTX_TG_CHAT_ID)
97
+ if not bot or not chat_id:
98
+ return False
99
+ try:
100
+ sent_id = await tc.raw_send_text(
101
+ bot.bot.token,
102
+ int(chat_id),
103
+ html,
104
+ parse_mode="HTML",
105
+ reply_markup=_keyboard_markup(keyboard),
106
+ )
107
+ return bool(sent_id)
108
+ except Exception as e:
109
+ PrintStyle.debug(f"Telegram intermediate response failed: {e}")
110
+ return False
111
+
112
+
113
async def finalize_response(
114
context: AgentContext,
115
response_text: str,
tests/test_telegram_intermediate_response.py
new
+70
@@ -0,0 +1,70 @@
1
+import asyncio
2
+
3
+from plugins._telegram_integration.helpers import draft_stream
4
+from plugins._telegram_integration.helpers.constants import (
5
+ CTX_TG_BOT,
6
+ CTX_TG_CHAT_ID,
7
+ CTX_TG_REPLY_TO,
8
+ CTX_TG_RESPONSE_MESSAGE_ID,
9
+)
10
+
11
+
12
+class FakeBot:
13
+ class Bot:
14
+ token = "token"
15
+
16
+ bot = Bot()
17
+
18
+
19
+class FakeContext:
20
+ def __init__(self):
21
+ self.data = {
22
+ CTX_TG_BOT: "main",
23
+ CTX_TG_CHAT_ID: 123,
24
+ CTX_TG_REPLY_TO: 456,
25
+ }
26
+
27
+ def get_data(self, key):
28
+ return self.data.get(key)
29
+
30
+
31
+def test_intermediate_response_sends_separate_non_reply_message(monkeypatch):
32
+ calls = []
33
+
34
+ async def fake_send(token, chat_id, text, reply_to_message_id=None, parse_mode="HTML", reply_markup=None):
35
+ calls.append(
36
+ {
37
+ "token": token,
38
+ "chat_id": chat_id,
39
+ "text": text,
40
+ "reply_to_message_id": reply_to_message_id,
41
+ "parse_mode": parse_mode,
42
+ "reply_markup": reply_markup,
43
+ }
44
+ )
45
+ return 789
46
+
47
+ context = FakeContext()
48
+ monkeypatch.setattr(draft_stream, "_bot_instance", lambda ctx: FakeBot())
49
+ monkeypatch.setattr(draft_stream.tc, "raw_send_text", fake_send)
50
+
51
+ sent = asyncio.run(
52
+ draft_stream.send_intermediate_response(
53
+ context,
54
+ "**Working** on the brief.",
55
+ keyboard=[[{"text": "Open", "callback_data": "open"}]],
56
+ )
57
+ )
58
+
59
+ assert sent is True
60
+ assert calls == [
61
+ {
62
+ "token": "token",
63
+ "chat_id": 123,
64
+ "text": "<b>Working</b> on the brief.",
65
+ "reply_to_message_id": None,
66
+ "parse_mode": "HTML",
67
+ "reply_markup": {"inline_keyboard": [[{"text": "Open", "callback_data": "open"}]]},
68
+ }
69
+ ]
70
+ assert CTX_TG_RESPONSE_MESSAGE_ID not in context.data