| """Verifier Budgeter - Module 7. |
| |
| Do not call verifiers everywhere. |
| |
| Call verifiers when: |
| - task is high-risk |
| - confidence is low |
| - retrieval evidence is weak |
| - output is irreversible |
| - prior failures exist |
| - cheap model was used |
| - final answer is likely to be hallucination-prone |
| |
| Compare: |
| A. no verifier |
| B. verifier on every output |
| C. heuristic verifier |
| D. learned verifier budgeter |
| E. risk-weighted verifier |
| """ |
|
|
| from typing import Dict, List, Optional |
| from dataclasses import dataclass |
| from enum import Enum |
|
|
| from .trace_schema import TaskType, Outcome |
| from .config import ACOConfig, VerifierConfig |
|
|
|
|
| class VerifierDecision(Enum): |
| CALL_VERIFIER = "call_verifier" |
| SKIP = "skip" |
| USE_LIGHT_VERIFIER = "use_light_verifier" |
| SELF_CHECK = "self_check" |
|
|
|
|
| @dataclass |
| class VerifierBudgetDecision: |
| decision: VerifierDecision |
| verifier_model_id: Optional[str] |
| reasoning: str |
| estimated_verifier_cost: float |
| estimated_value: float |
| confidence: float |
| checks: List[str] |
|
|
|
|
| class VerifierBudgeter: |
| """Selectively calls verifiers based on risk and value.""" |
|
|
| |
| HIGH_RISK_TASKS = {TaskType.LEGAL_REGULATED} |
| HALLUCINATION_PRONE_TASKS = {TaskType.RESEARCH, TaskType.DOCUMENT_DRAFTING} |
| |
| |
| LOW_CONFIDENCE_THRESHOLD = 0.6 |
| MEDIUM_CONFIDENCE_THRESHOLD = 0.8 |
|
|
| def __init__(self, config: Optional[ACOConfig] = None): |
| self.config = config or ACOConfig() |
| self.verifier_stats: Dict[str, Dict] = {} |
| self.decision_history: List[Dict] = [] |
|
|
| def decide( |
| self, |
| task_type: TaskType, |
| model_tier_used: int, |
| confidence: float, |
| has_prior_failures: bool, |
| is_irreversible: bool, |
| output_length_tokens: int, |
| retrieval_evidence_count: int, |
| step_number: int, |
| total_steps: int, |
| mode: str = "risk_weighted", |
| ) -> VerifierBudgetDecision: |
| """Decide whether and how to verify an agent output.""" |
| |
| if mode == "no_verifier": |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.SKIP, |
| verifier_model_id=None, |
| reasoning="No verifier mode", |
| estimated_verifier_cost=0.0, |
| estimated_value=0.0, |
| confidence=1.0, |
| checks=[], |
| ) |
| |
| if mode == "always": |
| verifier = list(self.config.verifiers.values())[0] if self.config.verifiers else None |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.CALL_VERIFIER, |
| verifier_model_id=verifier.verifier_model_id if verifier else None, |
| reasoning="Verifier on every output", |
| estimated_verifier_cost=verifier.cost_per_call if verifier else 0.0, |
| estimated_value=0.5, |
| confidence=0.5, |
| checks=["all"], |
| ) |
| |
| if mode == "heuristic": |
| return self._heuristic_decide( |
| task_type, model_tier_used, confidence, has_prior_failures, |
| is_irreversible, output_length_tokens, retrieval_evidence_count, |
| step_number, total_steps, |
| ) |
| |
| if mode in ("learned", "risk_weighted"): |
| return self._risk_weighted_decide( |
| task_type, model_tier_used, confidence, has_prior_failures, |
| is_irreversible, output_length_tokens, retrieval_evidence_count, |
| step_number, total_steps, |
| ) |
| |
| return self._risk_weighted_decide( |
| task_type, model_tier_used, confidence, has_prior_failures, |
| is_irreversible, output_length_tokens, retrieval_evidence_count, |
| step_number, total_steps, |
| ) |
|
|
| def _heuristic_decide( |
| self, |
| task_type: TaskType, |
| model_tier_used: int, |
| confidence: float, |
| has_prior_failures: bool, |
| is_irreversible: bool, |
| output_length_tokens: int, |
| retrieval_evidence_count: int, |
| step_number: int, |
| total_steps: int, |
| ) -> VerifierBudgetDecision: |
| """Simple heuristic-based verifier selection.""" |
| |
| should_verify = False |
| checks = [] |
| |
| if task_type in self.HIGH_RISK_TASKS: |
| should_verify = True |
| checks.append("legal_compliance") |
| |
| if confidence < self.LOW_CONFIDENCE_THRESHOLD: |
| should_verify = True |
| checks.append("factual_accuracy") |
| |
| if has_prior_failures: |
| should_verify = True |
| checks.append("failure_pattern") |
| |
| if is_irreversible: |
| should_verify = True |
| checks.append("safety") |
| |
| if model_tier_used <= 2 and task_type in (TaskType.CODING, TaskType.RESEARCH): |
| should_verify = True |
| checks.append("output_quality") |
| |
| if retrieval_evidence_count == 0 and task_type in self.HALLUCINATION_PRONE_TASKS: |
| should_verify = True |
| checks.append("hallucination") |
| |
| if not should_verify: |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.SKIP, |
| verifier_model_id=None, |
| reasoning="No heuristic triggers met", |
| estimated_verifier_cost=0.0, |
| estimated_value=0.0, |
| confidence=confidence, |
| checks=[], |
| ) |
| |
| |
| verifier = self._select_verifier(checks) |
| |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.CALL_VERIFIER, |
| verifier_model_id=verifier.verifier_model_id if verifier else None, |
| reasoning=f"Heuristic triggered: {', '.join(checks)}", |
| estimated_verifier_cost=verifier.cost_per_call if verifier else 0.0, |
| estimated_value=0.3 + (1.0 - confidence) * 0.5, |
| confidence=confidence, |
| checks=checks, |
| ) |
|
|
| def _risk_weighted_decide( |
| self, |
| task_type: TaskType, |
| model_tier_used: int, |
| confidence: float, |
| has_prior_failures: bool, |
| is_irreversible: bool, |
| output_length_tokens: int, |
| retrieval_evidence_count: int, |
| step_number: int, |
| total_steps: int, |
| ) -> VerifierBudgetDecision: |
| """Risk-weighted verifier selection with value estimation.""" |
| |
| |
| risk = 0.0 |
| checks = [] |
| |
| |
| if task_type in self.HIGH_RISK_TASKS: |
| risk += 0.4 |
| checks.append("legal_compliance") |
| elif task_type in {TaskType.CODING, TaskType.RESEARCH}: |
| risk += 0.15 |
| |
| |
| risk += max(0.0, (self.MEDIUM_CONFIDENCE_THRESHOLD - confidence) * 0.5) |
| if confidence < self.LOW_CONFIDENCE_THRESHOLD: |
| checks.append("factual_accuracy") |
| |
| |
| risk += max(0.0, (3 - model_tier_used) * 0.05) |
| if model_tier_used <= 2 and task_type in (TaskType.CODING, TaskType.RESEARCH, TaskType.DOCUMENT_DRAFTING): |
| checks.append("output_quality") |
| |
| |
| if has_prior_failures: |
| risk += 0.2 |
| checks.append("failure_pattern") |
| |
| |
| if is_irreversible: |
| risk += 0.25 |
| checks.append("safety") |
| |
| |
| if retrieval_evidence_count == 0 and task_type in self.HALLUCINATION_PRONE_TASKS: |
| risk += 0.2 |
| checks.append("hallucination") |
| |
| |
| if output_length_tokens > 2048: |
| risk += min(0.1, (output_length_tokens - 2048) / 50000) |
| |
| |
| if step_number == 1: |
| risk += 0.05 |
| if step_number == total_steps and total_steps > 1: |
| risk += 0.1 |
| checks.append("final_output") |
| |
| risk = min(risk, 1.0) |
| |
| |
| verifier = self._select_verifier(checks) |
| verifier_cost = verifier.cost_per_call if verifier else 0.01 |
| |
| |
| |
| error_cost = self._estimate_error_cost(task_type, is_irreversible) |
| p_error = risk |
| p_catch = 0.7 |
| expected_value = p_error * p_catch * error_cost - verifier_cost |
| |
| if expected_value > 0 or risk > 0.6: |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.CALL_VERIFIER, |
| verifier_model_id=verifier.verifier_model_id if verifier else None, |
| reasoning=f"Risk={risk:.2f}, expected_value={expected_value:.4f}, checks={checks}", |
| estimated_verifier_cost=verifier_cost, |
| estimated_value=expected_value, |
| confidence=confidence, |
| checks=list(set(checks)), |
| ) |
| |
| |
| if risk > 0.3: |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.SELF_CHECK, |
| verifier_model_id=None, |
| reasoning=f"Medium risk ({risk:.2f}) — use lightweight self-check instead of full verifier", |
| estimated_verifier_cost=0.0, |
| estimated_value=p_error * 0.3 * error_cost, |
| confidence=confidence, |
| checks=["self_consistency"], |
| ) |
| |
| return VerifierBudgetDecision( |
| decision=VerifierDecision.SKIP, |
| verifier_model_id=None, |
| reasoning=f"Low risk ({risk:.2f}), expected_value={expected_value:.4f} — skip verification", |
| estimated_verifier_cost=0.0, |
| estimated_value=0.0, |
| confidence=confidence, |
| checks=[], |
| ) |
|
|
| def _select_verifier(self, checks: List[str]) -> Optional[VerifierConfig]: |
| """Select appropriate verifier based on checks needed.""" |
| if not self.config.verifiers: |
| return None |
| |
| |
| |
| return list(self.config.verifiers.values())[0] |
|
|
| def _estimate_error_cost(self, task_type: TaskType, is_irreversible: bool) -> float: |
| """Estimate the cost of an undetected error.""" |
| base = 1.0 |
| if task_type == TaskType.LEGAL_REGULATED: |
| base = 10.0 |
| elif task_type == TaskType.CODING: |
| base = 3.0 |
| elif task_type == TaskType.RESEARCH: |
| base = 2.0 |
| if is_irreversible: |
| base *= 2.0 |
| return base |
|
|
| def record_verifier_outcome( |
| self, |
| verifier_model_id: str, |
| target_step: str, |
| passed: bool, |
| cost: float, |
| was_actually_correct: bool, |
| ) -> None: |
| """Record verifier performance for calibration.""" |
| stats = self.verifier_stats.setdefault(verifier_model_id, { |
| "calls": 0, "passed": 0, "true_positives": 0, "false_positives": 0, |
| "false_negatives": 0, "total_cost": 0.0, |
| }) |
| stats["calls"] += 1 |
| if passed: |
| stats["passed"] += 1 |
| if passed and was_actually_correct: |
| stats["true_positives"] += 1 |
| if passed and not was_actually_correct: |
| stats["false_positives"] += 1 |
| if not passed and was_actually_correct: |
| stats["false_negatives"] += 1 |
| stats["total_cost"] += cost |
|
|