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

1""" 

2Rejection engine for constraint-based candidate filtering. 

3 

4This module provides logic for rejecting candidates based on constraint violations. 

5""" 

6 

7from dataclasses import dataclass 

8from typing import Dict, List, Optional 

9 

10from loguru import logger 

11 

12from ..candidates.base_candidate import Candidate 

13from ..constraints.base_constraint import Constraint 

14from .evidence_analyzer import ConstraintEvidence 

15 

16 

17@dataclass 

18class RejectionResult: 

19 """Result of a rejection check.""" 

20 

21 should_reject: bool 

22 reason: str 

23 constraint_value: str 

24 positive_confidence: float 

25 negative_confidence: float 

26 

27 

28class RejectionEngine: 

29 """ 

30 Engine for making rejection decisions based on constraint violations. 

31 

32 This engine uses simple, clear rules to determine when candidates 

33 should be rejected based on their constraint evaluation results. 

34 """ 

35 

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. 

43 

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 

50 

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. 

59 

60 Args: 

61 candidate: The candidate being evaluated 

62 constraint: The constraint being checked 

63 evidence_list: List of evidence for this constraint 

64 

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 ) 

77 

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 ) 

85 

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 ) 

95 

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 ) 

105 

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 ) 

114 

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. 

122 

123 Args: 

124 candidate: The candidate being evaluated 

125 constraint_results: Dictionary mapping constraints to their evidence 

126 

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 ) 

134 

135 if result.should_reject: 

136 logger.info( 

137 f"❌ REJECTION: {candidate.name} - {constraint.value} - {result.reason}" 

138 ) 

139 return result 

140 

141 # No rejections found 

142 logger.info(f"✓ ACCEPTED: {candidate.name} - All constraints satisfied") 

143 return None