"""Summary-gate entrypoint — one Responses call per (summary, job) pair. Pure module: no FastAPI imports and no HTTPException. Never raises: a provider outage is a policy decision (SUMMARY_GATE_FAIL_OPEN). """ from __future__ import annotations import logging from summary_gate.agent_setup import get_gate from summary_gate.plugins import ( GATE_ENABLED, GATE_MAX_JD_CHARS, GATE_MAX_SUMMARY_CHARS, clip_text, should_score, ) from app.core.errors import ATSError, classify_error logger=logging.getLogger("summary.gate") EMPTY_INPUT="empty_input" _allow_cache: dict[tuple[str,str],bool]={} async def classify_summary(summary, job_description) -> tuple: """Judge one summary against one JD. Never raises. (verdict, "") on success; (None, error_code) when the model could not be consulted or returned something unusable. """ text=clip_text(summary,GATE_MAX_SUMMARY_CHARS).strip() jd=clip_text(job_description,GATE_MAX_JD_CHARS).strip() if not text or not jd: return None,EMPTY_INPUT try: gate=get_gate() verdict=await gate.classify(jd,text) logger.info( "summary gate: suitable=%s confidence=%s", verdict.is_suitable, verdict.confidence, ) return verdict,"" except ATSError as e: logger.warning("summary gate failed: code=%s",e.error_code) return None,e.error_code except Exception as e: code,_=classify_error(e) logger.warning("summary gate failed: code=%s exc=%s",code,type(e).__name__) return None,code async def allow_ats(summary, job_description) -> bool: """True when full ATS should run. No summary, or the gate disabled: pass through so the first score can write professional_summary. A present summary is the gradient: only a suitable verdict (above SUMMARY_GATE_MIN_CONFIDENCE) continues. """ if not GATE_ENABLED: return True text=(summary or "").strip() if not text: return True jd=(job_description or "").strip() cache_key=(text,jd) cached=_allow_cache.get(cache_key) if cached is not None: return cached verdict,error=await classify_summary(text,jd) proceed,_,_=should_score(verdict,error) _allow_cache[cache_key]=proceed return proceed