docs: add AgentForge initial design spec
Diseño completo de la plataforma de gobernanza de agentes IA: arquitectura two-tier (FastAPI core + Streamlit dashboard), modelos de dominio, contratos REST, interfaces Strategy (LLM, Guardrails, Registry), flujo LangGraph con HITL dinámico vía interrupt(), versionado tipo Git en sistema de ficheros, política y validadores, manejo de errores y plan de testing. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
+42
@@ -0,0 +1,42 @@
|
||||
# Secrets y configuración local
|
||||
.env
|
||||
.env.local
|
||||
*.key
|
||||
*.pem
|
||||
|
||||
# Datos runtime (registry, checkpoints, logs)
|
||||
data/
|
||||
*.sqlite
|
||||
*.sqlite-*
|
||||
*.jsonl
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
*.egg-info/
|
||||
.eggs/
|
||||
build/
|
||||
dist/
|
||||
.pytest_cache/
|
||||
.mypy_cache/
|
||||
.ruff_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Entornos virtuales
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
# IDE / OS
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Streamlit
|
||||
.streamlit/secrets.toml
|
||||
@@ -0,0 +1,631 @@
|
||||
# AgentForge — Documento de diseño
|
||||
|
||||
**Fecha:** 2026-05-09
|
||||
**Estado:** aprobado — listo para plan de implementación
|
||||
**Autor:** Juan
|
||||
**Revisión:** v1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen ejecutivo
|
||||
|
||||
**AgentForge** es una plataforma profesional de **gobernanza de agentes IA** orientada a entornos de producción. Cubre el ciclo de vida completo: catalogación, versionado de prompts y políticas, validación con guardrails en runtime, ejecución stateful con checkpointing, Human-in-the-Loop nativo, y trazabilidad end-to-end.
|
||||
|
||||
La plataforma se compone de dos servicios:
|
||||
|
||||
- **`agentforge-core`** — API REST (FastAPI) que materializa el dominio de gobierno: registry, versionado, ejecución de agentes vía LangGraph, validación de guardrails y persistencia.
|
||||
- **`agentforge-dashboard`** — UI ejecutiva (Streamlit) cliente del core; nunca habla con LLMs ni guardrails directamente.
|
||||
|
||||
Una imagen Docker por servicio, orquestadas con `docker-compose`. Arranque sin claves API (proveedor LLM mock por defecto).
|
||||
|
||||
## 2. Problema y motivación
|
||||
|
||||
Poner agentes IA en producción sin una capa de gobernanza produce sistemas opacos:
|
||||
|
||||
- Prompts que cambian en caliente sin historial.
|
||||
- Validaciones inconsistentes según quién despliega.
|
||||
- Acciones de alto impacto (rollbacks, cambios de configuración, ejecución de comandos) propuestas sin supervisión humana ni rollback plan.
|
||||
- Decisiones del agente no auditables — imposible reconstruir por qué se tomó una acción concreta.
|
||||
|
||||
AgentForge define el plano de control mínimo que un equipo de plataforma necesita antes de operar agentes con impacto real. El proyecto no resuelve la inteligencia del agente; resuelve su **operabilidad y gobierno**.
|
||||
|
||||
## 3. Decisiones de diseño (resumen)
|
||||
|
||||
| Eje | Decisión | Justificación |
|
||||
|---|---|---|
|
||||
| Topología | Two-tier: FastAPI core + Streamlit dashboard | Separación de responsabilidades; el core es API-first y consumible por terceros (RPA, n8n, otros agentes). Patrón de plataformas reales. |
|
||||
| LLM | Interfaz `LLMProvider` con `mock` (default), `azure`, `openai` | Demo arranca sin secrets. Demuestra portabilidad de proveedor. |
|
||||
| Guardrails | Interfaz `GuardrailEngine` con `GuardrailsAI` (default) y `NeMo` (opt-in vía flag) | Lo mejor de ambos mundos: arquitectura extensible visible + imagen ligera por defecto. |
|
||||
| Persistencia | Mezcla por capa: JSON (registry, definiciones), JSONL (logs append-only), YAML (versiones), SQLite (checkpoints LangGraph) | Cada capa con la herramienta correcta. Definiciones humanas en YAML; estado runtime en formatos legibles por máquina. |
|
||||
| Versionado | Simulación tipo Git en sistema de ficheros: `agents/<n>/versions/v1.yaml` + `index.yaml` con hashes y mensajes | No requiere git real; reproduce la disciplina de versionado y diff. |
|
||||
| Estado | LangGraph con `SqliteSaver` + `interrupt()` dinámico para HITL | Persistencia entre reinicios crítica para esperas humanas. |
|
||||
| Auth | Ninguna en MVP; placeholder hook `Depends(get_current_user)` | Pluggable a OAuth2/OIDC/MSAL en futuro. |
|
||||
| Comentarios | Español | Coherente con el autor. |
|
||||
|
||||
## 4. Arquitectura
|
||||
|
||||
```
|
||||
┌───────────────────────── docker-compose ──────────────────────────┐
|
||||
│ │
|
||||
│ ┌─────────────────────┐ HTTP/JSON ┌────────────────────┐ │
|
||||
│ │ agentforge-dashboard│ ────────────────► │ agentforge-core │ │
|
||||
│ │ Streamlit :8501 │ ◄──────────────── │ FastAPI :8000 │ │
|
||||
│ └─────────────────────┘ └─────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────────────┼──────────┐ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ LLM layer Guardrail layer Runtime State│
|
||||
│ ┌──────────────────┐ ┌────────────────────┐ ┌─────────┐ ┌───┐│
|
||||
│ │ LLMProvider (P) │ │ GuardrailEngine(P) │ │LangGraph│ │JSN││
|
||||
│ │ ├ MockProvider │ │ ├ GuardrailsAIEng │ │ + HITL │ │SQL││
|
||||
│ │ ├ AzureOpenAI │ │ ├ NeMoEngine (opt) │ │ + ckpt │ │JNL││
|
||||
│ │ └ OpenAI │ │ └ CompositeEngine │ └─────────┘ └───┘│
|
||||
│ └──────────────────┘ └────────────────────┘ │
|
||||
│ │
|
||||
│ Strategy pattern con Pydantic v2 + Protocol typing │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Principios:**
|
||||
|
||||
- **API-first:** el core es invocable por cualquier cliente HTTP; el dashboard es un consumidor sustituible.
|
||||
- **Configuration over code:** definiciones de agentes y políticas en YAML editables sin redeploy.
|
||||
- **Strategy/Plugin:** todas las dependencias externas (LLM, guardrails, registry) detrás de `Protocol`.
|
||||
- **Fail-closed por defecto:** errores en validadores se traducen a violación que bloquea la ejecución.
|
||||
- **Trazabilidad obligatoria:** cada ejecución tiene un `trace_id` UUID propagado por logs, API, dashboard y persistencia.
|
||||
|
||||
## 5. Estructura de repositorio
|
||||
|
||||
```
|
||||
agentforge/
|
||||
├── README.md
|
||||
├── ARCHITECTURE.md
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── Makefile # up / down / test / lint / format / smoke
|
||||
├── pyproject.toml # ruff + mypy + pytest config
|
||||
├── core/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── src/agentforge_core/
|
||||
│ ├── main.py # FastAPI app + middlewares
|
||||
│ ├── config.py # Pydantic Settings, .env
|
||||
│ ├── api/
|
||||
│ │ ├── agents.py
|
||||
│ │ ├── executions.py
|
||||
│ │ ├── policies.py
|
||||
│ │ └── violations.py
|
||||
│ ├── domain/ # modelos Pydantic v2
|
||||
│ │ ├── agent.py
|
||||
│ │ ├── execution.py
|
||||
│ │ ├── guardrail.py
|
||||
│ │ └── policy.py
|
||||
│ ├── registry/
|
||||
│ │ ├── repository.py
|
||||
│ │ └── versioning.py
|
||||
│ ├── llm/
|
||||
│ │ ├── base.py # Protocol LLMProvider
|
||||
│ │ ├── mock.py
|
||||
│ │ ├── azure.py
|
||||
│ │ ├── openai.py
|
||||
│ │ └── factory.py
|
||||
│ ├── guardrails/
|
||||
│ │ ├── base.py # Protocol GuardrailEngine
|
||||
│ │ ├── guardrails_ai.py
|
||||
│ │ ├── nemo.py
|
||||
│ │ ├── composite.py
|
||||
│ │ └── factory.py
|
||||
│ ├── runtime/
|
||||
│ │ ├── graph.py # build_graph(agent_def, policy)
|
||||
│ │ ├── state.py # AgentState (TypedDict)
|
||||
│ │ ├── nodes.py # cada nodo del grafo
|
||||
│ │ └── checkpointer.py # SqliteSaver wrapper
|
||||
│ └── observability/
|
||||
│ └── logging.py # structlog + trace_id propagation
|
||||
├── dashboard/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── src/agentforge_dashboard/
|
||||
│ ├── app.py
|
||||
│ ├── client.py # httpx client tipado al core
|
||||
│ ├── pages/
|
||||
│ │ ├── 1_🏛️_Registro.py
|
||||
│ │ ├── 2_▶️_Ejecutar.py
|
||||
│ │ ├── 3_🤝_Aprobaciones.py
|
||||
│ │ ├── 4_📜_Historial.py
|
||||
│ │ └── 5_📐_Politicas.py
|
||||
│ └── components/
|
||||
│ ├── trace_view.py
|
||||
│ ├── violation_view.py
|
||||
│ └── diff_view.py
|
||||
├── agents/
|
||||
│ └── incident_analyzer/
|
||||
│ ├── index.yaml # versiones disponibles + metadata
|
||||
│ ├── versions/
|
||||
│ │ ├── v1.yaml
|
||||
│ │ └── v2.yaml
|
||||
│ └── examples/
|
||||
│ ├── 01_sip_registration_drop.txt
|
||||
│ ├── 02_mos_degradation_pool_sbc.txt
|
||||
│ └── 03_hss_capacity_active_active.txt
|
||||
├── policies/
|
||||
│ └── default/
|
||||
│ ├── index.yaml
|
||||
│ └── versions/
|
||||
│ └── v1.yaml
|
||||
├── data/ # gitignored
|
||||
│ ├── registry.json
|
||||
│ ├── checkpoints.sqlite
|
||||
│ ├── violations.jsonl
|
||||
│ └── executions.jsonl
|
||||
├── docs/
|
||||
│ ├── architecture.md
|
||||
│ ├── futuro.md
|
||||
│ ├── manual_qa.md
|
||||
│ └── superpowers/specs/
|
||||
│ └── 2026-05-09-agentforge-design.md
|
||||
└── tests/
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
|
||||
- Pydantic models residen en `domain/`, no en `api/`. Los routers solo serializan.
|
||||
- Las factories (`llm/factory.py`, `guardrails/factory.py`) son la frontera donde el `.env` decide la implementación.
|
||||
- YAML para definiciones humanas (legibles, comentables, mejor diff). JSON/JSONL/SQLite para estado runtime.
|
||||
- `__init__.py` mínimos, sin re-exports masivos.
|
||||
|
||||
## 6. Modelos de dominio
|
||||
|
||||
```python
|
||||
class LLMConfig(BaseModel):
|
||||
provider: Literal["mock", "azure", "openai"] = "mock"
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.2
|
||||
max_tokens: int = 2000
|
||||
|
||||
class AgentVersionMeta(BaseModel):
|
||||
id: str # ej. "v2"
|
||||
hash: str # SHA-256 del YAML normalizado
|
||||
author: str
|
||||
message: str
|
||||
created_at: datetime
|
||||
|
||||
class AgentDefinition(BaseModel):
|
||||
name: str
|
||||
version: str # semver o vN
|
||||
owner: str
|
||||
purpose: str
|
||||
state: Literal["draft", "active", "deprecated"]
|
||||
guardrails: list[str] # nombres de policies
|
||||
llm: LLMConfig
|
||||
system_prompt: str
|
||||
output_schema: dict # JSON Schema del output esperado
|
||||
risk_threshold_for_hitl: int = 4 # risk_score ≥ X exige aprobación humana
|
||||
updated_at: datetime
|
||||
|
||||
class GuardrailViolation(BaseModel):
|
||||
trace_id: UUID
|
||||
timestamp: datetime
|
||||
stage: Literal["input", "output"]
|
||||
validator: str # ej. "DetectPII"
|
||||
severity: Literal["info", "warning", "block"]
|
||||
message: str
|
||||
blocked: bool # True si detuvo la ejecución
|
||||
|
||||
class ProposedAction(BaseModel):
|
||||
id: str # generado, único dentro de la ejecución
|
||||
action: str # ej. "rollback_image"
|
||||
target: str # ej. "cscf-cluster-aravaca-01"
|
||||
risk_score: int = Field(ge=1, le=5)
|
||||
rollback_plan: str
|
||||
requires_approval: bool
|
||||
|
||||
class DecisionStep(BaseModel):
|
||||
step: str # validate_input | llm_reason | ...
|
||||
timestamp: datetime
|
||||
duration_ms: int
|
||||
detail: dict # libre por nodo
|
||||
|
||||
class AgentExecution(BaseModel):
|
||||
trace_id: UUID
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
status: Literal[
|
||||
"running",
|
||||
"awaiting_approval",
|
||||
"blocked_by_guardrail",
|
||||
"completed",
|
||||
"failed",
|
||||
]
|
||||
started_at: datetime
|
||||
finished_at: datetime | None
|
||||
decision_path: list[DecisionStep]
|
||||
violations: list[GuardrailViolation]
|
||||
proposed_actions: list[ProposedAction]
|
||||
needs_human_for: list[ProposedAction] | None
|
||||
final_output: dict | None
|
||||
error: str | None
|
||||
|
||||
class AgentExecutionSummary(BaseModel):
|
||||
"""Versión ligera para listados (sin decision_path ni violations completas)."""
|
||||
trace_id: UUID
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
status: str
|
||||
started_at: datetime
|
||||
finished_at: datetime | None
|
||||
n_violations: int
|
||||
n_proposed_actions: int
|
||||
|
||||
class PolicyValidator(BaseModel):
|
||||
type: str # "detect_pii", "prompt_injection", ...
|
||||
config: dict # parámetros del validador
|
||||
|
||||
class PolicyDefinition(BaseModel):
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
input_validators: list[PolicyValidator]
|
||||
output_validators: list[PolicyValidator]
|
||||
on_validator_error: Literal["fail_open", "fail_closed"] = "fail_closed"
|
||||
```
|
||||
|
||||
## 7. API REST
|
||||
|
||||
```
|
||||
GET /health
|
||||
GET /agents → list[AgentDefinition]
|
||||
GET /agents/{name} → AgentDefinition (versión activa)
|
||||
GET /agents/{name}/versions → list[AgentVersionMeta]
|
||||
GET /agents/{name}/versions/{v} → AgentDefinition (versión concreta)
|
||||
GET /agents/{name}/versions/{v}/diff/{w} → DiffResult (unified diff)
|
||||
POST /agents/{name}/invoke → AgentExecution
|
||||
body: {"input": str, "version": str?}
|
||||
GET /executions → list[AgentExecutionSummary]
|
||||
GET /executions/{trace_id} → AgentExecution
|
||||
POST /executions/{trace_id}/approve → AgentExecution
|
||||
body: {"approved_action_ids": list[str],
|
||||
"comment": str?}
|
||||
POST /executions/{trace_id}/reject → AgentExecution
|
||||
body: {"reason": str}
|
||||
GET /violations → list[GuardrailViolation]
|
||||
query: trace_id?, severity?, since?
|
||||
GET /policies → list[PolicyDefinition]
|
||||
GET /policies/{name}/versions → list[PolicyVersionMeta]
|
||||
```
|
||||
|
||||
Códigos de error relevantes:
|
||||
|
||||
- `404` agente o ejecución no existe
|
||||
- `409` `/approve` o `/reject` sobre ejecución no en `awaiting_approval`
|
||||
- `422` body inválido (Pydantic)
|
||||
- `500` fallo interno (checkpoint corrupto, configuración inválida)
|
||||
|
||||
Todas las respuestas incluyen header `X-Trace-Id` (eco del request o generado).
|
||||
|
||||
## 8. Interfaces clave (Strategy)
|
||||
|
||||
```python
|
||||
class Message(BaseModel):
|
||||
role: Literal["system", "user", "assistant"]
|
||||
content: str
|
||||
|
||||
class CompletionResult(BaseModel):
|
||||
content: str
|
||||
model: str
|
||||
tokens_in: int
|
||||
tokens_out: int
|
||||
latency_ms: int
|
||||
|
||||
class LLMProvider(Protocol):
|
||||
name: str
|
||||
async def complete(
|
||||
self,
|
||||
messages: list[Message],
|
||||
schema: dict | None = None,
|
||||
temperature: float = 0.2,
|
||||
max_tokens: int = 2000,
|
||||
) -> CompletionResult: ...
|
||||
|
||||
class GuardrailEngine(Protocol):
|
||||
name: str
|
||||
async def validate_input(
|
||||
self, payload: str, policy: PolicyDefinition, trace_id: UUID
|
||||
) -> list[GuardrailViolation]: ...
|
||||
async def validate_output(
|
||||
self, payload: dict, policy: PolicyDefinition, trace_id: UUID
|
||||
) -> list[GuardrailViolation]: ...
|
||||
|
||||
class AgentRegistry(Protocol):
|
||||
def list_agents(self) -> list[AgentDefinition]: ...
|
||||
def get_agent(self, name: str, version: str | None = None) -> AgentDefinition: ...
|
||||
def list_versions(self, name: str) -> list[AgentVersionMeta]: ...
|
||||
def get_version(self, name: str, version: str) -> AgentDefinition: ...
|
||||
def diff_versions(self, name: str, v1: str, v2: str) -> DiffResult: ...
|
||||
def upsert_version(
|
||||
self, name: str, body: AgentDefinition, message: str, author: str
|
||||
) -> AgentVersionMeta: ...
|
||||
|
||||
class PolicyStore(Protocol):
|
||||
def list_policies(self) -> list[PolicyDefinition]: ...
|
||||
def get_policy(self, name: str, version: str | None = None) -> PolicyDefinition: ...
|
||||
def list_versions(self, name: str) -> list[PolicyVersionMeta]: ...
|
||||
```
|
||||
|
||||
## 9. Flujo de ejecución (LangGraph)
|
||||
|
||||
**Topología del grafo (común para todos los agentes; varía la `AgentDefinition` y `Policy`):**
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ validate_in │── violations.severity=block ──► [STATUS: blocked_by_guardrail] ─► END
|
||||
└──────┬───────┘
|
||||
│ pass
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ llm_reason │── 3xretry → fallback → giveup ─► [STATUS: failed] ─► END
|
||||
└──────┬───────┘
|
||||
│ ok
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ validate_out │── violations.severity=block ──► [STATUS: blocked_by_guardrail] ─► END
|
||||
└──────┬───────┘
|
||||
│ pass
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ propose_actions │
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐ ─ any(risk≥threshold or requires_approval) ─┐
|
||||
│ approve_gate │ │
|
||||
│ (dyn. interrupt)│ ─ todas seguras ──┐ │
|
||||
└──────────────────┘ │ │
|
||||
▼ ▼
|
||||
┌──────────────┐ [interrupt(payload)]
|
||||
│ finalize │ status=awaiting_approval
|
||||
└──────┬───────┘ │
|
||||
▼ │ POST /approve
|
||||
[STATUS: completed] │ POST /reject
|
||||
▼
|
||||
[Command(resume={...}) → finalize → completed
|
||||
o status=failed, error="rejected_by_human"]
|
||||
```
|
||||
|
||||
**Estado del grafo (TypedDict de LangGraph):**
|
||||
|
||||
```python
|
||||
class AgentState(TypedDict):
|
||||
trace_id: str
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
user_input: str
|
||||
messages: list[dict]
|
||||
raw_llm_output: str | None
|
||||
parsed_output: dict | None
|
||||
proposed_actions: list[dict]
|
||||
violations: list[dict]
|
||||
decision_path: Annotated[list[dict], operator.add] # acumulativo
|
||||
status: str
|
||||
error: str | None
|
||||
human_decision: dict | None # rellena en /approve|/reject
|
||||
```
|
||||
|
||||
`Annotated[..., operator.add]` permite que cada nodo añada pasos sin pisar lo previo (semántica nativa de LangGraph para reducers).
|
||||
|
||||
**Checkpointing:** `SqliteSaver("data/checkpoints.sqlite")` con `thread_id = trace_id`. El estado se persiste tras cada nodo. Si el dashboard cierra durante un `awaiting_approval`, `GET /executions/{trace_id}` reconstruye el snapshot exacto.
|
||||
|
||||
**Recorrido típico — `POST /agents/incident_analyzer/invoke`:**
|
||||
|
||||
1. API genera `trace_id` (UUID4); resuelve `AgentDefinition` vía `AgentRegistry`; resuelve `PolicyDefinition` referenciada en `agent_def.guardrails`; loga `execution.started` con `trace_id`.
|
||||
2. `runtime.graph.build_graph(agent_def, policy)` compila el grafo:
|
||||
- Inyecta `LLMProvider` vía factory (override por `LLM_PROVIDER` env).
|
||||
- Inyecta `GuardrailEngine` vía factory (`Composite[GuardrailsAI]`, +`NeMo` si flag).
|
||||
- Checkpointer = `SqliteSaver`. El nodo `approve_gate` invoca dinámicamente `interrupt(payload)` (API de LangGraph ≥0.2) solo cuando hay acciones que requieren aprobación; si todas son seguras, transiciona a `finalize` sin pausa. Esto evita el `interrupt_before` estático y mantiene el flujo declarativo.
|
||||
3. `graph.invoke({...}, config={"configurable":{"thread_id": trace_id}})` ejecuta hasta:
|
||||
- (a) END por guardrail block en `validate_input` o `validate_output`.
|
||||
- (b) END por LLM unrecoverable failure.
|
||||
- (c) INTERRUPT en `approve_gate` (HITL).
|
||||
- (d) END normal (`finalize`) si no hay acciones de alto riesgo.
|
||||
4. API serializa `AgentExecution` y devuelve 200. Append a `data/executions.jsonl` y violaciones a `data/violations.jsonl`.
|
||||
5. (caso HITL) Dashboard pinta cards con cada `ProposedAction` (action, target, badge `risk_score`, `rollback_plan`, botones `[Approve]`/`[Reject]`). Operador pulsa Approve.
|
||||
6. Dashboard `POST /executions/{trace_id}/approve` con body `{"approved_action_ids": ["..."], "comment": "..."}`.
|
||||
7. API: `graph.invoke(Command(resume={"human_decision": {...}}), config={"configurable":{"thread_id": trace_id}})`. Nodo `finalize` lee `state.human_decision`, compone `final_output`. `status="completed"`, `finished_at=now`. Append final a `executions.jsonl`.
|
||||
|
||||
## 10. Guardrails (capa runtime)
|
||||
|
||||
**Estructura `policies/default/versions/v1.yaml`:**
|
||||
|
||||
```yaml
|
||||
name: default
|
||||
version: v1
|
||||
description: Política base para agentes de operación de plataforma de voz
|
||||
input_validators:
|
||||
- type: detect_pii
|
||||
entities: [PERSON, EMAIL, PHONE_NUMBER, ES_NIF, IP_ADDRESS, IBAN_CODE]
|
||||
severity_on_match: block
|
||||
- type: prompt_injection
|
||||
severity_on_match: block
|
||||
- type: toxic_language
|
||||
threshold: 0.7
|
||||
severity_on_match: warning
|
||||
- type: forbidden_topics
|
||||
topics: ["instrucciones de explotación", "credenciales", "código malicioso"]
|
||||
severity_on_match: block
|
||||
|
||||
output_validators:
|
||||
- type: schema_match
|
||||
schema_ref: agent.output_schema # del agent_def en runtime
|
||||
severity_on_mismatch: block
|
||||
- type: pii_leakage
|
||||
severity_on_match: block
|
||||
- type: forbidden_action_keywords
|
||||
keywords: ["DROP TABLE", "rm -rf", "shutdown -h now", "delete production"]
|
||||
severity_on_match: block
|
||||
- type: telco_safety_rules # validador custom
|
||||
rules:
|
||||
- never_propose_action_targeting_production_without_rollback
|
||||
- never_propose_mass_action_without_canary
|
||||
|
||||
on_validator_error: fail_closed
|
||||
```
|
||||
|
||||
**Validadores (Guardrails AI engine):**
|
||||
|
||||
- `DetectPII` — usa `presidio_analyzer` + entidades configuradas; `ES_NIF` como recognizer custom (regex documentada).
|
||||
- `PromptInjection` — heurística de patrones (lista mantenible) + clasificador ligero. En modo `mock` LLM, lista de strings exactos para reproducibilidad de tests.
|
||||
- `ToxicLanguage` — validator del Guardrails Hub.
|
||||
- `ForbiddenTopics` — substring matcher en MVP. `docs/futuro.md` documenta upgrade a similarity con embeddings.
|
||||
- `SchemaMatch` — Pydantic parse contra `agent.output_schema`.
|
||||
- `PIILeakage` — Presidio sobre output stringificado.
|
||||
- `ForbiddenActionKeywords` — substring matcher sobre acciones propuestas.
|
||||
- `TelcoSafetyRules` — validador custom; reglas declarativas evaluadas sobre `proposed_actions[]`.
|
||||
|
||||
**Comportamiento del engine:**
|
||||
|
||||
- Toda violación se persiste a `violations.jsonl` (incluso `info`/`warning`).
|
||||
- Solo `severity=block` con `blocked=True` detiene el grafo.
|
||||
- `on_validator_error` decide qué pasa cuando un validador lanza excepción (`fail_closed` por defecto).
|
||||
- Composite engine ejecuta sub-engines en paralelo y agrega resultados.
|
||||
|
||||
**NeMo Guardrails (opt-in):** activado con `GUARDRAILS_NEMO_ENABLED=true`. Aporta topical rails declarados en Colang (`policies/default/nemo/rails.co`). MVP entrega configuración mínima (off-topic refusal); el flag por defecto está en `false` para mantener la imagen ligera.
|
||||
|
||||
## 11. Versionado de prompts y políticas (simulación tipo Git)
|
||||
|
||||
**Estructura `agents/<name>/index.yaml`:**
|
||||
|
||||
```yaml
|
||||
name: incident_analyzer
|
||||
versions:
|
||||
- id: v1
|
||||
hash: 7c9a4f...
|
||||
author: Juan
|
||||
message: "Versión inicial; cobertura básica de SIP/IMS"
|
||||
created_at: 2026-04-12T10:00:00Z
|
||||
- id: v2
|
||||
hash: a1b2c3...
|
||||
author: Juan
|
||||
message: "Añade detección de codec mismatch; sube risk_threshold a 4"
|
||||
created_at: 2026-05-01T12:00:00Z
|
||||
active_version: v2
|
||||
```
|
||||
|
||||
**Operaciones:**
|
||||
|
||||
- `Registry.upsert_version(name, body, message, author)` — calcula SHA-256 del YAML normalizado, valida formato, escribe `versions/vN.yaml`, actualiza `index.yaml`.
|
||||
- `Registry.diff_versions(name, v1, v2)` — `difflib.unified_diff` sobre los YAMLs.
|
||||
- `Registry.get_agent(name)` — devuelve la versión `active_version`.
|
||||
- Promoción de `draft` → `active` cambia `index.yaml.active_version`.
|
||||
|
||||
**Estructura idéntica para `policies/<name>/`.**
|
||||
|
||||
El dashboard ofrece:
|
||||
- Vista de "Versiones" del agente con autor, mensaje, hash, fecha.
|
||||
- Botón "Comparar con anterior" → render del diff con sintaxis coloreada.
|
||||
|
||||
## 12. Observabilidad
|
||||
|
||||
- **Logging**: `structlog` con renderer JSON. Cada log incluye `trace_id`, `agent_name`, `agent_version`, `step`, `duration_ms` cuando aplica.
|
||||
- **Trace-id propagation**: middleware FastAPI inyecta `X-Trace-Id` (genera UUID4 si no viene). `structlog` lo bind-ea en context (`structlog.contextvars`). Dashboard incluye `X-Trace-Id` en approve/reject para correlar logs.
|
||||
- **Eventos persistidos**:
|
||||
- `data/executions.jsonl` — un append por ejecución completada/fallida (no por step).
|
||||
- `data/violations.jsonl` — un append por violación (cualquier severidad).
|
||||
- **Métricas/Tracing OTel**: fuera de MVP. Documentado en `docs/futuro.md`.
|
||||
|
||||
## 13. Manejo de errores
|
||||
|
||||
| Tipo de fallo | Respuesta |
|
||||
|---|---|
|
||||
| Azure OpenAI 5xx / timeout | Retry exponencial (1s, 2s, 4s; 3 intentos). Si agota, fallback a `LLM_FALLBACK_PROVIDER` si está configurado. Si falla, `status=failed`, `error="llm_unavailable"`. La API devuelve `200` con la ejecución fallida (el dominio sí ha respondido). |
|
||||
| Guardrail validator excepción interna | Por defecto `fail_closed` → cuenta como `severity=block`, `blocked=True`. Configurable por policy. |
|
||||
| Pydantic validation falla en LLM output | 1 retry con prompt extendido `"Respond strictly with JSON matching this schema: ..."`. Si falla, `status=failed`, `error="output_schema_mismatch"`. |
|
||||
| Checkpoint SQLite corrupto | Log `error.checkpoint_corrupt`, devolver `500`, `error_class="checkpoint_unreadable"`. Sin auto-recovery (mejor falla explícita). |
|
||||
| Agente solicitado no existe | `404` con `{"detail":"agent 'X' not found"}`. |
|
||||
| `/approve` o `/reject` sobre estado no válido | `409` con `{"detail":"execution status 'X' does not allow approve"}`. |
|
||||
| Dashboard pierde conexión con core | `httpx` retry simple (2 intentos, 1s). Si sigue fallando, banner `st.error("Core API unreachable")`. |
|
||||
| Variable `.env` requerida ausente | `Pydantic Settings` falla en arranque con mensaje claro (no en runtime: el contenedor no debe estar "vivo y roto"). |
|
||||
|
||||
## 14. Testing
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── test_llm_mock.py
|
||||
│ ├── test_guardrails_ai.py
|
||||
│ ├── test_registry.py
|
||||
│ ├── test_runtime_state.py
|
||||
│ └── test_policy_loader.py
|
||||
├── integration/
|
||||
│ ├── test_invoke_happy_path.py
|
||||
│ ├── test_invoke_hitl.py
|
||||
│ ├── test_invoke_pii_block.py
|
||||
│ └── test_invoke_resume_after_restart.py
|
||||
└── fixtures/
|
||||
├── agents/incident_analyzer_test.yaml
|
||||
├── policies/test_policy.yaml
|
||||
└── inputs/
|
||||
```
|
||||
|
||||
- **Stack**: `pytest` + `pytest-asyncio` + `httpx.AsyncClient` (FastAPI TestClient) + `freezegun`.
|
||||
- **MockProvider** con lookup determinista (`hash(input) → respuesta`).
|
||||
- **Sin tests Streamlit** en MVP. Smoke manual en `docs/manual_qa.md`.
|
||||
- **CI** (`.github/workflows/ci.yml`): `ruff check` + `ruff format --check` + `mypy` + `pytest`.
|
||||
- **Cobertura objetivo** ~70% sobre `core/src/agentforge_core`.
|
||||
|
||||
## 15. Despliegue
|
||||
|
||||
- `docker-compose.yml` con dos servicios: `core` (uvicorn, `:8000`) y `dashboard` (Streamlit, `:8501`).
|
||||
- `Dockerfile.core` y `Dockerfile.dashboard` separados; cada uno con sus deps mínimas.
|
||||
- Healthchecks: `core` `GET /health`; `dashboard` `GET /` (Streamlit responde 200 cuando el server arranca).
|
||||
- Volume mount `./data:/app/data` y `./agents:/app/agents:ro`, `./policies:/app/policies:ro`.
|
||||
- `.env.example` con todas las variables documentadas y comentadas.
|
||||
- `LLM_PROVIDER=mock` por defecto → arranque sin claves.
|
||||
- Variables relevantes: `LLM_PROVIDER`, `LLM_FALLBACK_PROVIDER`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_DEPLOYMENT`, `OPENAI_API_KEY`, `GUARDRAILS_NEMO_ENABLED`, `LOG_LEVEL`, `DATA_DIR`.
|
||||
|
||||
## 16. README — estructura
|
||||
|
||||
1. **Qué es AgentForge** — Plataforma de gobernanza de agentes IA: catalogación, versionado de prompts y políticas, guardrails runtime, ejecución stateful con HITL, observabilidad.
|
||||
2. **Problema que resuelve** — Sin gobernanza, los agentes en producción son cajas negras (ver §2).
|
||||
3. **Diagrama de arquitectura** — Mermaid en línea + PNG renderizado en `docs/`.
|
||||
4. **Quickstart**: `cp .env.example .env && docker compose up`. Funciona out-of-the-box (`LLM_PROVIDER=mock`).
|
||||
5. **Demo guiada en 3 pasos**: 1) registro; 2) ejecuta `incident_analyzer` con `02_mos_degradation_pool_sbc.txt`; 3) aprueba el rollback en HITL.
|
||||
6. **Capacidades implementadas** — tabla `feature → ubicación en código`.
|
||||
7. **Roadmap** — link a `docs/futuro.md`.
|
||||
|
||||
## 17. Roadmap (`docs/futuro.md`)
|
||||
|
||||
- NeMo Guardrails con configuración rica (Colang KB).
|
||||
- Autenticación: OAuth2/OIDC con MSAL para entornos Azure AD.
|
||||
- OpenTelemetry: spans por nodo, métricas de violaciones por validador.
|
||||
- Multi-tenant: aislamiento por `tenant_id`, registry por tenant.
|
||||
- Evaluadores LLM-as-judge automatizados: regression suite que ejecuta cada agente sobre escenarios canónicos y mide deriva.
|
||||
- UI de aprobación con SLA, reasignación entre operadores, cola Kanban.
|
||||
- Persistencia migrable a Postgres + pgvector.
|
||||
|
||||
## 18. No-objetivos (out of scope MVP)
|
||||
|
||||
- Aprendizaje/fine-tuning de modelos.
|
||||
- Integración con sistemas reales de orquestación (RPA, Ansible, Kubernetes).
|
||||
- Inteligencia avanzada del agente (RAG, multi-step planning con tool use complejo).
|
||||
- Tests automatizados de UI Streamlit.
|
||||
- Soporte multi-idioma del dashboard.
|
||||
- High-availability del core (1 réplica suficiente para MVP).
|
||||
|
||||
## 19. Decisiones pospuestas
|
||||
|
||||
- **Postgres vs SQLite para checkpoints en multi-instance**: SQLite suficiente para MVP single-node; cambio a Postgres documentado en `docs/futuro.md`.
|
||||
- **NeMo full integration**: feature flag preparado, pero la configuración Colang completa es post-MVP.
|
||||
- **Métricas**: estructura de logging permite extracción posterior; no se entregan dashboards Grafana.
|
||||
|
||||
## 20. Referencias
|
||||
|
||||
- [LangGraph documentation](https://langchain-ai.github.io/langgraph/) — patrón checkpointer + interrupt.
|
||||
- [Guardrails AI](https://www.guardrailsai.com/) — validators hub.
|
||||
- [NVIDIA NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails) — Colang DSL.
|
||||
- [Microsoft Presidio](https://microsoft.github.io/presidio/) — PII detection.
|
||||
- [Pydantic v2](https://docs.pydantic.dev/latest/) — modelos y settings.
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) — API REST.
|
||||
- [Streamlit](https://streamlit.io/) — dashboard.
|
||||
- [structlog](https://www.structlog.org/) — logging estructurado.
|
||||
Reference in New Issue
Block a user