Show friendly Telegram errors
Anmol Malik committed
May 28, 2026 at 23:11 UTC
92b728c3d770b5e79b54d836c49771a45ad210d3
5 files changed
+241
plugins/_telegram_integration/extensions/python/_functions/agent/Agent/handle_exception/end/_85_telegram_error.py
new
+42
@@ -0,0 +1,42 @@
1
+from helpers.errors import HandledException
2
+from helpers.extension import Extension
3
+from helpers.print_style import PrintStyle
4
+from plugins._telegram_integration.helpers.constants import (
5
+ CTX_TG_BOT,
6
+ CTX_TG_ERROR_SENT,
7
+ CTX_TG_REPLY_TO,
8
+ CTX_TG_TYPING_STOP,
9
+)
10
+
11
+
12
+class TelegramFriendlyError(Extension):
13
+ async def execute(self, data: dict = {}, **kwargs):
14
+ if not self.agent or self.agent.number != 0:
15
+ return
16
+
17
+ context = self.agent.context
18
+ if not context.data.get(CTX_TG_BOT):
19
+ return
20
+
21
+ exception = data.get("exception")
22
+ if not exception or isinstance(exception, HandledException):
23
+ return
24
+
25
+ if context.data.get(CTX_TG_ERROR_SENT):
26
+ return
27
+
28
+ context.data[CTX_TG_ERROR_SENT] = True
29
+
30
+ from plugins._telegram_integration.helpers import draft_stream, error_ui
31
+ from plugins._telegram_integration.helpers.handler import send_telegram_reply
32
+
33
+ text = error_ui.friendly_error_message(exception)
34
+ error = await send_telegram_reply(context, text)
35
+ if error:
36
+ PrintStyle.debug(f"Telegram error reply failed: {error}")
37
+
38
+ typing_stop = context.data.pop(CTX_TG_TYPING_STOP, None)
39
+ if typing_stop:
40
+ typing_stop.set()
41
+ draft_stream.clear(context)
42
+ context.data.pop(CTX_TG_REPLY_TO, None)
plugins/_telegram_integration/helpers/constants.py
+1
@@ -22,3 +22,4 @@ CTX_TG_PROGRESS_LINES = "_telegram_progress_lines"
22
CTX_TG_RESPONSE_MESSAGE_ID = "_telegram_response_message_id"
23
CTX_TG_RESPONSE_TEXT = "_telegram_response_text"
24
CTX_TG_RESPONSE_LAST_UPDATE = "_telegram_response_last_update"
25
+CTX_TG_ERROR_SENT = "_telegram_error_sent"
plugins/_telegram_integration/helpers/draft_stream.py
+2
@@ -18,6 +18,7 @@ from plugins._telegram_integration.helpers.constants import (
18
CTX_TG_RESPONSE_TEXT,
19
CTX_TG_STREAM_ENABLED,
20
CTX_TG_TOOLS_ENABLED,
21
+ CTX_TG_ERROR_SENT,
22
)
23
24
MAX_STREAM_CHARS: int = 3900
@@ -42,6 +43,7 @@ TOOL_EMOJIS: dict[str, str] = {
43
44
45
async def start(context: AgentContext) -> None:
46
+ context.data.pop(CTX_TG_ERROR_SENT, None)
47
# Do not pre-create a placeholder Telegram message. Wait until we have
48
# real assistant text so the stream starts with meaningful content.
49
if not _stream_enabled(context):
plugins/_telegram_integration/helpers/error_ui.py
new
+100
@@ -0,0 +1,100 @@
1
+from __future__ import annotations
2
+
3
+import re
4
+
5
+
6
+MAX_DETAIL_CHARS = 500
7
+
8
+
9
+def friendly_error_message(exception: BaseException) -> str:
10
+ summary = _error_summary(exception)
11
+ category = _classify_error(exception, summary)
12
+
13
+ if category == "auth":
14
+ return (
15
+ "**Agent Zero hit a provider setup issue.**\n\n"
16
+ "The model provider rejected the request because an API key or credential is missing, invalid, or unauthorized.\n\n"
17
+ f"Details: `{summary}`\n\n"
18
+ "Please check the model/API key settings, then send the message again."
19
+ )
20
+
21
+ if category == "rate_limit":
22
+ return (
23
+ "**Agent Zero was rate limited by the model provider.**\n\n"
24
+ "The provider is asking us to slow down before trying again.\n\n"
25
+ f"Details: `{summary}`"
26
+ )
27
+
28
+ if category == "timeout":
29
+ return (
30
+ "**Agent Zero did not get a response in time.**\n\n"
31
+ "The provider or tool call timed out before the agent could finish this request.\n\n"
32
+ f"Details: `{summary}`"
33
+ )
34
+
35
+ if category == "provider":
36
+ return (
37
+ "**Agent Zero could not complete the model request.**\n\n"
38
+ "The model provider returned an error before the agent could finish.\n\n"
39
+ f"Details: `{summary}`"
40
+ )
41
+
42
+ return (
43
+ "**Agent Zero ran into an error while working on this.**\n\n"
44
+ f"Details: `{summary}`"
45
+ )
46
+
47
+
48
+def _classify_error(exception: BaseException, summary: str) -> str:
49
+ text = f"{type(exception).__name__} {summary}".lower()
50
+
51
+ if any(
52
+ marker in text
53
+ for marker in (
54
+ "api key",
55
+ "api_key",
56
+ "apikey",
57
+ "auth",
58
+ "credential",
59
+ "unauthorized",
60
+ "forbidden",
61
+ "invalid key",
62
+ "missing key",
63
+ "permission denied",
64
+ "401",
65
+ "403",
66
+ )
67
+ ):
68
+ return "auth"
69
+
70
+ if any(marker in text for marker in ("rate limit", "ratelimit", "too many requests", "429")):
71
+ return "rate_limit"
72
+
73
+ if any(marker in text for marker in ("timeout", "timed out", "deadline", "read timed out")):
74
+ return "timeout"
75
+
76
+ if any(
77
+ marker in text
78
+ for marker in (
79
+ "litellm",
80
+ "openai",
81
+ "anthropic",
82
+ "model",
83
+ "provider",
84
+ "badrequest",
85
+ "service unavailable",
86
+ "overloaded",
87
+ "503",
88
+ )
89
+ ):
90
+ return "provider"
91
+
92
+ return "generic"
93
+
94
+
95
+def _error_summary(exception: BaseException) -> str:
96
+ text = str(exception).strip() or type(exception).__name__
97
+ text = re.sub(r"\s+", " ", text)
98
+ if len(text) > MAX_DETAIL_CHARS:
99
+ text = f"{text[: MAX_DETAIL_CHARS - 3].rstrip()}..."
100
+ return text
tests/test_telegram_error_ui.py
new
+96
@@ -0,0 +1,96 @@
1
+import asyncio
2
+
3
+from plugins._telegram_integration.extensions.python._functions.agent.Agent.handle_exception.end import (
4
+ _85_telegram_error,
5
+)
6
+from plugins._telegram_integration.helpers import error_ui
7
+from plugins._telegram_integration.helpers.constants import (
8
+ CTX_TG_BOT,
9
+ CTX_TG_ERROR_SENT,
10
+ CTX_TG_REPLY_TO,
11
+ CTX_TG_TYPING_STOP,
12
+)
13
+
14
+
15
+class FakeContext:
16
+ def __init__(self):
17
+ self.data = {
18
+ CTX_TG_BOT: "main",
19
+ CTX_TG_REPLY_TO: 456,
20
+ }
21
+
22
+
23
+class FakeAgent:
24
+ number = 0
25
+
26
+ def __init__(self):
27
+ self.context = FakeContext()
28
+
29
+
30
+def test_friendly_error_message_for_missing_api_key():
31
+ message = error_ui.friendly_error_message(
32
+ RuntimeError("OPENAI_API_KEY is missing or invalid")
33
+ )
34
+
35
+ assert "provider setup issue" in message
36
+ assert "API key" in message
37
+ assert "OPENAI_API_KEY is missing or invalid" in message
38
+
39
+
40
+def test_friendly_error_message_for_rate_limit():
41
+ message = error_ui.friendly_error_message(
42
+ RuntimeError("Rate limit exceeded: too many requests")
43
+ )
44
+
45
+ assert "rate limited" in message
46
+ assert "too many requests" in message
47
+
48
+
49
+def test_telegram_exception_hook_sends_once_and_cleans_stream_state(monkeypatch):
50
+ sends = []
51
+ cleared = []
52
+ typing_stopped = []
53
+
54
+ async def fake_send(context, text, attachments=None, keyboard=None):
55
+ sends.append(
56
+ {
57
+ "text": text,
58
+ "reply_to": context.data.get(CTX_TG_REPLY_TO),
59
+ "attachments": attachments,
60
+ "keyboard": keyboard,
61
+ }
62
+ )
63
+ return None
64
+
65
+ def fake_clear(context):
66
+ cleared.append(True)
67
+
68
+ class FakeStop:
69
+ def set(self):
70
+ typing_stopped.append(True)
71
+
72
+ agent = FakeAgent()
73
+ agent.context.data[CTX_TG_TYPING_STOP] = FakeStop()
74
+
75
+ monkeypatch.setattr(
76
+ "plugins._telegram_integration.helpers.handler.send_telegram_reply",
77
+ fake_send,
78
+ )
79
+ monkeypatch.setattr(
80
+ "plugins._telegram_integration.helpers.draft_stream.clear",
81
+ fake_clear,
82
+ )
83
+
84
+ extension = _85_telegram_error.TelegramFriendlyError(agent=agent)
85
+ data = {"exception": RuntimeError("provider returned 503 service unavailable")}
86
+
87
+ asyncio.run(extension.execute(data=data))
88
+ asyncio.run(extension.execute(data=data))
89
+
90
+ assert len(sends) == 1
91
+ assert "could not complete the model request" in sends[0]["text"]
92
+ assert sends[0]["reply_to"] == 456
93
+ assert cleared == [True]
94
+ assert typing_stopped == [True]
95
+ assert agent.context.data[CTX_TG_ERROR_SENT] is True
96
+ assert CTX_TG_REPLY_TO not in agent.context.data