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