Files
miku-discord/bot/utils/error_handler.py

268 lines
8.7 KiB
Python

# utils/error_handler.py
import aiohttp
import traceback
import datetime
import re
from utils.logger import get_logger
logger = get_logger('error_handler')
# Webhook URL for error notifications
ERROR_WEBHOOK_URL = "https://discord.com/api/webhooks/1462216811293708522/4kdGenpxZFsP0z3VBgebYENODKmcRrmEzoIwCN81jCirnAxuU2YvxGgwGCNBb6TInA9Z"
# User-friendly error message that Miku will say
MIKU_ERROR_MESSAGE = "Someone tell Koko-nii there is a problem with my AI."
def is_error_response(response_text: str) -> bool:
"""
Detect if a response text is an error message.
Args:
response_text: The response text to check
Returns:
bool: True if the response appears to be an error message
"""
if not response_text or not isinstance(response_text, str):
return False
response_lower = response_text.lower().strip()
# Common error patterns
error_patterns = [
r'^error:?\s*\d{3}', # "Error: 502" or "Error 502"
r'^error:?\s+', # "Error: " or "Error "
r'^\d{3}\s+error', # "502 Error"
r'^sorry,?\s+(there\s+was\s+)?an?\s+error', # "Sorry, an error" or "Sorry, there was an error"
r'^sorry,?\s+the\s+response\s+took\s+too\s+long', # Timeout error
r'connection\s+(refused|failed|error|timeout)',
r'timed?\s*out',
r'failed\s+to\s+(connect|respond|process)',
r'service\s+unavailable',
r'internal\s+server\s+error',
r'bad\s+gateway',
r'gateway\s+timeout',
]
# Check if response matches any error pattern
for pattern in error_patterns:
if re.search(pattern, response_lower):
return True
# Check for HTTP status codes indicating errors
if re.match(r'^\d{3}$', response_text.strip()):
status_code = int(response_text.strip())
if status_code >= 400: # HTTP error codes
return True
return False
async def send_error_webhook(error_message: str, context: dict = None):
"""
Send error notification to the webhook.
Args:
error_message: The error message or exception details
context: Optional dictionary with additional context (user, channel, etc.)
"""
try:
def truncate_field(text: str, max_length: int = 1000) -> str:
"""Truncate text to fit Discord's field value limit (1024 chars)."""
if not text:
return "N/A"
text = str(text)
if len(text) > max_length:
return text[:max_length - 20] + "\n...(truncated)"
return text
# Build embed for webhook
embed = {
"title": "🚨 Miku Bot Error",
"color": 0xFF0000, # Red color
"timestamp": datetime.datetime.utcnow().isoformat(),
"fields": []
}
# Add error message (limit to 1000 chars to leave room for code blocks)
error_value = f"```\n{truncate_field(error_message, 900)}\n```"
embed["fields"].append({
"name": "Error Message",
"value": error_value,
"inline": False
})
# Add context if provided
if context:
if 'user' in context and context['user']:
embed["fields"].append({
"name": "User",
"value": truncate_field(context['user'], 200),
"inline": True
})
if 'channel' in context and context['channel']:
embed["fields"].append({
"name": "Channel",
"value": truncate_field(context['channel'], 200),
"inline": True
})
if 'guild' in context and context['guild']:
embed["fields"].append({
"name": "Server",
"value": truncate_field(context['guild'], 200),
"inline": True
})
if 'prompt' in context and context['prompt']:
prompt_value = f"```\n{truncate_field(context['prompt'], 400)}\n```"
embed["fields"].append({
"name": "User Prompt",
"value": prompt_value,
"inline": False
})
if 'exception_type' in context and context['exception_type']:
embed["fields"].append({
"name": "Exception Type",
"value": f"`{truncate_field(context['exception_type'], 200)}`",
"inline": True
})
if 'traceback' in context and context['traceback']:
tb_value = f"```python\n{truncate_field(context['traceback'], 800)}\n```"
embed["fields"].append({
"name": "Traceback",
"value": tb_value,
"inline": False
})
# Ensure we have at least one field (Discord requirement)
if not embed["fields"]:
embed["fields"].append({
"name": "Status",
"value": "Error occurred with no additional context",
"inline": False
})
# Send webhook
payload = {
"content": "<@344584170839236608>", # Mention Koko-nii
"embeds": [embed]
}
async with aiohttp.ClientSession() as session:
async with session.post(ERROR_WEBHOOK_URL, json=payload) as response:
if response.status in [200, 204]:
logger.info(f"✅ Error webhook sent successfully")
else:
error_text = await response.text()
logger.error(f"❌ Failed to send error webhook: {response.status} - {error_text}")
except Exception as e:
logger.error(f"❌ Exception while sending error webhook: {e}")
logger.error(traceback.format_exc())
async def handle_llm_error(
error: Exception,
user_prompt: str = None,
user_id: str = None,
guild_id: str = None,
author_name: str = None
) -> str:
"""
Handle LLM errors by logging them and sending webhook notification.
Args:
error: The exception that occurred
user_prompt: The user's prompt (if available)
user_id: The user ID (if available)
guild_id: The guild ID (if available)
author_name: The user's display name (if available)
Returns:
str: User-friendly error message for Miku to say
"""
logger.error(f"🚨 LLM Error occurred: {type(error).__name__}: {str(error)}")
# Build context
context = {
"exception_type": type(error).__name__,
"traceback": traceback.format_exc()
}
if user_prompt:
context["prompt"] = user_prompt
if author_name:
context["user"] = author_name
elif user_id:
context["user"] = f"User ID: {user_id}"
if guild_id:
context["guild"] = f"Guild ID: {guild_id}"
# Get full error message
error_message = f"{type(error).__name__}: {str(error)}"
# Send webhook notification
await send_error_webhook(error_message, context)
return MIKU_ERROR_MESSAGE
async def handle_response_error(
response_text: str,
user_prompt: str = None,
user_id: str = None,
guild_id: str = None,
author_name: str = None,
channel_name: str = None
) -> str:
"""
Handle error responses from the LLM by checking if the response is an error message.
Args:
response_text: The response text from the LLM
user_prompt: The user's prompt (if available)
user_id: The user ID (if available)
guild_id: The guild ID (if available)
author_name: The user's display name (if available)
channel_name: The channel name (if available)
Returns:
str: Either the original response (if not an error) or user-friendly error message
"""
if not is_error_response(response_text):
return response_text
logger.error(f"🚨 Error response detected: {response_text}")
# Build context
context = {}
if user_prompt:
context["prompt"] = user_prompt
if author_name:
context["user"] = author_name
elif user_id:
context["user"] = f"User ID: {user_id}"
if channel_name:
context["channel"] = channel_name
elif guild_id:
context["channel"] = f"Guild ID: {guild_id}"
if guild_id:
context["guild"] = f"Guild ID: {guild_id}"
# Send webhook notification
await send_error_webhook(f"LLM returned error response: {response_text}", context)
return MIKU_ERROR_MESSAGE