Coverage for src / local_deep_research / advanced_search_system / constraint_checking / strict_checker.py: 94%
80 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-14 23:55 +0000
1"""
2Strict constraint checker - example of creating a custom variant.
4This implementation is very strict about constraint satisfaction,
5requiring high confidence for all constraints.
6"""
8from typing import Dict, List, Tuple
10from loguru import logger
12from ..candidates.base_candidate import Candidate
13from ..constraints.base_constraint import Constraint, ConstraintType
14from .base_constraint_checker import (
15 BaseConstraintChecker,
16 ConstraintCheckResult,
17)
20class StrictChecker(BaseConstraintChecker):
21 """
22 Strict constraint checker that requires high confidence for all constraints.
24 This is an example of how to create custom constraint checking variants
25 by inheriting from BaseConstraintChecker.
26 """
28 def __init__(
29 self,
30 *args,
31 strict_threshold: float = 0.9, # Very high threshold
32 name_pattern_required: bool = True, # NAME_PATTERN constraints are mandatory
33 **kwargs,
34 ):
35 """
36 Initialize strict checker.
38 Args:
39 strict_threshold: Very high threshold for all constraints
40 name_pattern_required: Whether NAME_PATTERN constraints are mandatory
41 """
42 super().__init__(*args, **kwargs)
43 self.strict_threshold = strict_threshold
44 self.name_pattern_required = name_pattern_required
46 def check_candidate(
47 self, candidate: Candidate, constraints: List[Constraint]
48 ) -> ConstraintCheckResult:
49 """Check candidate with strict requirements."""
50 logger.info(f"Checking candidate: {candidate.name} (strict mode)")
52 constraint_scores = {}
53 detailed_results = []
54 rejection_reason = None
55 should_reject = False
57 for constraint in constraints:
58 # Gather evidence
59 evidence_list = self._gather_evidence_for_constraint(
60 candidate, constraint
61 )
63 # Calculate score
64 score = self._evaluate_constraint_strictly(
65 candidate, constraint, evidence_list
66 )
68 # Check for rejection
69 reject, reason = self.should_reject_candidate(
70 candidate, constraint, evidence_list
71 )
73 if reject and not should_reject:
74 should_reject = True
75 rejection_reason = reason
77 # Store results
78 constraint_scores[constraint.value] = {
79 "total": score,
80 "strict_pass": score >= self.strict_threshold,
81 "weight": constraint.weight,
82 }
84 detailed_results.append(
85 {
86 "constraint": constraint.value,
87 "score": score,
88 "strict_pass": score >= self.strict_threshold,
89 "weight": constraint.weight,
90 "type": constraint.type.value,
91 }
92 )
94 self._log_constraint_result(candidate, constraint, score, {})
96 # Calculate total score
97 if should_reject:
98 total_score = 0.0
99 else:
100 # All constraints must pass strict threshold
101 all_pass = all(r["strict_pass"] for r in detailed_results)
102 total_score = 1.0 if all_pass else 0.0
104 logger.info(
105 f"Strict evaluation for {candidate.name}: {'PASS' if total_score > 0 else 'FAIL'}"
106 )
108 return ConstraintCheckResult(
109 candidate=candidate,
110 total_score=total_score,
111 constraint_scores=constraint_scores,
112 should_reject=should_reject,
113 rejection_reason=rejection_reason,
114 detailed_results=detailed_results,
115 )
117 def should_reject_candidate(
118 self,
119 candidate: Candidate,
120 constraint: Constraint,
121 evidence_data: List[Dict],
122 ) -> Tuple[bool, str]:
123 """Strict rejection rules."""
124 if not evidence_data:
125 return True, f"No evidence for constraint '{constraint.value}'"
127 score = self._evaluate_constraint_strictly(
128 candidate, constraint, evidence_data
129 )
131 # Special handling for NAME_PATTERN constraints
132 if (
133 constraint.type == ConstraintType.NAME_PATTERN
134 and self.name_pattern_required
135 ):
136 if score < 0.95: # Even stricter for name patterns
137 return (
138 True,
139 f"NAME_PATTERN constraint '{constraint.value}' failed strict evaluation",
140 )
142 # General strict threshold
143 if score < self.strict_threshold:
144 return (
145 True,
146 f"Constraint '{constraint.value}' below strict threshold ({score:.0%})",
147 )
149 return False, ""
151 def _evaluate_constraint_strictly(
152 self,
153 candidate: Candidate,
154 constraint: Constraint,
155 evidence_list: List[Dict],
156 ) -> float:
157 """Evaluate constraint with strict criteria."""
158 if not evidence_list:
159 return 0.0
161 # For NAME_PATTERN constraints, use direct name checking
162 if constraint.type == ConstraintType.NAME_PATTERN:
163 return self._check_name_pattern_strictly(
164 candidate.name, constraint.value
165 )
167 # For other constraints, use LLM with strict prompt
168 combined_evidence = "\n".join(
169 [e.get("text", "")[:300] for e in evidence_list[:2]]
170 )
172 prompt = f"""
173STRICT EVALUATION: Does "{candidate.name}" definitely and clearly satisfy: "{constraint.value}"?
175Evidence:
176{combined_evidence}
178Be very strict. Only return a high score if there is clear, unambiguous evidence.
180Score (0.0-1.0):
181"""
183 try:
184 response = self.model.invoke(prompt).content.strip()
185 import re
187 match = re.search(r"(\d*\.?\d+)", response)
188 if match: 188 ↛ 194line 188 didn't jump to line 194 because the condition on line 188 was always true
189 return max(0.0, min(float(match.group(1)), 1.0))
190 except Exception:
191 logger.exception("Error in strict evaluation")
192 return 0.5 # Uncertain on error, don't auto-reject
194 return 0.0
196 def _check_name_pattern_strictly(
197 self, candidate_name: str, pattern_description: str
198 ) -> float:
199 """Strict name pattern checking."""
200 # Example: Check for body parts in name
201 if "body part" in pattern_description.lower():
202 body_parts = [
203 "arm",
204 "leg",
205 "foot",
206 "feet",
207 "hand",
208 "eye",
209 "ear",
210 "nose",
211 "mouth",
212 "tooth",
213 "teeth",
214 "head",
215 "face",
216 "neck",
217 "back",
218 "chest",
219 "heart",
220 "finger",
221 "thumb",
222 "toe",
223 "knee",
224 "elbow",
225 "shoulder",
226 "spine",
227 "bone",
228 ]
230 name_lower = candidate_name.lower()
231 for part in body_parts:
232 if part in name_lower.split() or part in name_lower:
233 logger.info(
234 f"✓ Found body part '{part}' in '{candidate_name}'"
235 )
236 return 1.0
238 logger.info(f"✗ No body part found in '{candidate_name}'")
239 return 0.0
241 # For other name patterns, use LLM
242 prompt = f"""
243Does the name "{candidate_name}" match this pattern: "{pattern_description}"?
245Be very strict. Return 1.0 only if it clearly matches, 0.0 otherwise.
247Score:
248"""
250 try:
251 response = self.model.invoke(prompt).content.strip()
252 import re
254 match = re.search(r"(\d*\.?\d+)", response)
255 if match: 255 ↛ 264line 255 didn't jump to line 264 because the condition on line 255 was always true
256 return max(0.0, min(float(match.group(1)), 1.0))
257 except Exception:
258 logger.warning(
259 f"LLM constraint scoring failed for '{candidate_name}', "
260 "returning uncertain score",
261 )
262 return 0.5
264 return 0.0