From eac2ed02a4a8610afb0455f4b91be44be85abb4e Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 11 May 2026 14:38:52 +0200 Subject: [PATCH] =?UTF-8?q?fix(guardrails):=20Presidio=20usa=20en=5Fcore?= =?UTF-8?q?=5Fweb=5Fsm=20cacheado=20(en=5Fcore=5Fweb=5Flg=20no=20est=C3=A1?= =?UTF-8?q?=20en=20la=20imagen)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `detect_pii` instanciaba `AnalyzerEngine()` sin configuración, así que Presidio intentaba cargar su modelo por defecto `en_core_web_lg` (~560 MB), que no está en la imagen Docker — `core/Dockerfile` instala `en_core_web_sm`. Resultado: el validador fallaba con `[E050] Can't find model 'en_core_web_lg'` y, al ser la política fail-closed, *toda* invocación quedaba `blocked_by_guardrail`. Además se reinstanciaba el engine en cada llamada (caro; probablemente la causa del crash del proceso al segundo invoke). Ahora: - `_presidio_analyzer()`: singleton perezoso con `NlpEngineProvider` → `en_core_web_sm`. - si Presidio no está instalado o el modelo no carga, se hace fallback a la detección por regex (antes solo se hacía fallback ante ImportError). Detectado ejecutando el smoke de docker-compose (el venv local no tiene Presidio, así que los tests usan la rama de regex y no lo veían). Co-Authored-By: Claude Opus 4.7 --- .../agentforge_core/guardrails/validators.py | 34 ++++++++++++++++--- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/core/src/agentforge_core/guardrails/validators.py b/core/src/agentforge_core/guardrails/validators.py index b0d128b..26b0b3a 100644 --- a/core/src/agentforge_core/guardrails/validators.py +++ b/core/src/agentforge_core/guardrails/validators.py @@ -21,6 +21,33 @@ _INJECTION_PATTERNS = [ r"reveal (the )?(system|hidden) (prompt|instruction)", ] +# Singleton perezoso del AnalyzerEngine de Presidio. Construirlo es caro (carga el +# modelo spaCy y los recognizers), así que se reutiliza entre llamadas. +_PRESIDIO_ANALYZER: Any = None + + +def _presidio_analyzer() -> Any: + """``AnalyzerEngine`` de Presidio configurado con el modelo spaCy ``en_core_web_sm``. + + Presidio usa por defecto ``en_core_web_lg`` (~560 MB), que **no** está en la imagen + Docker: ``core/Dockerfile`` instala ``en_core_web_sm``. Si Presidio no está instalado + o el modelo no se puede cargar, esto lanza y el llamador (`detect_pii`) hace fallback + a la detección por regex. + """ + global _PRESIDIO_ANALYZER + if _PRESIDIO_ANALYZER is None: + from presidio_analyzer import AnalyzerEngine + from presidio_analyzer.nlp_engine import NlpEngineProvider + + nlp_engine = NlpEngineProvider( + nlp_configuration={ + "nlp_engine_name": "spacy", + "models": [{"lang_code": "en", "model_name": "en_core_web_sm"}], + } + ).create_engine() + _PRESIDIO_ANALYZER = AnalyzerEngine(nlp_engine=nlp_engine) + return _PRESIDIO_ANALYZER + def _violation( *, @@ -45,11 +72,11 @@ def _violation( def detect_pii( text: str, config: dict[str, Any], trace_id: UUID, stage: str ) -> list[GuardrailViolation]: - """Detección PII vía Presidio Analyzer (con fallback a regex si no está instalado).""" + """Detección PII vía Presidio Analyzer (con fallback a regex si no está disponible).""" try: - from presidio_analyzer import AnalyzerEngine + analyzer = _presidio_analyzer() except Exception: - # Si Presidio no está, fallback a regex básica + # Presidio no instalado o modelo spaCy no disponible → fallback a regex básica. return _pii_regex_fallback(text, config, trace_id, stage) entities = config.get( @@ -59,7 +86,6 @@ def detect_pii( sev = config.get("severity_on_match", "block") blocked = sev == "block" - analyzer = AnalyzerEngine() results = analyzer.analyze(text=text, entities=entities, language="en") if not results: return []