Unify task status error schema and classify PDF AI failures
This commit is contained in:
@@ -11,7 +11,16 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class PdfAiError(Exception):
|
||||
"""Custom exception for PDF AI service failures."""
|
||||
pass
|
||||
def __init__(
|
||||
self,
|
||||
user_message: str,
|
||||
error_code: str = "PDF_AI_ERROR",
|
||||
detail: str | None = None,
|
||||
):
|
||||
super().__init__(user_message)
|
||||
self.user_message = user_message
|
||||
self.error_code = error_code
|
||||
self.detail = detail
|
||||
|
||||
|
||||
def _estimate_tokens(text: str) -> int:
|
||||
@@ -33,7 +42,11 @@ def _extract_text_from_pdf(input_path: str, max_pages: int = 50) -> str:
|
||||
texts.append(f"[Page {i + 1}]\n{text}")
|
||||
return "\n\n".join(texts)
|
||||
except Exception as e:
|
||||
raise PdfAiError(f"Failed to extract text from PDF: {str(e)}")
|
||||
raise PdfAiError(
|
||||
"Failed to extract text from PDF.",
|
||||
error_code="PDF_TEXT_EXTRACTION_FAILED",
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
def _call_openrouter(
|
||||
@@ -48,7 +61,10 @@ def _call_openrouter(
|
||||
from app.services.ai_cost_service import check_ai_budget, AiBudgetExceededError
|
||||
check_ai_budget()
|
||||
except AiBudgetExceededError:
|
||||
raise PdfAiError("Monthly AI processing budget has been reached. Please try again next month.")
|
||||
raise PdfAiError(
|
||||
"Monthly AI processing budget has been reached. Please try again next month.",
|
||||
error_code="AI_BUDGET_EXCEEDED",
|
||||
)
|
||||
except Exception:
|
||||
pass # Don't block if cost service unavailable
|
||||
|
||||
@@ -57,7 +73,8 @@ def _call_openrouter(
|
||||
if not settings.api_key:
|
||||
logger.error("OPENROUTER_API_KEY is not set or is a placeholder value.")
|
||||
raise PdfAiError(
|
||||
"AI features are temporarily unavailable. Our team has been notified."
|
||||
"AI features are temporarily unavailable. Our team has been notified.",
|
||||
error_code="OPENROUTER_MISSING_API_KEY",
|
||||
)
|
||||
|
||||
messages = [
|
||||
@@ -84,25 +101,29 @@ def _call_openrouter(
|
||||
if response.status_code == 401:
|
||||
logger.error("OpenRouter API key is invalid or expired (401).")
|
||||
raise PdfAiError(
|
||||
"AI features are temporarily unavailable due to a configuration issue. Our team has been notified."
|
||||
"AI features are temporarily unavailable due to a configuration issue. Our team has been notified.",
|
||||
error_code="OPENROUTER_UNAUTHORIZED",
|
||||
)
|
||||
|
||||
if response.status_code == 402:
|
||||
logger.error("OpenRouter account has insufficient credits (402).")
|
||||
raise PdfAiError(
|
||||
"AI processing credits have been exhausted. Please try again later."
|
||||
"AI processing credits have been exhausted. Please try again later.",
|
||||
error_code="OPENROUTER_INSUFFICIENT_CREDITS",
|
||||
)
|
||||
|
||||
if response.status_code == 429:
|
||||
logger.warning("OpenRouter rate limit reached (429).")
|
||||
raise PdfAiError(
|
||||
"AI service is experiencing high demand. Please wait a moment and try again."
|
||||
"AI service is experiencing high demand. Please wait a moment and try again.",
|
||||
error_code="OPENROUTER_RATE_LIMIT",
|
||||
)
|
||||
|
||||
if response.status_code >= 500:
|
||||
logger.error("OpenRouter server error (%s).", response.status_code)
|
||||
raise PdfAiError(
|
||||
"AI service provider is experiencing issues. Please try again shortly."
|
||||
"AI service provider is experiencing issues. Please try again shortly.",
|
||||
error_code="OPENROUTER_SERVER_ERROR",
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
@@ -112,7 +133,11 @@ def _call_openrouter(
|
||||
if data.get("error"):
|
||||
error_msg = data["error"].get("message", "") if isinstance(data["error"], dict) else str(data["error"])
|
||||
logger.error("OpenRouter returned an error payload: %s", error_msg)
|
||||
raise PdfAiError("AI service encountered an issue. Please try again.")
|
||||
raise PdfAiError(
|
||||
"AI service encountered an issue. Please try again.",
|
||||
error_code="OPENROUTER_ERROR_PAYLOAD",
|
||||
detail=error_msg,
|
||||
)
|
||||
|
||||
reply = (
|
||||
data.get("choices", [{}])[0]
|
||||
@@ -122,7 +147,10 @@ def _call_openrouter(
|
||||
)
|
||||
|
||||
if not reply:
|
||||
raise PdfAiError("AI returned an empty response. Please try again.")
|
||||
raise PdfAiError(
|
||||
"AI returned an empty response. Please try again.",
|
||||
error_code="OPENROUTER_EMPTY_RESPONSE",
|
||||
)
|
||||
|
||||
# Log usage
|
||||
try:
|
||||
@@ -142,13 +170,23 @@ def _call_openrouter(
|
||||
except PdfAiError:
|
||||
raise
|
||||
except requests.exceptions.Timeout:
|
||||
raise PdfAiError("AI service timed out. Please try again.")
|
||||
raise PdfAiError(
|
||||
"AI service timed out. Please try again.",
|
||||
error_code="OPENROUTER_TIMEOUT",
|
||||
)
|
||||
except requests.exceptions.ConnectionError:
|
||||
logger.error("Cannot connect to OpenRouter API at %s", settings.base_url)
|
||||
raise PdfAiError("AI service is unreachable. Please try again shortly.")
|
||||
raise PdfAiError(
|
||||
"AI service is unreachable. Please try again shortly.",
|
||||
error_code="OPENROUTER_CONNECTION_ERROR",
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error("OpenRouter API error: %s", e)
|
||||
raise PdfAiError("AI service is temporarily unavailable.")
|
||||
raise PdfAiError(
|
||||
"AI service is temporarily unavailable.",
|
||||
error_code="OPENROUTER_REQUEST_ERROR",
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -166,11 +204,11 @@ def chat_with_pdf(input_path: str, question: str) -> dict:
|
||||
{"reply": "...", "pages_analyzed": int}
|
||||
"""
|
||||
if not question or not question.strip():
|
||||
raise PdfAiError("Please provide a question.")
|
||||
raise PdfAiError("Please provide a question.", error_code="PDF_AI_INVALID_INPUT")
|
||||
|
||||
text = _extract_text_from_pdf(input_path)
|
||||
if not text.strip():
|
||||
raise PdfAiError("Could not extract any text from the PDF.")
|
||||
raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY")
|
||||
|
||||
# Truncate to fit context window
|
||||
max_chars = 12000
|
||||
@@ -206,7 +244,7 @@ def summarize_pdf(input_path: str, length: str = "medium") -> dict:
|
||||
"""
|
||||
text = _extract_text_from_pdf(input_path)
|
||||
if not text.strip():
|
||||
raise PdfAiError("Could not extract any text from the PDF.")
|
||||
raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY")
|
||||
|
||||
length_instruction = {
|
||||
"short": "Provide a brief summary in 2-3 sentences.",
|
||||
@@ -245,11 +283,11 @@ def translate_pdf(input_path: str, target_language: str) -> dict:
|
||||
{"translation": "...", "pages_analyzed": int, "target_language": str}
|
||||
"""
|
||||
if not target_language or not target_language.strip():
|
||||
raise PdfAiError("Please specify a target language.")
|
||||
raise PdfAiError("Please specify a target language.", error_code="PDF_AI_INVALID_INPUT")
|
||||
|
||||
text = _extract_text_from_pdf(input_path)
|
||||
if not text.strip():
|
||||
raise PdfAiError("Could not extract any text from the PDF.")
|
||||
raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY")
|
||||
|
||||
max_chars = 10000
|
||||
truncated = text[:max_chars]
|
||||
@@ -325,7 +363,8 @@ def extract_tables(input_path: str) -> dict:
|
||||
|
||||
if not result_tables:
|
||||
raise PdfAiError(
|
||||
"No tables found in the PDF. This tool works best with PDFs containing tabular data."
|
||||
"No tables found in the PDF. This tool works best with PDFs containing tabular data.",
|
||||
error_code="PDF_TABLES_NOT_FOUND",
|
||||
)
|
||||
|
||||
logger.info(f"Extracted {len(result_tables)} tables from PDF")
|
||||
@@ -338,6 +377,10 @@ def extract_tables(input_path: str) -> dict:
|
||||
except PdfAiError:
|
||||
raise
|
||||
except ImportError:
|
||||
raise PdfAiError("tabula-py library is not installed.")
|
||||
raise PdfAiError("tabula-py library is not installed.", error_code="TABULA_NOT_INSTALLED")
|
||||
except Exception as e:
|
||||
raise PdfAiError(f"Failed to extract tables: {str(e)}")
|
||||
raise PdfAiError(
|
||||
"Failed to extract tables.",
|
||||
error_code="PDF_TABLE_EXTRACTION_FAILED",
|
||||
detail=str(e),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user