agent_errors.py•10.5 kB
"""
Agent-friendly error handler with helpful fix suggestions.
This module provides decorators and utilities to catch common errors
and provide actionable solutions for agents.
"""
import asyncio
import functools
import traceback
from collections.abc import Callable
from typing import Any, TypeVar
from maverick_mcp.utils.logging import get_logger
logger = get_logger(__name__)
T = TypeVar("T")
# Common error patterns and their fixes
ERROR_FIXES = {
# DataFrame column errors
"KeyError.*close": {
"error": "DataFrame column 'close' not found",
"fix": "Use 'Close' with capital C - DataFrame columns are case-sensitive",
"example": "df['Close'] not df['close']",
},
"KeyError.*open": {
"error": "DataFrame column 'open' not found",
"fix": "Use 'Open' with capital O",
"example": "df['Open'] not df['open']",
},
"KeyError.*high": {
"error": "DataFrame column 'high' not found",
"fix": "Use 'High' with capital H",
"example": "df['High'] not df['high']",
},
"KeyError.*low": {
"error": "DataFrame column 'low' not found",
"fix": "Use 'Low' with capital L",
"example": "df['Low'] not df['low']",
},
"KeyError.*volume": {
"error": "DataFrame column 'volume' not found",
"fix": "Use 'Volume' with capital V",
"example": "df['Volume'] not df['volume']",
},
# Authentication errors
"401.*Unauthorized": {
"error": "Authentication required",
"fix": "Set AUTH_ENABLED=false for development or use generate_dev_token tool",
"example": "AUTH_ENABLED=false python -m maverick_mcp.api.server",
},
# Connection errors
"Redis.*Connection.*refused": {
"error": "Redis connection failed",
"fix": "Start Redis: brew services start redis",
"example": "Or set REDIS_HOST=none to skip caching",
},
"psycopg2.*could not connect": {
"error": "PostgreSQL connection failed",
"fix": "Use SQLite for development: DATABASE_URL=sqlite:///dev.db",
"example": "Or start PostgreSQL: brew services start postgresql",
},
# Import errors
"ModuleNotFoundError.*maverick": {
"error": "Maverick MCP modules not found",
"fix": "Install dependencies: uv sync",
"example": "Make sure you're in the project root directory",
},
"ImportError.*ta_lib": {
"error": "TA-Lib not installed",
"fix": "Install TA-Lib: brew install ta-lib && uv pip install ta-lib",
"example": "TA-Lib requires system libraries",
},
# Type errors
"TypeError.*NoneType.*has no attribute": {
"error": "Trying to access attribute on None",
"fix": "Check if the object exists before accessing attributes",
"example": "if obj is not None: obj.attribute",
},
# Value errors
"ValueError.*not enough values to unpack": {
"error": "Unpacking mismatch",
"fix": "Check the return value - it might be None or have fewer values",
"example": "result = func(); if result: a, b = result",
},
# Async errors
"RuntimeError.*no running event loop": {
"error": "Async function called without event loop",
"fix": "Use asyncio.run() or await in async context",
"example": "asyncio.run(async_function())",
},
# File errors
"FileNotFoundError": {
"error": "File not found",
"fix": "Check the file path - use absolute paths for reliability",
"example": "Path(__file__).parent / 'data.csv'",
},
# Port errors
"Address already in use.*8000": {
"error": "Port 8000 already in use",
"fix": "Stop the existing server: make stop",
"example": "Or use a different port: --port 8001",
},
}
def find_error_fix(error_str: str) -> dict[str, str] | None:
"""Find a fix suggestion for the given error string."""
import re
error_str_lower = str(error_str).lower()
for pattern, fix_info in ERROR_FIXES.items():
if re.search(pattern.lower(), error_str_lower):
return fix_info
return None
def agent_friendly_errors[T](
func: Callable[..., T] | None = None,
*,
provide_fix: bool = True,
log_errors: bool = True,
reraise: bool = True,
) -> Callable[..., T] | Callable[[Callable[..., T]], Callable[..., T]]:
"""
Decorator that catches errors and provides helpful fix suggestions.
Args:
provide_fix: Whether to include fix suggestions
log_errors: Whether to log errors
reraise: Whether to re-raise the error after logging
Usage:
@agent_friendly_errors
def my_function():
...
@agent_friendly_errors(reraise=False)
def my_function():
...
"""
def decorator(f: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(f)
def wrapper(*args: Any, **kwargs: Any) -> T:
try:
return f(*args, **kwargs)
except Exception as e:
error_msg = str(e)
error_type = type(e).__name__
# Build error info
error_info = {
"function": f.__name__,
"error_type": error_type,
"error_message": error_msg,
}
# Find fix suggestion
if provide_fix:
fix_info = find_error_fix(error_msg)
if fix_info:
error_info["fix_suggestion"] = fix_info
# Log the error
if log_errors:
logger.error(
f"Error in {f.__name__}: {error_type}: {error_msg}",
extra=error_info,
exc_info=True,
)
if fix_info:
logger.info(
f"💡 Fix suggestion: {fix_info['fix']}",
extra={"example": fix_info.get("example", "")},
)
# Create enhanced error message
if fix_info and provide_fix:
enhanced_msg = (
f"{error_msg}\n\n"
f"💡 Fix: {fix_info['fix']}\n"
f"Example: {fix_info.get('example', '')}"
)
# Replace the error message
e.args = (enhanced_msg,) + e.args[1:]
if reraise:
raise
# Return error info if not re-raising
return error_info # type: ignore[return-value]
# Add async support
if asyncio.iscoroutinefunction(f):
@functools.wraps(f)
async def async_wrapper(*args: Any, **kwargs: Any) -> T:
try:
return await f(*args, **kwargs)
except Exception as e:
# Same error handling logic
error_msg = str(e)
error_type = type(e).__name__
error_info = {
"function": f.__name__,
"error_type": error_type,
"error_message": error_msg,
}
if provide_fix:
fix_info = find_error_fix(error_msg)
if fix_info:
error_info["fix_suggestion"] = fix_info
if log_errors:
logger.error(
f"Error in {f.__name__}: {error_type}: {error_msg}",
extra=error_info,
exc_info=True,
)
if fix_info:
logger.info(
f"💡 Fix suggestion: {fix_info['fix']}",
extra={"example": fix_info.get("example", "")},
)
if fix_info and provide_fix:
enhanced_msg = (
f"{error_msg}\n\n"
f"💡 Fix: {fix_info['fix']}\n"
f"Example: {fix_info.get('example', '')}"
)
e.args = (enhanced_msg,) + e.args[1:]
if reraise:
raise
return error_info # type: ignore[return-value]
return async_wrapper # type: ignore[return-value]
return wrapper
# Handle being called with or without parentheses
if func is None:
return decorator
else:
return decorator(func)
# Context manager for agent-friendly error handling
class AgentErrorContext:
"""Context manager that provides helpful error messages."""
def __init__(self, operation: str = "operation"):
self.operation = operation
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
if exc_type is not None:
error_msg = str(exc_val)
fix_info = find_error_fix(error_msg)
if fix_info:
logger.error(
f"Error during {self.operation}: {exc_type.__name__}: {error_msg}"
)
logger.info(
f"💡 Fix: {fix_info['fix']}",
extra={"example": fix_info.get("example", "")},
)
# Don't suppress the exception
return False
return False
# Utility function to get common error context
def get_error_context(error: Exception) -> dict[str, Any]:
"""Extract useful context from an error."""
context = {
"error_type": type(error).__name__,
"error_message": str(error),
"traceback": traceback.format_exc().split("\n"),
}
# Add specific context based on error type
if isinstance(error, KeyError):
context["key"] = error.args[0] if error.args else "unknown"
elif isinstance(error, ValueError):
context["value_error_details"] = error.args
elif isinstance(error, ConnectionError):
context["connection_type"] = "network"
elif hasattr(error, "response"): # HTTP errors
context["status_code"] = getattr(error.response, "status_code", None)
context["response_text"] = getattr(error.response, "text", None)
return context