Coverage for presidio_analyzer / llm_utils / langextract_helper.py: 89%
65 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-29 09:03 +0000
1"""LangExtract helper utilities."""
2import logging
3from typing import Dict, List, Optional
5from presidio_analyzer import AnalysisExplanation, RecognizerResult
7logger = logging.getLogger("presidio-analyzer")
9try:
10 import langextract as lx
11 import langextract.factory as lx_factory
13 # Make sure builtin providers are pre-loaded to work around a bug in LangExtract
14 # that fails to load them automatically if the provider name is specified for a
15 # built-in provider rather than inferred from the model_id.
16 lx.providers.load_builtins_once()
17 lx.providers.load_plugins_once()
18except ImportError:
19 lx = None
20 lx_factory = None
22__all__ = [
23 "lx",
24 "lx_factory",
25 "check_langextract_available",
26 "extract_lm_config",
27 "get_supported_entities",
28 "create_reverse_entity_mapping",
29 "calculate_extraction_confidence",
30 "convert_langextract_to_presidio_results",
31]
34def check_langextract_available():
35 """Check if langextract is available and raise error if not."""
36 if not lx:
37 raise ImportError(
38 "LangExtract is not installed. "
39 "Install it with: poetry install --extras langextract"
40 )
43# Default alignment score mappings for LangExtract extractions
44DEFAULT_ALIGNMENT_SCORES = {
45 "MATCH_EXACT": 0.95,
46 "MATCH_FUZZY": 0.80,
47 "MATCH_LESSER": 0.70,
48 "NOT_ALIGNED": 0.60,
49}
52def extract_lm_config(config: Dict) -> Dict:
53 """Extract LM recognizer configuration section with default values.
55 :param config: Full configuration dictionary.
56 :return: LM recognizer config with keys: supported_entities, min_score,
57 labels_to_ignore, enable_generic_consolidation.
58 """
59 lm_config_section = config.get("lm_recognizer", {})
61 return {
62 "supported_entities": lm_config_section.get("supported_entities"),
63 "min_score": lm_config_section.get("min_score", 0.5),
64 "labels_to_ignore": lm_config_section.get("labels_to_ignore", []),
65 "enable_generic_consolidation": lm_config_section.get(
66 "enable_generic_consolidation", True
67 ),
68 }
71def get_supported_entities(
72 lm_config: Dict,
73 langextract_config: Dict
74) -> Optional[List[str]]:
75 """Get supported entities list, checking LM config first then LangExtract config.
77 :param lm_config: LM recognizer configuration dictionary.
78 :param langextract_config: LangExtract configuration dictionary.
79 :return: List of supported entity types, or None if not specified.
80 """
81 return (
82 lm_config.get("supported_entities")
83 or langextract_config.get("supported_entities")
84 )
87def create_reverse_entity_mapping(entity_mappings: Dict) -> Dict:
88 """Create reverse mapping from values to keys.
90 :param entity_mappings: Original entity mapping dictionary.
91 :return: Reversed dictionary mapping values to keys.
92 """
93 return {v: k for k, v in entity_mappings.items()}
96def calculate_extraction_confidence(
97 extraction,
98 alignment_scores: Optional[Dict[str, float]] = None
99) -> float:
100 """Calculate confidence score based on extraction alignment status.
102 :param extraction: LangExtract extraction object with optional alignment_status.
103 :param alignment_scores: Custom score mapping for alignment statuses (optional).
104 :return: Confidence score between 0.0 and 1.0.
105 """
106 default_score = 0.85
108 if alignment_scores is None:
109 alignment_scores = DEFAULT_ALIGNMENT_SCORES
111 if not hasattr(extraction, "alignment_status") or not (
112 extraction.alignment_status
113 ):
114 return default_score
116 status = str(extraction.alignment_status).upper()
117 for status_key, score in alignment_scores.items():
118 if status_key in status:
119 return score
121 return default_score
124def convert_langextract_to_presidio_results(
125 langextract_result,
126 entity_mappings: Dict,
127 supported_entities: List[str],
128 enable_generic_consolidation: bool,
129 recognizer_name: str,
130 alignment_scores: Optional[Dict[str, float]] = None
131) -> List[RecognizerResult]:
132 """Convert LangExtract extraction results to Presidio RecognizerResult objects.
134 :param langextract_result: LangExtract result object with extractions.
135 :param entity_mappings: Mapping of extraction classes to Presidio entity types.
136 :param supported_entities: List of supported Presidio entity types.
137 :param enable_generic_consolidation: Whether to consolidate unknown entities.
138 :param recognizer_name: Name of recognizer for result metadata.
139 :param alignment_scores: Custom alignment score mappings (optional).
140 :return: List of Presidio RecognizerResult objects.
141 """
142 results = []
143 if not langextract_result or not langextract_result.extractions:
144 return results
146 supported_entities_set = set(supported_entities)
148 for extraction in langextract_result.extractions:
149 extraction_class = extraction.extraction_class
151 if extraction_class in supported_entities_set:
152 entity_type = extraction_class
153 else:
154 extraction_class_lower = extraction_class.lower()
155 entity_type = entity_mappings.get(extraction_class_lower)
157 if not entity_type:
158 if enable_generic_consolidation:
159 entity_type = extraction_class.upper()
160 logger.debug(
161 "Unknown extraction class '%s' will be consolidated to "
162 "GENERIC_PII_ENTITY",
163 extraction_class,
164 )
165 else:
166 logger.warning(
167 "Unknown extraction class '%s' not found in entity "
168 "mappings, skipping",
169 extraction_class,
170 )
171 continue
173 if not extraction.char_interval:
174 logger.warning("Extraction missing char_interval, skipping")
175 continue
177 confidence = calculate_extraction_confidence(extraction, alignment_scores)
179 metadata = {}
180 if hasattr(extraction, 'attributes') and extraction.attributes:
181 metadata['attributes'] = extraction.attributes
182 if hasattr(extraction, 'alignment_status') and extraction.alignment_status:
183 metadata['alignment'] = str(extraction.alignment_status)
185 explanation = AnalysisExplanation(
186 recognizer=recognizer_name,
187 original_score=confidence,
188 textual_explanation=(
189 f"LangExtract extraction with "
190 f"{extraction.alignment_status} alignment"
191 if hasattr(extraction, "alignment_status")
192 and extraction.alignment_status
193 else "LangExtract extraction"
194 ),
195 )
197 result = RecognizerResult(
198 entity_type=entity_type,
199 start=extraction.char_interval.start_pos,
200 end=extraction.char_interval.end_pos,
201 score=confidence,
202 analysis_explanation=explanation,
203 recognition_metadata=metadata if metadata else None
204 )
206 results.append(result)
208 return results