Coverage for presidio_analyzer / predefined_recognizers / third_party / langextract_recognizer.py: 98%
56 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
1import logging
2from abc import ABC, abstractmethod
3from typing import Any, Dict, List, Optional
5from presidio_analyzer.llm_utils import (
6 check_langextract_available,
7 convert_langextract_to_presidio_results,
8 convert_to_langextract_format,
9 extract_lm_config,
10 get_model_config,
11 get_supported_entities,
12 load_prompt_file,
13 load_yaml_examples,
14 load_yaml_file,
15 lx,
16 render_jinja_template,
17 validate_config_fields,
18)
19from presidio_analyzer.lm_recognizer import LMRecognizer
21logger = logging.getLogger("presidio-analyzer")
24class LangExtractRecognizer(LMRecognizer, ABC):
25 """
26 Base class for LangExtract-based PII recognizers.
28 Subclasses implement _call_langextract() for specific LLM providers.
29 """
31 def __init__(
32 self,
33 config_path: str,
34 name: str = "LangExtract LLM PII",
35 supported_language: str = "en",
36 extract_params: Optional[Dict[str, Any]] = None,
37 ):
38 """Initialize LangExtract recognizer.
40 :param config_path: Path to configuration file.
41 :param name: Name of the recognizer (provided by subclass).
42 :param supported_language: Language this recognizer supports (default: "en").
43 :param extract_params: Dict with 'extract' and/or 'language_model'
44 keys containing param defaults.
45 """
46 check_langextract_available()
48 full_config = load_yaml_file(config_path)
50 lm_config = extract_lm_config(full_config)
51 langextract_config = full_config.get("langextract", {})
53 supported_entities = get_supported_entities(lm_config, langextract_config)
55 if not supported_entities:
56 raise ValueError(
57 "Configuration must contain 'supported_entities' in "
58 "'lm_recognizer' or 'langextract'"
59 )
61 validate_config_fields(
62 full_config,
63 [
64 ("langextract",),
65 ("langextract", "model"),
66 ("langextract", "model", "model_id"),
67 ("langextract", "entity_mappings"),
68 ("langextract", "prompt_file"),
69 ("langextract", "examples_file"),
70 ]
71 )
73 self.config = langextract_config
74 model_config = get_model_config(
75 full_config, provider_key="langextract"
76 )
78 super().__init__(
79 supported_entities=supported_entities,
80 supported_language=supported_language,
81 name=name,
82 version="1.0.0",
83 model_id=model_config["model_id"],
84 temperature=model_config.get("temperature"),
85 min_score=lm_config.get("min_score"),
86 labels_to_ignore=lm_config.get("labels_to_ignore"),
87 enable_generic_consolidation=lm_config.get(
88 "enable_generic_consolidation"
89 ),
90 )
92 examples_data = load_yaml_examples(
93 langextract_config["examples_file"]
94 )
95 self.examples = convert_to_langextract_format(examples_data)
97 prompt_template = load_prompt_file(
98 langextract_config["prompt_file"]
99 )
100 self.prompt_description = render_jinja_template(
101 prompt_template,
102 supported_entities=self.supported_entities,
103 enable_generic_consolidation=self.enable_generic_consolidation,
104 labels_to_ignore=self.labels_to_ignore,
105 )
107 self.entity_mappings = langextract_config["entity_mappings"]
108 self.debug = langextract_config.get("debug", False)
109 self._model_config = model_config
111 # Process extract params with config override
112 self._extract_params = {}
113 self._language_model_params = {}
115 if extract_params:
116 if "extract" in extract_params:
117 for param_name, default_value in extract_params["extract"].items():
118 self._extract_params[param_name] = self._model_config.get(
119 param_name, default_value
120 )
122 if "language_model" in extract_params:
123 for param_name, default_value in (
124 extract_params["language_model"].items()
125 ):
126 self._language_model_params[param_name] = (
127 self._model_config.get(param_name, default_value)
128 )
130 def _call_llm(self, text: str, entities: List[str], **kwargs):
131 """Call LangExtract LLM."""
132 # Build extract params
133 extract_params = {
134 "text": text,
135 "prompt": self.prompt_description,
136 "examples": self.examples,
137 "debug": self.debug,
138 }
140 # Add temperature if configured
141 if self.temperature is not None:
142 extract_params["temperature"] = self.temperature
144 # Add any additional kwargs
145 extract_params.update(kwargs)
147 langextract_result = self._call_langextract(**extract_params)
149 return convert_langextract_to_presidio_results(
150 langextract_result=langextract_result,
151 entity_mappings=self.entity_mappings,
152 supported_entities=self.supported_entities,
153 enable_generic_consolidation=self.enable_generic_consolidation,
154 recognizer_name=self.__class__.__name__
155 )
157 def _call_langextract(self, **kwargs):
158 """Call LangExtract with configured parameters."""
159 try:
160 extract_params = {
161 "text_or_documents": kwargs.pop("text"),
162 "prompt_description": kwargs.pop("prompt"),
163 "examples": kwargs.pop("examples"),
164 }
166 extract_params.update(self._get_provider_params())
167 extract_params.update(self._extract_params)
168 if self._language_model_params:
169 extract_params["language_model_params"] = self._language_model_params
170 extract_params.update(kwargs)
172 return lx.extract(**extract_params)
173 except Exception:
174 logger.exception(
175 "LangExtract extraction failed (model '%s')",
176 self.model_id
177 )
178 raise
180 @abstractmethod
181 def _get_provider_params(self) -> Dict[str, Any]:
182 """Return provider-specific params.
184 Examples: model_id, model_url, azure_endpoint, etc.
185 """
186 ...