| 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 |