Files
agentforge/docs/superpowers/specs/2026-05-09-agentforge-design.md
JuanandClaude Opus 4.7 b471fa454c 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>
2026-05-09 20:08:59 +02:00

632 lines
31 KiB
Markdown

# 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.