Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:49:50 +08:00
commit adc4b2be25
147 changed files with 24716 additions and 0 deletions

View File

@@ -0,0 +1,635 @@
# Error Handling Guide
**Comprehensive error handling for MXCP servers: SQL errors (managed by MXCP) and Python errors (YOU must handle).**
## Two Types of Error Handling
### 1. SQL Errors (Managed by MXCP)
**MXCP automatically handles**:
- SQL syntax errors
- Type mismatches
- Parameter binding errors
- Database connection errors
**Your responsibility**:
- Write correct SQL
- Use proper parameter binding (`$param`)
- Match return types to actual data
### 2. Python Errors (YOU Must Handle)
**You MUST handle**:
- External API failures
- Invalid input
- Resource not found
- Business logic errors
- Async/await errors
**Return structured error objects, don't raise exceptions to MXCP.**
## Python Error Handling Pattern
### ❌ WRONG: Let Exceptions Bubble Up
```python
# python/api_wrapper.py
async def fetch_user(user_id: int) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(f"https://api.example.com/users/{user_id}")
response.raise_for_status() # ❌ Will crash if 404/500!
return response.json()
```
**Problem**: When API returns 404, exception crashes the tool. LLM gets unhelpful error.
### ✅ CORRECT: Return Structured Errors
```python
# python/api_wrapper.py
import httpx
async def fetch_user(user_id: int) -> dict:
"""
Fetch user from external API.
Returns:
Success: {"success": true, "user": {...}}
Error: {"success": false, "error": "User not found", "error_code": "NOT_FOUND"}
"""
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(
f"https://api.example.com/users/{user_id}"
)
if response.status_code == 404:
return {
"success": False,
"error": f"User with ID {user_id} not found",
"error_code": "NOT_FOUND",
"user_id": user_id
}
if response.status_code >= 500:
return {
"success": False,
"error": "External API is currently unavailable. Please try again later.",
"error_code": "API_ERROR",
"status_code": response.status_code
}
response.raise_for_status() # Other HTTP errors
return {
"success": True,
"user": response.json()
}
except httpx.TimeoutException:
return {
"success": False,
"error": "Request timed out after 10 seconds. The API may be slow or unavailable.",
"error_code": "TIMEOUT"
}
except httpx.HTTPError as e:
return {
"success": False,
"error": f"HTTP error occurred: {str(e)}",
"error_code": "HTTP_ERROR"
}
except Exception as e:
return {
"success": False,
"error": f"Unexpected error: {str(e)}",
"error_code": "UNKNOWN_ERROR"
}
```
**Why good**:
- ✅ LLM gets clear error message
- ✅ LLM knows what went wrong (error_code)
- ✅ LLM can take action (retry, try different ID, etc.)
- ✅ Tool never crashes
## Error Response Structure
### Standard Error Format
```python
{
"success": False,
"error": "Human-readable error message for LLM",
"error_code": "MACHINE_READABLE_CODE",
"details": { # Optional: additional context
"attempted_value": user_id,
"valid_range": "1-1000"
}
}
```
### Standard Success Format
```python
{
"success": True,
"data": {
# Actual response data
}
}
```
## Common Error Scenarios
### 1. Input Validation Errors
```python
def process_order(order_id: str, quantity: int) -> dict:
"""Process an order with validation"""
# Validate order_id format
if not order_id.startswith("ORD_"):
return {
"success": False,
"error": f"Invalid order ID format. Expected format: 'ORD_XXXXX', got: '{order_id}'",
"error_code": "INVALID_FORMAT",
"expected_format": "ORD_XXXXX",
"provided": order_id
}
# Validate quantity range
if quantity <= 0:
return {
"success": False,
"error": f"Quantity must be positive. Got: {quantity}",
"error_code": "INVALID_QUANTITY",
"provided": quantity,
"valid_range": "1 or greater"
}
if quantity > 1000:
return {
"success": False,
"error": f"Quantity {quantity} exceeds maximum allowed (1000). Please split into multiple orders.",
"error_code": "QUANTITY_EXCEEDED",
"provided": quantity,
"maximum": 1000
}
# Process order...
return {"success": True, "order_id": order_id, "quantity": quantity}
```
### 2. Resource Not Found Errors
```python
from mxcp.runtime import db
def get_customer(customer_id: str) -> dict:
"""Get customer by ID with proper error handling"""
try:
result = db.execute(
"SELECT * FROM customers WHERE customer_id = $1",
{"customer_id": customer_id}
)
customer = result.fetchone()
if customer is None:
return {
"success": False,
"error": f"Customer '{customer_id}' not found in database. Use list_customers to see available customers.",
"error_code": "CUSTOMER_NOT_FOUND",
"customer_id": customer_id,
"suggestion": "Call list_customers tool to see all available customer IDs"
}
return {
"success": True,
"customer": dict(customer)
}
except Exception as e:
return {
"success": False,
"error": f"Database error while fetching customer: {str(e)}",
"error_code": "DATABASE_ERROR"
}
```
### 3. External API Errors
```python
import httpx
async def create_customer_in_stripe(email: str, name: str) -> dict:
"""Create Stripe customer with comprehensive error handling"""
try:
import stripe
from mxcp.runtime import get_secret
# Get API key
secret = get_secret("stripe")
if not secret:
return {
"success": False,
"error": "Stripe API key not configured. Please set up 'stripe' secret in config.yml",
"error_code": "MISSING_CREDENTIALS",
"required_secret": "stripe"
}
stripe.api_key = secret.get("api_key")
# Create customer
customer = stripe.Customer.create(
email=email,
name=name
)
return {
"success": True,
"customer_id": customer.id,
"email": customer.email
}
except stripe.error.InvalidRequestError as e:
return {
"success": False,
"error": f"Invalid request to Stripe: {str(e)}",
"error_code": "INVALID_REQUEST",
"details": str(e)
}
except stripe.error.AuthenticationError:
return {
"success": False,
"error": "Stripe API key is invalid or expired. Please update credentials.",
"error_code": "AUTHENTICATION_FAILED"
}
except stripe.error.RateLimitError:
return {
"success": False,
"error": "Stripe rate limit exceeded. Please try again in a few seconds.",
"error_code": "RATE_LIMIT",
"suggestion": "Wait 5-10 seconds and retry"
}
except stripe.error.StripeError as e:
return {
"success": False,
"error": f"Stripe error: {str(e)}",
"error_code": "STRIPE_ERROR"
}
except ImportError:
return {
"success": False,
"error": "Stripe library not installed. Run: pip install stripe",
"error_code": "MISSING_DEPENDENCY",
"fix": "pip install stripe>=5.0.0"
}
except Exception as e:
return {
"success": False,
"error": f"Unexpected error: {str(e)}",
"error_code": "UNKNOWN_ERROR"
}
```
### 4. Business Logic Errors
```python
def transfer_funds(from_account: str, to_account: str, amount: float) -> dict:
"""Transfer funds with business logic validation"""
# Check amount
if amount <= 0:
return {
"success": False,
"error": f"Transfer amount must be positive. Got: ${amount}",
"error_code": "INVALID_AMOUNT"
}
# Check account exists and get balance
from_balance = db.execute(
"SELECT balance FROM accounts WHERE account_id = $1",
{"account_id": from_account}
).fetchone()
if from_balance is None:
return {
"success": False,
"error": f"Source account '{from_account}' not found",
"error_code": "ACCOUNT_NOT_FOUND",
"account_id": from_account
}
# Check sufficient funds
if from_balance["balance"] < amount:
return {
"success": False,
"error": f"Insufficient funds. Available: ${from_balance['balance']:.2f}, Requested: ${amount:.2f}",
"error_code": "INSUFFICIENT_FUNDS",
"available": from_balance["balance"],
"requested": amount,
"shortfall": amount - from_balance["balance"]
}
# Perform transfer...
return {
"success": True,
"transfer_id": "TXN_12345",
"from_account": from_account,
"to_account": to_account,
"amount": amount
}
```
### 5. Async/Await Errors
```python
import asyncio
async def fetch_multiple_users(user_ids: list[int]) -> dict:
"""Fetch multiple users concurrently with error handling"""
async def fetch_one(user_id: int) -> dict:
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(f"https://api.example.com/users/{user_id}")
if response.status_code == 404:
return {
"user_id": user_id,
"success": False,
"error": f"User {user_id} not found"
}
response.raise_for_status()
return {
"user_id": user_id,
"success": True,
"user": response.json()
}
except asyncio.TimeoutError:
return {
"user_id": user_id,
"success": False,
"error": f"Timeout fetching user {user_id}"
}
except Exception as e:
return {
"user_id": user_id,
"success": False,
"error": str(e)
}
# Fetch all concurrently
results = await asyncio.gather(*[fetch_one(uid) for uid in user_ids])
# Separate successes and failures
successes = [r for r in results if r["success"]]
failures = [r for r in results if not r["success"]]
return {
"success": len(failures) == 0,
"total_requested": len(user_ids),
"successful": len(successes),
"failed": len(failures),
"users": [r["user"] for r in successes],
"errors": [{"user_id": r["user_id"], "error": r["error"]} for r in failures]
}
```
## Error Messages for LLMs
### Principles for Good Error Messages
1. **Be Specific**: Tell exactly what went wrong
2. **Be Actionable**: Suggest what to do next
3. **Provide Context**: Include relevant values/IDs
4. **Use Plain Language**: Avoid technical jargon
### ❌ BAD Error Messages
```python
return {"error": "Error"} # ❌ Useless
return {"error": "Invalid input"} # ❌ Which input? Why invalid?
return {"error": "DB error"} # ❌ What kind of error?
return {"error": str(e)} # ❌ Raw exception message (often cryptic)
```
### ✅ GOOD Error Messages
```python
return {
"error": "Customer ID 'CUST_999' not found. Use list_customers to see available IDs."
}
return {
"error": "Date format invalid. Expected 'YYYY-MM-DD' (e.g., '2024-01-15'), got: '01/15/2024'"
}
return {
"error": "Quantity 5000 exceeds maximum allowed (1000). Split into multiple orders or contact support."
}
return {
"error": "API rate limit exceeded. Please wait 30 seconds and try again."
}
```
## SQL Error Handling (MXCP Managed)
### You Don't Handle These (MXCP Does)
MXCP automatically handles and returns errors for:
- Invalid SQL syntax
- Missing tables/columns
- Type mismatches
- Parameter binding errors
**Your job**: Write correct SQL and let MXCP handle errors.
### Prevent SQL Errors
#### 1. Validate Schema
```yaml
# Always define return types to match SQL output
tool:
name: get_stats
return:
type: object
properties:
total: { type: number } # Matches SQL: SUM(amount)
count: { type: integer } # Matches SQL: COUNT(*)
source:
code: |
SELECT
SUM(amount) as total,
COUNT(*) as count
FROM orders
```
#### 2. Handle NULL Values
```sql
-- BAD: Might return NULL which breaks type system
SELECT amount FROM orders WHERE id = $order_id
-- GOOD: Handle potential NULL
SELECT COALESCE(amount, 0) as amount
FROM orders
WHERE id = $order_id
-- GOOD: Use IFNULL/COALESCE for aggregations
SELECT
COALESCE(SUM(amount), 0) as total,
COALESCE(AVG(amount), 0) as average
FROM orders
WHERE status = $status
```
#### 3. Handle Empty Results
```sql
-- If no results, return empty array (not NULL)
SELECT * FROM customers WHERE city = $city
-- Returns: [] if no customers (MXCP handles this)
-- For aggregations, always return a row
SELECT
COUNT(*) as count,
COALESCE(SUM(amount), 0) as total
FROM orders
WHERE status = $status
-- Always returns one row, even if no matching orders
```
## Error Codes Convention
**Use consistent error codes across your tools**:
```python
# Standard error codes
ERROR_CODES = {
# Input validation
"INVALID_FORMAT": "Input format is incorrect",
"INVALID_RANGE": "Value outside valid range",
"MISSING_REQUIRED": "Required parameter missing",
# Resource errors
"NOT_FOUND": "Resource not found",
"ALREADY_EXISTS": "Resource already exists",
"DELETED": "Resource has been deleted",
# Permission errors
"UNAUTHORIZED": "User not authenticated",
"FORBIDDEN": "User lacks permission",
# External service errors
"API_ERROR": "External API error",
"TIMEOUT": "Request timed out",
"RATE_LIMIT": "Rate limit exceeded",
# System errors
"DATABASE_ERROR": "Database operation failed",
"CONFIGURATION_ERROR": "Missing or invalid configuration",
"DEPENDENCY_ERROR": "Required library not installed",
# Business logic errors
"INSUFFICIENT_FUNDS": "Not enough balance",
"INVALID_STATE": "Operation not allowed in current state",
"QUOTA_EXCEEDED": "Usage quota exceeded",
# Unknown
"UNKNOWN_ERROR": "Unexpected error occurred"
}
```
## Testing Error Handling
### Unit Tests for Error Cases
```python
# tests/test_error_handling.py
import pytest
from python.my_module import fetch_user
@pytest.mark.asyncio
async def test_fetch_user_not_found(httpx_mock):
"""Test 404 error handling"""
httpx_mock.add_response(
url="https://api.example.com/users/999",
status_code=404
)
result = await fetch_user(999)
assert result["success"] is False
assert result["error_code"] == "NOT_FOUND"
assert "999" in result["error"] # Error mentions the ID
@pytest.mark.asyncio
async def test_fetch_user_timeout(httpx_mock):
"""Test timeout handling"""
httpx_mock.add_exception(httpx.TimeoutException("Timeout"))
result = await fetch_user(123)
assert result["success"] is False
assert result["error_code"] == "TIMEOUT"
assert "timeout" in result["error"].lower()
def test_invalid_input():
"""Test input validation"""
result = process_order("INVALID", quantity=5)
assert result["success"] is False
assert result["error_code"] == "INVALID_FORMAT"
assert "ORD_" in result["error"] # Mentions expected format
```
## Error Handling Checklist
Before declaring Python tool complete:
- [ ] All external API calls wrapped in try/except
- [ ] All exceptions return structured error objects
- [ ] Error messages are clear and actionable
- [ ] Error codes are consistent
- [ ] Input validation with helpful error messages
- [ ] NULL/None values handled gracefully
- [ ] Timeout handling for network calls
- [ ] Missing dependencies handled (ImportError)
- [ ] Database errors caught and explained
- [ ] Success/failure clearly indicated in response
- [ ] Unit tests for error scenarios
- [ ] Error messages help LLM understand what to do next
## Summary
**SQL Tools (MXCP Handles)**:
- Write correct SQL
- Handle NULL values with COALESCE
- Match return types to SQL output
**Python Tools (YOU Handle)**:
- ✅ Wrap ALL external calls in try/except
- ✅ Return structured error objects (`{"success": False, "error": "...", "error_code": "..."}`)
- ✅ Validate inputs with clear error messages
- ✅ Be specific and actionable in error messages
- ✅ Use consistent error codes
- ✅ Test error scenarios
- ✅ NEVER let exceptions bubble up to MXCP
**Golden Rule**: Errors should help the LLM understand what went wrong and what to do next.