feat(quality): A4 — L2 Pro premium judge (8 dims, gpt-4o, Pro-gated, opt-in)
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled

This commit is contained in:
2026-07-14 16:56:04 +02:00
parent 8d0fc818ef
commit c794eff823
6 changed files with 1143 additions and 0 deletions

View File

@@ -23,6 +23,7 @@ from core.logging import get_logger
from .script_detector import evaluate_document, DocumentQualityResult
from .sampler import sample_chunks_for_l1
from .llm_judge import L1Result, LLMJudge
from .l2_judge import L2Result, L2ProJudge
logger = get_logger(__name__)
@@ -271,3 +272,155 @@ def make_judge_from_env_safe() -> Optional[LLMJudge]:
except Exception as e:
logger.warning("l1_judge_init_failed", error=str(e)[:200])
return None
# ---------- L2 (Pro tier) ----------
async def run_l2_check(
source_chunks: List[str],
translated_chunks: List[str],
target_lang: Optional[str],
l0_failed_indices: Optional[Set[int]] = None,
job_id: Optional[str] = None,
file_extension: Optional[str] = None,
max_samples: int = 15,
min_chunks: int = 20,
judge: Optional[L2ProJudge] = None,
log_only: bool = True,
) -> L2Result:
"""
Run the L2 Pro premium judge (8 dimensions, gpt-4o default).
Args:
source_chunks: Original texts.
translated_chunks: Translated texts.
target_lang: Target language code (e.g. "fr", "en").
l0_failed_indices: Indices that L0 flagged as bad — skipped.
job_id: For logging.
file_extension: For logging.
max_samples: How many chunks to send to the LLM.
min_chunks: Skip the check if document has fewer chunks.
judge: An L2ProJudge instance. If None, created from env vars.
log_only: If True, never propagate the verdict (observation mode).
If False, the caller can decide what to do with the verdict.
Returns an L2Result. verdict="skip" on any internal error.
Never raises — defensive wrapper.
"""
skip = L2Result(verdict="skip", error="not_run")
if l0_failed_indices is None:
l0_failed_indices = set()
# Sample (reuse the L1 sampler — it's just chunk selection, model-agnostic)
sample = sample_chunks_for_l1(
source_chunks, translated_chunks, l0_failed_indices,
max_samples=max_samples, min_chunks=min_chunks,
)
if not sample:
logger.info(
"quality_l2_check_skipped",
job_id=job_id,
reason="insufficient_chunks_or_all_flagged",
chunk_count=len(source_chunks),
)
_record_l2_metric(verdict="skip", model="none")
return skip
# Get the judge
if judge is None:
judge = make_l2_judge_from_env_safe()
if judge is None:
logger.info(
"quality_l2_check_skipped",
job_id=job_id,
reason="no_l2_judge_configured",
)
_record_l2_metric(verdict="skip", model="none")
return skip
# Get the language name for the prompt
target_lang_name = _LANG_NAMES.get((target_lang or "").lower(), target_lang or "auto")
# Call the LLM
try:
result = await judge.judge_batch(sample, target_lang or "auto", target_lang_name)
except Exception as e:
logger.warning(
"quality_l2_check_failed",
job_id=job_id,
error=str(e)[:200],
error_type=type(e).__name__,
)
_record_l2_metric(verdict="error", model="unknown")
return L2Result(verdict="skip", error=str(e)[:200])
# Log (always) — caller decides what to do
logger.info(
"quality_l2_check",
job_id=job_id,
file_extension=file_extension,
target_lang=target_lang,
verdict=result.verdict,
chunks_evaluated=result.chunks_evaluated,
chunks_passed=result.chunks_passed,
chunks_failed=result.chunks_failed,
failure_rate=result.failure_rate,
average_score=result.average_score,
dimension_pass_rates=result.dimension_pass_rates,
model=result.model_used,
elapsed_ms=result.elapsed_ms,
cost_estimate_usd=result.cost_estimate_usd,
log_only=log_only,
)
# Record Prometheus metric
duration_s = None
if result.elapsed_ms is not None:
try:
duration_s = float(result.elapsed_ms) / 1000.0
except Exception:
duration_s = None
_record_l2_metric(
verdict=result.verdict or "skip",
model=result.model_used or "unknown",
duration_seconds=duration_s,
cost_usd=result.cost_estimate_usd,
)
return result
def _record_l2_metric(
verdict: str,
model: str = "unknown",
duration_seconds: float = None,
cost_usd: float = None,
) -> None:
"""Best-effort Prometheus metric emission for L2.
Never raises.
"""
try:
from middleware.metrics import record_l2_verdict
record_l2_verdict(
verdict=verdict,
model=model,
duration_seconds=duration_seconds,
cost_usd=cost_usd,
)
except Exception:
pass
def make_l2_judge_from_env_safe() -> Optional[L2ProJudge]:
"""Read env vars and build an L2 judge, or return None if not configured.
Defensive wrapper — a misconfigured L2 environment NEVER breaks a job.
"""
try:
from .l2_judge import make_l2_judge_from_env
return make_l2_judge_from_env()
except Exception as e:
logger.warning("l2_judge_init_failed", error=str(e)[:200])
return None