feat(guardrails): añade NeMo stub, CompositeEngine paralelo y factory

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Juan
2026-05-10 10:51:17 +02:00
co-authored by Claude Opus 4.7
parent 79c6fe32ff
commit af8036c2d2
4 changed files with 174 additions and 0 deletions
@@ -0,0 +1,38 @@
"""Engine que ejecuta varios sub-engines en paralelo y agrega resultados."""
from __future__ import annotations
import asyncio
from typing import Any
from uuid import UUID
from agentforge_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition
from agentforge_core.guardrails.base import GuardrailEngine
class CompositeGuardrailEngine:
"""Compone varios GuardrailEngine y agrega sus violaciones."""
name = "composite"
def __init__(self, engines: list[GuardrailEngine]) -> None:
if not engines:
raise ValueError("CompositeGuardrailEngine requiere al menos un engine.")
self._engines = engines
async def validate_input(
self, payload: str, policy: PolicyDefinition, trace_id: UUID
) -> list[GuardrailViolation]:
results = await asyncio.gather(
*(e.validate_input(payload, policy, trace_id) for e in self._engines)
)
return [v for sub in results for v in sub]
async def validate_output(
self, payload: dict[str, Any], policy: PolicyDefinition, trace_id: UUID
) -> list[GuardrailViolation]:
results = await asyncio.gather(
*(e.validate_output(payload, policy, trace_id) for e in self._engines)
)
return [v for sub in results for v in sub]
@@ -0,0 +1,33 @@
"""Factory que ensambla el GuardrailEngine según settings."""
from __future__ import annotations
from agentforge_core.config import Settings
from agentforge_core.guardrails.base import GuardrailEngine
from agentforge_core.guardrails.composite import CompositeGuardrailEngine
from agentforge_core.guardrails.guardrails_ai import GuardrailsAIEngine
from agentforge_core.guardrails.nemo import NeMoGuardrailsEngine
def build_guardrail_engine(settings: Settings) -> GuardrailEngine:
"""Ensambla el engine: GuardrailsAIEngine siempre; NeMo si está habilitado."""
engines: list[GuardrailEngine] = [GuardrailsAIEngine()]
if settings.guardrails_nemo_enabled:
engines.append(
NeMoGuardrailsEngine(
allowed_keywords=[
"sip",
"ims",
"cscf",
"sbc",
"mos",
"hss",
"registro",
"incidente",
"voz",
"codec",
"rollback",
],
)
)
return CompositeGuardrailEngine(engines)
@@ -0,0 +1,59 @@
"""Stub de NeMo Guardrails. Solo se carga si GUARDRAILS_NEMO_ENABLED=true.
En MVP se entrega como capa adicional de topical rails. La integración Colang
completa queda documentada en docs/futuro.md como evolución natural.
"""
from __future__ import annotations
from datetime import UTC, datetime
from typing import Any
from uuid import UUID
import structlog
from agentforge_core.domain.guardrail import GuardrailViolation
from agentforge_core.domain.policy import PolicyDefinition
log = structlog.get_logger(__name__)
class NeMoGuardrailsEngine:
"""Implementación mínima: detecta off-topic vía heurística básica.
Cuando se active la integración Colang real, este engine cargará un YAML
de configuración y delegará a ``nemoguardrails.LLMRails``. Por ahora aplica
una regla simple: si el input no menciona ninguno de los keywords de
``allowed_keywords``, marca off-topic con severity warning.
"""
name = "nemo"
def __init__(self, allowed_keywords: list[str] | None = None) -> None:
self._allowed = [k.lower() for k in (allowed_keywords or [])]
async def validate_input(
self, payload: str, policy: PolicyDefinition, trace_id: UUID
) -> list[GuardrailViolation]:
if not self._allowed:
return []
lower = payload.lower()
if any(k in lower for k in self._allowed):
return []
log.info("nemo_offtopic", trace_id=str(trace_id))
return [
GuardrailViolation(
trace_id=trace_id,
timestamp=datetime.now(UTC),
stage="input",
validator="NeMoTopicalRails",
severity="warning",
message="Input fuera de los temas permitidos.",
blocked=False,
)
]
async def validate_output(
self, payload: dict[str, Any], policy: PolicyDefinition, trace_id: UUID
) -> list[GuardrailViolation]:
return []