"""Deterministic attention and action policy for evaluator v2.""" from __future__ import annotations import hashlib import json from .findings import FindingDerivation from .schemas import ( ActionExecution, ActionType, CallDecision, DecisionPolicyTrace, DecisionStatus, Finding, FindingCategory, FindingQualification, FindingSeverity, PresentationSelection, QualificationReason, RecommendedAction, RecoveryEffect, ReliabilityStatus, SignalBundle, SourceProvenance, Visibility, ) DECISION_POLICY_ID = "evaluator-v2-attention" DECISION_POLICY_VERSION = "0.1.0" ATTENTION_AGGREGATION = "any_qualifying_finding" RECOVERY_FINDING_TYPE = "escalation.recovery_observed" FOLLOW_UP_FINDING_TYPES = { "process.repeat_contact_unresolved", } _PRECEDENCE_ORDER = ( "critical.escalation", "critical.required_control", "critical.outcome", "critical.process", "critical.agent_behavior", "critical.data_quality", "review.outcome", "review.required_control", "review.escalation", "review.process", "review.agent_behavior", "review.data_quality", ) _PRECEDENCE_INDEX = { group: index for index, group in enumerate(_PRECEDENCE_ORDER) } def _source() -> SourceProvenance: return SourceProvenance( producer="evaluator_v2.decisions", producer_version=DECISION_POLICY_VERSION, method=( "any_qualifying_finding_with_named_precedence_and_" "constrained_actions" ), ) def signal_bundle_sha256(bundle: SignalBundle) -> str: payload = json.dumps( bundle.model_dump(mode="json"), sort_keys=True, separators=(",", ":"), ).encode("utf-8") return hashlib.sha256(payload).hexdigest() def _precedence_group(finding: Finding) -> str: return f"{finding.severity.value}.{finding.category.value}" def _qualification(finding: Finding) -> FindingQualification: if finding.visibility == Visibility.INTERNAL: return FindingQualification( finding_id=finding.finding_id, qualifies_for_attention=False, reason=QualificationReason.EXCLUDED_INTERNAL, ) if finding.reliability.status in ( ReliabilityStatus.UNUSABLE, ReliabilityStatus.UNAVAILABLE, ): return FindingQualification( finding_id=finding.finding_id, qualifies_for_attention=False, reason=QualificationReason.EXCLUDED_UNRELIABLE, ) if finding.severity == FindingSeverity.INFO: return FindingQualification( finding_id=finding.finding_id, qualifies_for_attention=False, reason=QualificationReason.EXCLUDED_INFORMATIONAL, ) if finding.reliability.status == ReliabilityStatus.LIMITED: reason = QualificationReason.QUALIFIES_LIMITED_REVIEW elif finding.severity == FindingSeverity.CRITICAL: reason = QualificationReason.QUALIFIES_CRITICAL else: reason = QualificationReason.QUALIFIES_REVIEW group = _precedence_group(finding) if group not in _PRECEDENCE_INDEX: raise ValueError( f"attention policy has no precedence for {group!r}" ) return FindingQualification( finding_id=finding.finding_id, qualifies_for_attention=True, reason=reason, precedence_group=group, ) def _controlling_ids( qualifications: list[FindingQualification], ) -> list[str]: qualifying = [ item for item in qualifications if item.qualifies_for_attention and item.precedence_group is not None ] if not qualifying: return [] controlling_group = min( (item.precedence_group for item in qualifying), key=lambda group: _PRECEDENCE_INDEX[group], ) return sorted( item.finding_id for item in qualifying if item.precedence_group == controlling_group ) def _controlling_findings( derivation: FindingDerivation, controlling_ids: list[str], ) -> list[Finding]: by_id = { finding.finding_id: finding for finding in derivation.triggered_findings } return [by_id[finding_id] for finding_id in controlling_ids] def _reason(findings: list[Finding]) -> str: titles = "; ".join(finding.title for finding in findings) return f"Controlling findings: {titles}." def _automatic_manager_review( findings: list[Finding], ) -> RecommendedAction: return RecommendedAction( action_type=ActionType.MANAGER_REVIEW, execution=ActionExecution.AUTOMATIC, label="Email manager review summary", reason=_reason(findings), finding_ids=[ finding.finding_id for finding in findings ], automation_allowed=True, requires_human_approval=False, ) def _approval_action( findings: list[Finding], action_type: ActionType, label: str, ) -> RecommendedAction: return RecommendedAction( action_type=action_type, execution=ActionExecution.REQUIRES_APPROVAL, label=label, reason=_reason(findings), finding_ids=[ finding.finding_id for finding in findings ], automation_allowed=False, requires_human_approval=True, ) def _recommended_action( controlling: list[Finding], decision_status: DecisionStatus, ) -> RecommendedAction: if not controlling: reason = ( "No action because applicable requirements remain " "unassessed; this call has not been cleared." if decision_status == DecisionStatus.INSUFFICIENT_EVIDENCE else "No negative finding qualified for attention." ) return RecommendedAction( action_type=ActionType.NONE, execution=ActionExecution.NO_ACTION, label="No automated action", reason=reason, ) if any( finding.severity == FindingSeverity.CRITICAL for finding in controlling ): return _automatic_manager_review(controlling) categories = {finding.category for finding in controlling} if ( FindingCategory.OUTCOME in categories or any( finding.finding_type in FOLLOW_UP_FINDING_TYPES for finding in controlling ) ): return _approval_action( controlling, ActionType.CUSTOMER_FOLLOW_UP, "Prepare customer follow-up email", ) if categories.intersection( { FindingCategory.PROCESS, FindingCategory.AGENT_BEHAVIOR, } ): return _approval_action( controlling, ActionType.AGENT_COACHING, "Email coaching recommendation to manager", ) return _automatic_manager_review(controlling) def _decision_trace( derivation: FindingDerivation, ) -> DecisionPolicyTrace: qualifications = [ _qualification(finding) for finding in derivation.triggered_findings ] recovery_ids = sorted( finding.finding_id for finding in derivation.positive_findings if finding.finding_type == RECOVERY_FINDING_TYPE ) return DecisionPolicyTrace( policy_id=DECISION_POLICY_ID, policy_version=DECISION_POLICY_VERSION, aggregation=ATTENTION_AGGREGATION, qualifications=qualifications, controlling_finding_ids=_controlling_ids(qualifications), recovery_finding_ids=recovery_ids, recovery_effect=( RecoveryEffect.CONTEXT_ONLY if recovery_ids else RecoveryEffect.NONE ), ) def _decision_status( derivation: FindingDerivation, ) -> DecisionStatus: requirement_uncertainty = any( uncertainty.code.startswith("requirement.") for uncertainty in derivation.uncertainties ) if not requirement_uncertainty: return DecisionStatus.COMPLETE if ( derivation.triggered_findings or derivation.positive_findings ): return DecisionStatus.PARTIAL return DecisionStatus.INSUFFICIENT_EVIDENCE def build_call_decision( bundle: SignalBundle, derivation: FindingDerivation, ) -> CallDecision: """Build a decision without averaging, scoring, or LLM arbitration.""" if bundle.call_id != derivation.call_id: raise ValueError( "finding derivation and signal bundle call_id values must match" ) trace = _decision_trace(derivation) controlling = _controlling_findings( derivation, trace.controlling_finding_ids, ) decision_status = _decision_status(derivation) return CallDecision( call_id=bundle.call_id, evaluator_version=( f"v2-policy-{DECISION_POLICY_VERSION}" ), domain_profile_id=derivation.profile_id, signal_bundle_sha256=signal_bundle_sha256(bundle), decision_status=decision_status, attention_required=bool(trace.controlling_finding_ids), triggered_findings=derivation.triggered_findings, positive_findings=derivation.positive_findings, recommended_action=_recommended_action( controlling, decision_status, ), uncertainties=derivation.uncertainties, decision_trace=trace, presentation=PresentationSelection(), provenance=_source(), )