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
Some checks failed
Deploy to Production / Build and Deploy (push) Has been cancelled
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user