57 lines
2.3 KiB
Python
57 lines
2.3 KiB
Python
"""Intake-gate entrypoint — one Responses call per email.
|
|
|
|
Pure module: no FastAPI imports and no HTTPException.
|
|
Called from inbox.views.Email; no HTTP surface of its own.
|
|
|
|
Returns (verdict, error_code) and never raises, mirroring
|
|
inbox/plugins.extract_resume_text's (text, error) shape. A provider outage must be a
|
|
policy decision at the call site (INBOX_TRIAGE_FAIL_OPEN in plugins.should_ingest), not
|
|
a 500 on /email/fetch.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
|
|
from app.core.errors import ATSError, classify_error
|
|
|
|
from inbox_classifier.agent_setup import get_classifier
|
|
from inbox_classifier.decorators import email_signals
|
|
from inbox_classifier.plugins import TRIAGE_MAX_BODY_CHARS, TRIAGE_MAX_SUBJECT_CHARS
|
|
|
|
logger=logging.getLogger("inbox.triage")
|
|
|
|
# No subject and no body: there is nothing to judge, so this is unclassifiable rather
|
|
# than a "no". It routes through the fail policy, which under the default fail-open
|
|
# means the mail is ingested — a signal-free message is never silently dropped.
|
|
EMPTY_MESSAGE="empty_message"
|
|
|
|
|
|
async def classify_email(email_data) -> tuple:
|
|
"""Judge one email from its subject and body. Never raises.
|
|
|
|
(verdict, "") on success; (None, error_code) when the model could not be consulted
|
|
or returned something unusable.
|
|
"""
|
|
subject,body=email_signals(email_data,TRIAGE_MAX_SUBJECT_CHARS,TRIAGE_MAX_BODY_CHARS)
|
|
if not subject and not body:
|
|
return None,EMPTY_MESSAGE
|
|
|
|
try:
|
|
# get_classifier() is inside the try on purpose: a missing OPENAI_API_KEY raises
|
|
# RuntimeError from llm_setup.get_client(), and a stale OPENAI_MODEL raises
|
|
# pydantic ValidationError from Settings. Both belong on the fail policy, not on
|
|
# a 500 for the whole fetch round.
|
|
classifier=get_classifier()
|
|
verdict=await classifier.classify(subject,body)
|
|
return verdict,""
|
|
except ATSError as e:
|
|
logger.warning("triage failed: code=%s",e.error_code)
|
|
return None,e.error_code
|
|
except Exception as e:
|
|
# classify_error never returns provider text. Log the exception TYPE and the code
|
|
# only — never the message, which can carry prompt or body content.
|
|
code,_=classify_error(e)
|
|
logger.warning("triage failed: code=%s exc=%s",code,type(e).__name__)
|
|
return None,code
|