Error in llama-swap catchall implemented + webhook notifier
This commit is contained in:
267
bot/utils/error_handler.py
Normal file
267
bot/utils/error_handler.py
Normal file
@@ -0,0 +1,267 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user