Coverage for src / local_deep_research / advanced_search_system / constraint_checking / rejection_engine.py: 80%
35 statements
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
« prev ^ index » next coverage.py v7.12.0, created at 2026-01-11 00:51 +0000
1"""
2Rejection engine for constraint-based candidate filtering.
4This module provides logic for rejecting candidates based on constraint violations.
5"""
7from dataclasses import dataclass
8from typing import Dict, List, Optional
10from loguru import logger
12from ..candidates.base_candidate import Candidate
13from ..constraints.base_constraint import Constraint
14from .evidence_analyzer import ConstraintEvidence
17@dataclass
18class RejectionResult:
19 """Result of a rejection check."""
21 should_reject: bool
22 reason: str
23 constraint_value: str
24 positive_confidence: float
25 negative_confidence: float
28class RejectionEngine:
29 """
30 Engine for making rejection decisions based on constraint violations.
32 This engine uses simple, clear rules to determine when candidates
33 should be rejected based on their constraint evaluation results.
34 """
36 def __init__(
37 self,
38 negative_threshold: float = 0.25, # Reject if negative evidence > 25%
39 positive_threshold: float = 0.4, # Reject if positive evidence < 40%
40 ):
41 """
42 Initialize the rejection engine.
44 Args:
45 negative_threshold: Threshold for negative evidence rejection
46 positive_threshold: Minimum positive evidence required
47 """
48 self.negative_threshold = negative_threshold
49 self.positive_threshold = positive_threshold
51 def should_reject_candidate(
52 self,
53 candidate: Candidate,
54 constraint: Constraint,
55 evidence_list: List[ConstraintEvidence],
56 ) -> RejectionResult:
57 """
58 Determine if a candidate should be rejected based on constraint evidence.
60 Args:
61 candidate: The candidate being evaluated
62 constraint: The constraint being checked
63 evidence_list: List of evidence for this constraint
65 Returns:
66 RejectionResult: Whether to reject and why
67 """
68 if not evidence_list:
69 # No evidence - don't reject but note the lack of evidence
70 return RejectionResult(
71 should_reject=False,
72 reason="No evidence available",
73 constraint_value=constraint.value,
74 positive_confidence=0.0,
75 negative_confidence=0.0,
76 )
78 # Calculate average confidence scores
79 avg_positive = sum(e.positive_confidence for e in evidence_list) / len(
80 evidence_list
81 )
82 avg_negative = sum(e.negative_confidence for e in evidence_list) / len(
83 evidence_list
84 )
86 # PRIMARY REJECTION RULE: High negative evidence
87 if avg_negative > self.negative_threshold:
88 return RejectionResult(
89 should_reject=True,
90 reason=f"High negative evidence ({avg_negative:.0%})",
91 constraint_value=constraint.value,
92 positive_confidence=avg_positive,
93 negative_confidence=avg_negative,
94 )
96 # SECONDARY REJECTION RULE: Low positive evidence
97 if avg_positive < self.positive_threshold: 97 ↛ 107line 97 didn't jump to line 107 because the condition on line 97 was always true
98 return RejectionResult(
99 should_reject=True,
100 reason=f"Insufficient positive evidence ({avg_positive:.0%})",
101 constraint_value=constraint.value,
102 positive_confidence=avg_positive,
103 negative_confidence=avg_negative,
104 )
106 # No rejection needed
107 return RejectionResult(
108 should_reject=False,
109 reason="Constraints satisfied",
110 constraint_value=constraint.value,
111 positive_confidence=avg_positive,
112 negative_confidence=avg_negative,
113 )
115 def check_all_constraints(
116 self,
117 candidate: Candidate,
118 constraint_results: Dict[Constraint, List[ConstraintEvidence]],
119 ) -> Optional[RejectionResult]:
120 """
121 Check all constraints for a candidate and return first rejection reason.
123 Args:
124 candidate: The candidate being evaluated
125 constraint_results: Dictionary mapping constraints to their evidence
127 Returns:
128 RejectionResult if should reject, None if should accept
129 """
130 for constraint, evidence_list in constraint_results.items(): 130 ↛ 131line 130 didn't jump to line 131 because the loop on line 130 never started
131 result = self.should_reject_candidate(
132 candidate, constraint, evidence_list
133 )
135 if result.should_reject:
136 logger.info(
137 f"❌ REJECTION: {candidate.name} - {constraint.value} - {result.reason}"
138 )
139 return result
141 # No rejections found
142 logger.info(f"✓ ACCEPTED: {candidate.name} - All constraints satisfied")
143 return None