commit b471fa454c43d1809a3eff8e1692a3da9301ea3b Author: Juan Date: Sat May 9 20:08:59 2026 +0200 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3162b3b --- /dev/null +++ b/.gitignore @@ -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 diff --git a/docs/superpowers/specs/2026-05-09-agentforge-design.md b/docs/superpowers/specs/2026-05-09-agentforge-design.md new file mode 100644 index 0000000..b00feaa --- /dev/null +++ b/docs/superpowers/specs/2026-05-09-agentforge-design.md @@ -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//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//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//`.** + +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.