from __future__ import annotations import math from .domain import ( CandidateScorer, CandidateScoringError, RankedCandidate, RerankConfig, RerankRequest, RerankResult, ) class Reranker: def __init__(self, scorer: CandidateScorer, config: RerankConfig | None = None) -> None: self._scorer = scorer self._config = config or RerankConfig() def rerank(self, request: RerankRequest) -> RerankResult: if len(request.candidates) < 2: return self._preserve(request, reason="insufficient_candidates") if len(request.candidates) > self._config.max_candidates: return self._preserve(request, reason="unsafe_input") if any( len(candidate.surface) > self._config.max_candidate_chars for candidate in request.candidates ): return self._preserve(request, reason="unsafe_input") surfaces = [candidate.surface for candidate in request.candidates] if ( any(not surface for surface in surfaces) or len(set(surfaces)) != len(surfaces) or any(not math.isfinite(candidate.prior_score) for candidate in request.candidates) ): return self._preserve(request, reason="unsafe_input") if ( len(request.left_context) + len(request.right_context) > self._config.max_context_words ): return self._preserve(request, reason="unsafe_input") if len(request.reading) > self._config.max_reading_chars: return self._preserve(request, reason="unsafe_input") if ( sum(len(word) for word in request.left_context + request.right_context) > self._config.max_context_chars ): return self._preserve(request, reason="unsafe_input") try: model_scores = tuple( float(score) for score in self._scorer.score_candidates(request) ) except CandidateScoringError as error: return self._preserve(request, reason=error.code) except Exception: return self._preserve(request, reason="scorer_error") if len(model_scores) != len(request.candidates) or not all( math.isfinite(score) for score in model_scores ): return self._preserve(request, reason="invalid_scores") combined_scores = tuple( model_score + self._config.prior_weight * candidate.prior_score for candidate, model_score in zip(request.candidates, model_scores, strict=True) ) model_order = sorted( range(len(request.candidates)), key=lambda index: (-combined_scores[index], index), ) margin = combined_scores[model_order[0]] - combined_scores[model_order[1]] changed = model_order[0] != 0 and margin >= self._config.min_margin order = model_order if changed else list(range(len(request.candidates))) if changed: reason = "accepted" elif model_order[0] == 0: reason = "baseline_best" else: reason = "low_margin" ranked = tuple( RankedCandidate( surface=request.candidates[index].surface, original_rank=index, prior_score=request.candidates[index].prior_score, model_score=model_scores[index], combined_score=combined_scores[index], ) for index in order ) return RerankResult( reading=request.reading, ranked=ranked, changed=changed, reason=reason, margin=margin, ) @staticmethod def _preserve(request: RerankRequest, *, reason: str) -> RerankResult: return RerankResult( reading=request.reading, ranked=tuple( RankedCandidate( surface=candidate.surface, original_rank=index, prior_score=candidate.prior_score, model_score=None, combined_score=None, ) for index, candidate in enumerate(request.candidates) ), changed=False, reason=reason, margin=None, )