open-notebook/open_notebook/utils/error_classifier.py
Luis Novo 5d84ab0768 fix: embedding batch sizing and 413 error classification (1.7.4)
- Add batching to generate_embeddings() (50 texts per batch with per-batch retry)
  to prevent 413 Payload Too Large errors on large documents
- Add 413 error classification rule for user-friendly error messages
- Fix misleading "Created 0 embedded chunks" log in process_source_command
  by removing premature get_embedded_chunks() call (embedding is fire-and-forget)

Closes #594
2026-02-18 11:39:47 -03:00

103 lines
3.6 KiB
Python

"""
Error classification utility for LLM provider errors.
Maps raw exceptions from AI providers/Esperanto/LangChain to user-friendly
error messages and appropriate exception types.
"""
from loguru import logger
from open_notebook.exceptions import (
AuthenticationError,
ConfigurationError,
ExternalServiceError,
NetworkError,
OpenNotebookError,
RateLimitError,
)
# Classification rules: (keywords, exception_class, user_message or None to pass through)
_CLASSIFICATION_RULES: list[tuple[list[str], type[OpenNotebookError], str | None]] = [
# Authentication errors
(
["authentication", "unauthorized", "invalid api key", "invalid_api_key", "401"],
AuthenticationError,
"Authentication failed. Please check your API key in Settings -> Credentials.",
),
# Rate limit errors
(
["rate limit", "rate_limit", "429", "too many requests", "quota exceeded"],
RateLimitError,
"Rate limit exceeded. Please wait a moment and try again.",
),
# Model not found (pass through original message)
(
["model not found", "does not exist", "model_not_found"],
ConfigurationError,
None,
),
# Configuration errors from provision.py (pass through)
(
["no model configured", "please go to settings"],
ConfigurationError,
None,
),
# Network errors
(
["connecterror", "timeoutexception", "connection refused", "connection error", "timed out", "timeout"],
NetworkError,
"Could not connect to the AI provider. Please check your network connection and provider URL.",
),
# Context length errors
(
["context length", "token limit", "maximum context", "context_length_exceeded", "max_tokens"],
ExternalServiceError,
"Content too large for the selected model. Try using a smaller selection or a model with a larger context window.",
),
# Payload too large errors
(
["413", "payload too large", "request entity too large"],
ExternalServiceError,
"The request payload is too large for the AI provider. Try reducing the content size or using a different model.",
),
# Provider availability errors
(
["500", "502", "503", "service unavailable", "overloaded", "internal server error"],
ExternalServiceError,
"The AI provider is temporarily unavailable. Please try again in a few minutes.",
),
]
def classify_error(exception: BaseException) -> tuple[type[OpenNotebookError], str]:
"""
Classify a raw exception into a user-friendly error type and message.
Args:
exception: Any exception from LLM providers/Esperanto/LangChain
Returns:
Tuple of (exception_class, user_friendly_message)
"""
error_str = str(exception).lower()
error_type_name = type(exception).__name__.lower()
combined = f"{error_type_name}: {error_str}"
for keywords, exc_class, message in _CLASSIFICATION_RULES:
for keyword in keywords:
if keyword in combined:
user_message = message if message is not None else _truncate(str(exception))
return exc_class, user_message
# Unclassified error - log for future improvement
logger.warning(
f"Unclassified LLM error ({type(exception).__name__}): {exception}"
)
return ExternalServiceError, f"AI service error: {_truncate(str(exception))}"
def _truncate(text: str, max_length: int = 200) -> str:
"""Truncate text to max_length to avoid leaking verbose internal details."""
if len(text) <= max_length:
return text
return text[:max_length] + "..."