Coverage for src/local_deep_research/advanced_search_system/constraints/constraint_analyzer.py: 100%
46 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-22 21:20 +0000
1"""
2Constraint analyzer for extracting constraints from queries.
3"""
5import re
6from typing import List
8from langchain_core.language_models import BaseChatModel
9from loguru import logger
11from ...utilities.json_utils import get_llm_response_text
12from .base_constraint import Constraint, ConstraintType
13from ...utilities.llm_utils import invoke_llm_sync
16class ConstraintAnalyzer:
17 """Analyzes queries to extract constraints."""
19 def __init__(self, model: BaseChatModel):
20 """Initialize the constraint analyzer."""
21 self.model = model
23 def extract_constraints(self, query: str) -> List[Constraint]:
24 """Extract constraints from a query."""
25 prompt = f"""
26Generate constraints to verify if an answer candidate correctly answers this question.
28Question: {query}
30Create constraints that would help verify if a proposed answer is correct. Focus on the RELATIONSHIP between the question and answer, not just query analysis.
32Examples:
33- "Which university did Alice study at?" → "Alice studied at this university"
34- "What year was the company founded?" → "The company was founded in this year"
35- "Who invented the device?" → "This person invented the device"
36- "Where is the building located?" → "The building is located at this place"
38For each constraint, identify:
391. Type: property, name_pattern, event, statistic, temporal, location, comparison, existence
402. Description: What relationship must hold between question and answer
413. Value: The specific relationship to verify
424. Weight: How critical this constraint is (0.0-1.0)
44Format your response as:
45CONSTRAINT_1:
46Type: [type]
47Description: [description]
48Value: [value]
49Weight: [0.0-1.0]
51CONSTRAINT_2:
52Type: [type]
53Description: [description]
54Value: [value]
55Weight: [0.0-1.0]
57Focus on answer verification, not query parsing.
58"""
60 content = get_llm_response_text(invoke_llm_sync(self.model, prompt))
62 constraints = []
63 current_constraint = {}
64 constraint_id = 1
66 for line in content.strip().split("\n"):
67 line = line.strip()
69 if line.startswith("CONSTRAINT_"):
70 if current_constraint and all(
71 k in current_constraint
72 for k in ["type", "description", "value"]
73 ):
74 constraint = Constraint(
75 id=f"c{constraint_id}",
76 type=self._parse_constraint_type(
77 current_constraint["type"]
78 ),
79 description=current_constraint["description"],
80 value=current_constraint["value"],
81 weight=self._parse_weight(
82 current_constraint.get("weight", 1.0)
83 ),
84 )
85 constraints.append(constraint)
86 constraint_id += 1
87 current_constraint = {}
88 elif ":" in line:
89 key, value = line.split(":", 1)
90 key = key.strip().lower()
91 value = value.strip()
92 if key in ["type", "description", "value", "weight"]:
93 current_constraint[key] = value
95 # Don't forget the last constraint
96 if current_constraint and all(
97 k in current_constraint for k in ["type", "description", "value"]
98 ):
99 constraint = Constraint(
100 id=f"c{constraint_id}",
101 type=self._parse_constraint_type(current_constraint["type"]),
102 description=current_constraint["description"],
103 value=current_constraint["value"],
104 weight=self._parse_weight(
105 current_constraint.get("weight", 1.0)
106 ),
107 )
108 constraints.append(constraint)
110 logger.info(f"Extracted {len(constraints)} constraints from query")
111 return constraints
113 def _parse_constraint_type(self, type_str: str) -> ConstraintType:
114 """Parse constraint type from string."""
115 type_map = {
116 "property": ConstraintType.PROPERTY,
117 "name_pattern": ConstraintType.NAME_PATTERN,
118 "event": ConstraintType.EVENT,
119 "statistic": ConstraintType.STATISTIC,
120 "temporal": ConstraintType.TEMPORAL,
121 "location": ConstraintType.LOCATION,
122 "comparison": ConstraintType.COMPARISON,
123 "existence": ConstraintType.EXISTENCE,
124 }
125 return type_map.get(type_str.lower(), ConstraintType.PROPERTY)
127 def _parse_weight(self, weight_value) -> float:
128 """Parse weight value to float, handling text annotations.
130 Args:
131 weight_value: String or numeric weight value, possibly with text annotations
133 Returns:
134 float: Parsed weight value
135 """
136 if isinstance(weight_value, (int, float)):
137 return float(weight_value)
138 if isinstance(weight_value, str):
139 # Extract the first number from the string
140 match = re.search(r"(\d+(\.\d+)?)", weight_value)
141 if match:
142 return float(match.group(1))
143 return 1.0 # Default weight