docs: añade docs/explicacion.md — guía didáctica del proyecto y sus módulos
Documento de alto nivel para terceros: las seis ideas clave, vista de los dos servicios y las capas del core, recorrido módulo a módulo con sus dependencias (quién depende de quién), el patrón Protocol+factory+Settings, el viaje completo de una petición (con diagrama de secuencia), la estrategia de persistencia (YAML/ JSON/JSONL/SQLite), glosario, mapa del repo y ruta de lectura. README enlaza a él. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -28,7 +28,10 @@ necesita antes de operar agentes con impacto real.
|
|||||||
└────────────────────────────────────────────────────────────────────┘
|
└────────────────────────────────────────────────────────────────────┘
|
||||||
```
|
```
|
||||||
|
|
||||||
Detalles completos en [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
Detalles completos en [`ARCHITECTURE.md`](ARCHITECTURE.md). Para una explicación
|
||||||
|
didáctica de extremo a extremo (conceptos, recorrido por todos los módulos y sus
|
||||||
|
interrelaciones, y el viaje de una petición de principio a fin), ver
|
||||||
|
[`docs/explicacion.md`](docs/explicacion.md).
|
||||||
|
|
||||||
## Quickstart
|
## Quickstart
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,540 @@
|
|||||||
|
# AgentForge explicado de principio a fin
|
||||||
|
|
||||||
|
> **Para quién es esto.** Una guía didáctica para alguien que llega nuevo al
|
||||||
|
> proyecto —técnico o no— y quiere entender *qué hace*, *por qué está hecho así*
|
||||||
|
> y *cómo encajan las piezas* sin tener que leer todo el código primero.
|
||||||
|
>
|
||||||
|
> Si solo quieres arrancarlo, ve al [`README.md`](../README.md). Si quieres las
|
||||||
|
> decisiones técnicas en bruto, ve a [`ARCHITECTURE.md`](../ARCHITECTURE.md).
|
||||||
|
> Este documento está en medio: cuenta la historia.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. El problema, en una frase
|
||||||
|
|
||||||
|
> Poner agentes de IA en producción **sin una capa de gobierno** produce sistemas
|
||||||
|
> opacos: prompts que cambian sin historial, validaciones inconsistentes, acciones
|
||||||
|
> de alto impacto sin supervisión y ninguna auditoría de lo que decidió el agente.
|
||||||
|
|
||||||
|
**AgentForge es el "plano de control" que pones *delante* de tus agentes** antes de
|
||||||
|
dejarlos tocar nada importante. No es un framework para *construir* agentes; es la
|
||||||
|
capa que los **cataloga, versiona, valida, ejecuta de forma supervisada y audita**.
|
||||||
|
|
||||||
|
El caso de ejemplo que trae el repo es un agente de operaciones de telco
|
||||||
|
(`incident_analyzer`): recibe la descripción de un incidente de plataforma de voz
|
||||||
|
(caída de registros SIP, degradación de MOS, saturación de HSS...) y propone
|
||||||
|
acciones con análisis de riesgo y plan de rollback. Acciones de riesgo alto quedan
|
||||||
|
**pausadas esperando aprobación humana**. Todo queda registrado.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Las seis ideas grandes
|
||||||
|
|
||||||
|
Si entiendes estas seis ideas, entiendes el proyecto. Todo lo demás son detalles.
|
||||||
|
|
||||||
|
| # | Idea | Dónde vive |
|
||||||
|
|---|------|------------|
|
||||||
|
| 1 | **Agentes y políticas como ficheros declarativos, versionados como Git.** Un agente es un YAML (prompt, modelo, esquema de salida, umbral de aprobación). Cambias el YAML → nueva versión, con hash y diff. Sin redeploy. | `agents/`, `policies/`, `registry/` |
|
||||||
|
| 2 | **Los guardrails son una *política*, no código disperso.** Una política lista validadores de entrada y de salida con su configuración. El motor los aplica; "qué se valida" es configuración. | `policies/`, `guardrails/` |
|
||||||
|
| 3 | **La ejecución del agente es un grafo de estados con checkpoints.** No es "llama al LLM y ya"; es un flujo: validar entrada → razonar → validar salida → proponer acciones → puerta de aprobación → finalizar. Cada paso se persiste. | `runtime/` (LangGraph) |
|
||||||
|
| 4 | **Human-in-the-Loop (HITL) de verdad.** Si el agente propone algo arriesgado, el grafo **se pausa** en mitad de la ejecución, el estado se guarda en disco, y se reanuda más tarde —incluso tras reiniciar el proceso— cuando un humano aprueba o rechaza. | nodo `approve_gate` + endpoints `/approve`, `/reject` |
|
||||||
|
| 5 | **Trazabilidad obligatoria.** Cada petición lleva un `trace_id` (UUID) que se propaga por el middleware → los logs → la API → los ficheros de auditoría. Cada paso del grafo deja una entrada en el `decision_path` con su duración. | `observability/`, `domain/execution.py` |
|
||||||
|
| 6 | **Todo lo "intercambiable" está detrás de una interfaz + un factory.** El proveedor de LLM, el motor de guardrails, el registry... son `Protocol`s con varias implementaciones. Un factory elige cuál según la configuración. Cambias `.env`, no el código. | `llm/`, `guardrails/`, `registry/` (los `factory.py`) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Vista de pájaro: dos servicios
|
||||||
|
|
||||||
|
AgentForge son **dos procesos** que se hablan por HTTP/JSON:
|
||||||
|
|
||||||
|
```
|
||||||
|
┌───────────────────────────── docker-compose ──────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────┐ HTTP/JSON ┌────────────────────────┐ │
|
||||||
|
│ │ agentforge-dashboard │ ───────────────► │ agentforge-core │ │
|
||||||
|
│ │ Streamlit :8501 │ ◄─────────────── │ FastAPI :8000 │ │
|
||||||
|
│ │ (la "consola") │ │ (el cerebro) │ │
|
||||||
|
│ └──────────────────────┘ └───────────┬────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────┬────────────────────┼───────────────┤
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ capa LLM capa Guardrails runtime LangGraph │
|
||||||
|
│ (Strategy+factory) (Strategy+factory) (grafo + checkpointer) │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────────┴──────────┬──────────┘ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Persistencia: YAML · JSON · JSONL · SQLite │
|
||||||
|
└────────────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`agentforge-core`** (FastAPI, puerto 8000) — todo el dominio: registry de
|
||||||
|
agentes, motor de guardrails, runtime de ejecución, persistencia. No tiene UI.
|
||||||
|
- **`agentforge-dashboard`** (Streamlit, puerto 8501) — una consola visual. **No
|
||||||
|
contiene lógica de negocio**: es un cliente HTTP del core con cinco páginas.
|
||||||
|
|
||||||
|
La separación importa: el core podría servir a una CLI, a otro servicio, a un
|
||||||
|
pipeline... el dashboard es solo una de las caras posibles.
|
||||||
|
|
||||||
|
### Las capas del core (de fuera hacia dentro)
|
||||||
|
|
||||||
|
```
|
||||||
|
HTTP ─► middlewares ─► routers (api/) ─► dependencias (deps.py)
|
||||||
|
│ │ ← aquí se inyectan los
|
||||||
|
│ │ objetos del dominio
|
||||||
|
▼ ▼
|
||||||
|
orchestrator ──► graph (LangGraph) ──► nodes
|
||||||
|
│ │
|
||||||
|
│ ├─► llm/ (¿qué dice el LLM?)
|
||||||
|
│ ├─► guardrails/ (¿pasa los filtros?)
|
||||||
|
│ └─► domain/ (¿qué forma tienen los datos?)
|
||||||
|
▼
|
||||||
|
checkpointer (SQLite) + persistence (JSONL) + registry (YAML/JSON)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Recorrido por los módulos (y quién depende de quién)
|
||||||
|
|
||||||
|
El código del core vive bajo `core/src/agentforge_core/`. Lo agrupo por capas, de
|
||||||
|
las más internas (sin dependencias) a las más externas.
|
||||||
|
|
||||||
|
### 4.1 `domain/` — el vocabulario del sistema
|
||||||
|
|
||||||
|
Modelos Pydantic puros. **No dependen de nada del proyecto**; todo lo demás depende
|
||||||
|
de ellos. Son el "idioma común".
|
||||||
|
|
||||||
|
| Fichero | Qué define |
|
||||||
|
|---------|------------|
|
||||||
|
| `agent.py` | `AgentDefinition` (prompt, modelo, `output_schema`, `guardrails`, `risk_threshold_for_hitl`, ...), `LLMConfig`, `AgentVersionMeta`. |
|
||||||
|
| `policy.py` | `PolicyDefinition` (listas de `PolicyValidator` de entrada y de salida, `on_validator_error`), `PolicyVersionMeta`. |
|
||||||
|
| `guardrail.py` | `GuardrailViolation` (trace_id, stage `input`/`output`, validator, severity, message, blocked). |
|
||||||
|
| `execution.py` | `AgentExecution` (el "expediente" de una ejecución: status, `decision_path`, `violations`, `proposed_actions`, `needs_human_for`, `final_output`, `error`), `ProposedAction`, `DecisionStep`, `AgentExecutionSummary`. |
|
||||||
|
|
||||||
|
> **Pista didáctica:** si quieres entender el sistema rápido, empieza leyendo
|
||||||
|
> `domain/execution.py`. Te dice exactamente qué información se produce y se guarda.
|
||||||
|
|
||||||
|
### 4.2 `config.py` y `observability/logging.py` — los cimientos transversales
|
||||||
|
|
||||||
|
- **`config.py` → `Settings`** (pydantic-settings): lee `.env` (proveedor LLM,
|
||||||
|
rutas de `agents/`/`policies/`/`data/`, nivel de log, claves de Azure/OpenAI,
|
||||||
|
flags de NeMo). Es el único sitio que sabe de variables de entorno. **Todos los
|
||||||
|
`factory.py` reciben un `Settings`.**
|
||||||
|
- **`observability/logging.py`**: configura `structlog` con salida JSON y un
|
||||||
|
contexto donde se "bind-ea" el `trace_id`. Lo usa todo el código que loguea
|
||||||
|
(`log = structlog.get_logger(__name__)`).
|
||||||
|
|
||||||
|
Dependen: de nada del proyecto. Dependen de ellos: prácticamente todo.
|
||||||
|
|
||||||
|
### 4.3 `llm/` — la capa de proveedores de LLM (Strategy pattern)
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---------|-----|
|
||||||
|
| `base.py` | El `Protocol` `LLMProvider` (`async complete(messages, temperature, max_tokens) -> CompletionResult`) y los tipos `Message`, `CompletionResult`. **La interfaz.** |
|
||||||
|
| `mock.py` | `MockProvider`: determinista, sin claves. Elige una respuesta canónica buscando subcadenas (`"sip"`, `"mos"`, `"hss"`) en el input. Es lo que hace que el demo funcione out-of-the-box. |
|
||||||
|
| `azure.py`, `openai.py` | Implementaciones reales (Azure OpenAI / OpenAI) con reintentos exponenciales y, opcionalmente, *fallback* entre proveedores. |
|
||||||
|
| `factory.py` | `build_llm_provider(settings)`: devuelve el provider según `LLM_PROVIDER` (`mock` / `azure` / `openai`), y envuelve un *fallback* opcional. |
|
||||||
|
|
||||||
|
Depende de: `domain/`, `config.py`. Dependen de él: el `runtime/` (el nodo
|
||||||
|
`llm_reason`) y el `deps.py` de la API.
|
||||||
|
|
||||||
|
### 4.4 `registry/` — catálogo y versionado de agentes y políticas
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---------|-----|
|
||||||
|
| `repository.py` | `FileSystemAgentRegistry`: lee `agents/<nombre>/index.yaml` + `versions/<id>.yaml`. CRUD de agentes y versiones, lectura de la versión "activa". |
|
||||||
|
| `policy_store.py` | `FileSystemPolicyStore`: lo mismo para `policies/`. Devuelve `PolicyDefinition`s. |
|
||||||
|
| `versioning.py` | `compute_hash(yaml_text)` (SHA-256 sobre el contenido normalizado) y el *diff unificado* entre dos versiones. Es la "magia tipo Git": versiones inmutables identificadas por hash, comparables. |
|
||||||
|
| `factory.py` | `build_registry(settings)` y `build_policy_store(settings)`. |
|
||||||
|
|
||||||
|
Depende de: `domain/`, `config.py`. Dependen de él: la API (`/agents`, `/policies`),
|
||||||
|
el `orchestrator` (para resolver qué agente/política ejecutó una traza dada).
|
||||||
|
|
||||||
|
### 4.5 `guardrails/` — el motor de validación
|
||||||
|
|
||||||
|
Aquí está el corazón del "gobierno". La estructura sigue otra vez **Protocol +
|
||||||
|
implementaciones + composite + factory**:
|
||||||
|
|
||||||
|
```
|
||||||
|
GuardrailEngine (Protocol) ← base.py: validate_input / validate_output
|
||||||
|
▲
|
||||||
|
┌────────────┼─────────────┐
|
||||||
|
│ │
|
||||||
|
GuardrailsAIEngine NeMoGuardrailsEngine ← guardrails_ai.py / nemo.py
|
||||||
|
(el real: aplica los (stub; desactivado
|
||||||
|
validadores de la por defecto)
|
||||||
|
política)
|
||||||
|
│
|
||||||
|
└──────────┐
|
||||||
|
▼
|
||||||
|
CompositeGuardrailEngine ← composite.py: corre N sub-engines
|
||||||
|
(corre los sub-engines en paralelo en paralelo (asyncio.gather) y
|
||||||
|
con asyncio y agrega las violaciones) une las listas de violaciones
|
||||||
|
|
||||||
|
factory.py: build_guardrail_engine(settings) → ensambla el Composite
|
||||||
|
validators.py: las funciones concretas — detect_pii, prompt_injection,
|
||||||
|
toxic_language, forbidden_topics, schema_match, pii_leakage,
|
||||||
|
forbidden_action_keywords, telco_safety_rules
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cómo se conecta una política con un validador:** la política
|
||||||
|
(`policies/default/versions/v1.yaml`) lista, por ejemplo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
input_validators:
|
||||||
|
- type: detect_pii
|
||||||
|
config: { entities: [EMAIL_ADDRESS, ES_NIF, IP_ADDRESS, IBAN_CODE], severity_on_match: block }
|
||||||
|
- type: prompt_injection
|
||||||
|
config: { severity_on_match: block }
|
||||||
|
...
|
||||||
|
on_validator_error: fail_closed
|
||||||
|
```
|
||||||
|
|
||||||
|
El `GuardrailsAIEngine` recorre esa lista, busca cada `type` en su registro de
|
||||||
|
funciones de `validators.py`, la llama con el texto y el `config`, y junta las
|
||||||
|
`GuardrailViolation` que devuelva. `on_validator_error: fail_closed` significa que
|
||||||
|
**si un validador peta, cuenta como bloqueo** (seguridad antes que disponibilidad).
|
||||||
|
|
||||||
|
> **Sobre `detect_pii`:** usa Presidio (con el modelo spaCy `en_core_web_sm`) si
|
||||||
|
> está instalado; si no, cae a una detección por regex (email, teléfono, NIF
|
||||||
|
> español, IP). Por eso el comportamiento en local (sin Presidio) y en Docker
|
||||||
|
> (con Presidio) puede diferir — la política solo pide *recognizers de patrón*
|
||||||
|
> fiables para evitar falsos positivos del NER.
|
||||||
|
|
||||||
|
Depende de: `domain/`, `config.py`. Dependen de él: el `runtime/` (los nodos
|
||||||
|
`validate_input`/`validate_output`) y el `deps.py`.
|
||||||
|
|
||||||
|
### 4.6 `runtime/` — la ejecución como grafo de estados (LangGraph)
|
||||||
|
|
||||||
|
Esta es la capa más "viva". Modela una ejecución del agente como un grafo dirigido.
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---------|-----|
|
||||||
|
| `state.py` | `AgentState`: un `TypedDict` con todo lo que fluye por el grafo (input, salida del LLM, acciones propuestas, violaciones acumuladas, `decision_path`, status, decisión humana, ...). Algunos campos usan reducers (`Annotated[list, operator.add]`) para que cada nodo *añada* en vez de sobrescribir. |
|
||||||
|
| `nodes.py` | Las **funciones-nodo**, construidas por *factories* parametrizadas con el agente, la política, el motor de guardrails y el provider LLM: `validate_input`, `llm_reason`, `validate_output`, `propose_actions`, `approve_gate`, `finalize`. Cada nodo añade un `DecisionStep` con su duración. |
|
||||||
|
| `graph.py` | `build_graph(...)`: cablea los nodos y las **aristas condicionales** (p. ej. si `validate_input` bloqueó → salta directo al final). Compila el grafo con un `checkpointer`. |
|
||||||
|
| `checkpointer.py` | `build_checkpointer(data_dir)`: un `AsyncSqliteSaver` de LangGraph sobre `data_dir/checkpoints.sqlite`, expuesto como *context manager* asíncrono. Es lo que hace que un `awaiting_approval` **sobreviva a un reinicio**. |
|
||||||
|
| `orchestrator.py` | `AgentOrchestrator`: la **única puerta de entrada** al runtime. Tres operaciones: `invoke()` (lanza), `resume()` (reanuda un HITL con la decisión), `snapshot()` (lee el estado actual sin avanzarlo). Cada llamada abre su propio checkpointer y traduce el `StateSnapshot` de LangGraph a un `AgentExecution` del dominio. |
|
||||||
|
|
||||||
|
**El grafo, dibujado:**
|
||||||
|
|
||||||
|
```
|
||||||
|
START
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ bloqueada (PII, injection...)
|
||||||
|
│ validate_input │ ─────────────────────────────────────────► END
|
||||||
|
└────────┬────────┘
|
||||||
|
│ ok
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ LLM no disponible / error
|
||||||
|
│ llm_reason │ ─────────────────────────────────────────► END
|
||||||
|
└────────┬────────┘
|
||||||
|
│ ok
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ salida no cumple el esquema / PII en la salida
|
||||||
|
│ validate_output │ ─────────────────────────────────────────► END
|
||||||
|
└────────┬────────┘
|
||||||
|
│ ok
|
||||||
|
▼
|
||||||
|
┌─────────────────┐
|
||||||
|
│ propose_actions │ (extrae las acciones propuestas del JSON del LLM)
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌─────────────────┐ ¿hay alguna acción con risk_score ≥ umbral
|
||||||
|
│ approve_gate │ o requires_approval=True?
|
||||||
|
└────────┬────────┘
|
||||||
|
│
|
||||||
|
┌────┴───────────────────────────────┐
|
||||||
|
│ no │ sí
|
||||||
|
▼ ▼
|
||||||
|
┌──────────┐ interrupt({...}) ──► el grafo SE PAUSA aquí.
|
||||||
|
│ finalize │ El estado queda en checkpoints.sqlite.
|
||||||
|
└────┬─────┘ Más tarde llega resume(decision={...})
|
||||||
|
│ y se reanuda en este mismo punto.
|
||||||
|
▼ │
|
||||||
|
END ▼
|
||||||
|
┌──────────┐
|
||||||
|
│ finalize │ (filtra a las acciones aprobadas;
|
||||||
|
└────┬─────┘ si fue rechazo → status=failed)
|
||||||
|
▼
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
Depende de: `llm/`, `guardrails/`, `domain/`, `config.py` (vía los objetos que le
|
||||||
|
inyectan). Dependen de él: la API (`api/executions.py` solo conoce el
|
||||||
|
`AgentOrchestrator`, no LangGraph).
|
||||||
|
|
||||||
|
### 4.7 `api/` — la fachada HTTP (FastAPI)
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---------|-----|
|
||||||
|
| `middlewares.py` | `TraceIdMiddleware`: lee `X-Trace-Id` de la petición (o genera uno), lo bind-ea al contexto de structlog, y lo devuelve en la respuesta. **Es el origen del hilo de trazabilidad.** |
|
||||||
|
| `deps.py` | Las **dependencias inyectables**. Cada `get_*` (`get_settings`, `get_registry`, `get_policy_store`, `get_llm_provider`, `get_guardrail_engine`, `get_orchestrator`) está cacheada con `lru_cache(maxsize=1)`: la app construye cada cosa **una sola vez**. Expone aliases (`RegistryDep = Annotated[..., Depends(get_registry)]`, etc.) que los routers piden por parámetro. Los tests llaman a `.cache_clear()` para reconstruir todo apuntando a un `DATA_DIR` temporal. |
|
||||||
|
| `persistence.py` | Helpers de **log append-only en JSONL**: `append_execution`, `append_violation`, `read_execution_summaries`. Inmutable, auditable, fácil de "shipear" a un sistema de logs. |
|
||||||
|
| `agents.py` | Router `/agents`: list, get, versiones, `GET /agents/{n}/versions/{a}/diff/{b}`. |
|
||||||
|
| `executions.py` | El más cargado: `POST /agents/{n}/invoke`, `GET /executions`, `GET /executions/{trace_id}`, `POST /executions/{trace_id}/approve`, `POST /executions/{trace_id}/reject`. Mantiene además `execution_index.json` (mapa `trace_id → agente/versión`) para poder reanudar tras un reinicio. |
|
||||||
|
| `policies.py`, `violations.py` | Routers `/policies` y `/violations` (este con filtros por severidad, stage, etc.). |
|
||||||
|
|
||||||
|
Y la **raíz de la app**: `core/src/agentforge_core/main.py` → `create_app()` instancia
|
||||||
|
`FastAPI`, añade el `TraceIdMiddleware`, monta los routers, y expone `/health`.
|
||||||
|
|
||||||
|
Depende de: todo lo de arriba (vía `deps.py`). Dependen de él: el dashboard (por
|
||||||
|
HTTP) y los tests de integración (`tests/integration/`, vía `TestClient`).
|
||||||
|
|
||||||
|
### 4.8 `dashboard/` — la consola Streamlit
|
||||||
|
|
||||||
|
| Fichero | Rol |
|
||||||
|
|---------|-----|
|
||||||
|
| `client.py` | `CoreClient`: un cliente HTTP síncrono (httpx, con reintentos) que envuelve **todos** los endpoints del core y mapea 404/409/422 a `{"error": ...}`. Es lo único que sabe hablar con el core. |
|
||||||
|
| `app.py` | La página raíz: sidebar de branding + un health-check del core. |
|
||||||
|
| `pages/1_🏛️_Registro.py` | Catálogo de agentes: detalle (prompt, esquema, LLM, guardrails), tabla de versiones, **diff coloreado v1↔v2**. |
|
||||||
|
| `pages/2_▶️_Ejecutar.py` | Lanza un agente: botones con los escenarios pregrabados, textarea, `invoke`, y render del status + output + violaciones + *timeline* del `decision_path`. Avisa si quedó en `awaiting_approval`. |
|
||||||
|
| `pages/3_🤝_Aprobaciones.py` | La cola de HITL: lista las ejecuciones `awaiting_approval`, muestra cada acción propuesta (risk_score coloreado, target, rollback_plan) con un checkbox, y aprueba el subconjunto elegido o rechaza con motivo. |
|
||||||
|
| `pages/4_📜_Historial.py` | Pestaña de ejecuciones (tabla + detalle por `trace_id`) y pestaña de violaciones (filtrable por severidad). |
|
||||||
|
| `pages/5_📐_Politicas.py` | Inventario de políticas: validadores de entrada/salida (cada uno expandible con su config) y versiones. |
|
||||||
|
| `components/` | Trozos reutilizables de UI: `diff_view` (pinta `+`/`-`/`@@`), `trace_view` (el timeline del `decision_path`), `violation_view` (badges de severidad). |
|
||||||
|
|
||||||
|
Depende de: el `agentforge-core` por HTTP (vía `AGENTFORGE_CORE_URL`). **Nadie del
|
||||||
|
core depende del dashboard.** No tiene tests unitarios (mal coste/beneficio para
|
||||||
|
Streamlit); su verificación es la checklist manual de [`docs/manual_qa.md`](manual_qa.md).
|
||||||
|
|
||||||
|
### 4.9 Lo que no es código: `agents/`, `policies/`, `data/`
|
||||||
|
|
||||||
|
- **`agents/incident_analyzer/`** — el agente de ejemplo: `index.yaml` (catálogo de
|
||||||
|
versiones), `versions/v1.yaml` y `v2.yaml`, y `examples/*.txt` (tres escenarios de
|
||||||
|
incidente de telco que el demo usa).
|
||||||
|
- **`policies/default/`** — la política de guardrails de ejemplo (`index.yaml` +
|
||||||
|
`versions/v1.yaml`).
|
||||||
|
- **`data/`** — estado *runtime* (gitignored): `checkpoints.sqlite`,
|
||||||
|
`executions.jsonl`, `violations.jsonl`, `execution_index.json`. Se crea sola.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. El patrón que se repite: `Protocol` + `factory` + `Settings`
|
||||||
|
|
||||||
|
Tres veces (LLM, guardrails, registry) verás la misma estructura:
|
||||||
|
|
||||||
|
```
|
||||||
|
base.py → un Protocol (la interfaz: "qué se puede hacer")
|
||||||
|
<impl_a>.py → una implementación (p. ej. mock)
|
||||||
|
<impl_b>.py → otra implementación (p. ej. azure)
|
||||||
|
factory.py → build_X(settings: Settings) -> X ← elige y monta
|
||||||
|
```
|
||||||
|
|
||||||
|
¿Por qué? Porque permite **cambiar el comportamiento sin tocar el código**: pones
|
||||||
|
`LLM_PROVIDER=azure` en `.env` y el factory te da el provider de Azure; pones
|
||||||
|
`mock` y tienes un demo determinista sin claves. Lo mismo con guardrails (puedes
|
||||||
|
añadir el engine de NeMo) y con el registry (hoy es de ficheros; mañana podría ser
|
||||||
|
de base de datos). Es el principio de **"configuración antes que código"**.
|
||||||
|
|
||||||
|
Y todo se enchufa **una sola vez** al arrancar, en `api/deps.py` (los `lru_cache`):
|
||||||
|
el registry, el policy store, el provider, el motor de guardrails y el orchestrator
|
||||||
|
son singletons del proceso. Los routers solo los *piden*; no saben construirlos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. El recorrido de UNA petición, de principio a fin
|
||||||
|
|
||||||
|
Esta es la sección que conviene leer despacio: aquí se ve cómo encaja todo. Sigamos
|
||||||
|
`POST /agents/incident_analyzer/invoke` con el escenario SIP (el que dispara HITL).
|
||||||
|
|
||||||
|
### Acto 1 — la petición entra y se ejecuta el grafo
|
||||||
|
|
||||||
|
```
|
||||||
|
Cliente (dashboard o curl)
|
||||||
|
│ POST /agents/incident_analyzer/invoke { "input": "<texto del incidente SIP>" }
|
||||||
|
▼
|
||||||
|
TraceIdMiddleware ............ genera trace_id = UUID, lo bind-ea al log
|
||||||
|
▼
|
||||||
|
router invoke_agent (api/executions.py)
|
||||||
|
│ pide por inyección: RegistryDep, PolicyStoreDep, OrchestratorDep, SettingsDep
|
||||||
|
│ registry.get_agent("incident_analyzer") → AgentDefinition (versión activa = v2)
|
||||||
|
│ policies.get_policy(agent.guardrails[0]) → PolicyDefinition ("default")
|
||||||
|
▼
|
||||||
|
orchestrator.invoke(agent_def, policy, user_input)
|
||||||
|
│ abre el checkpointer (AsyncSqliteSaver sobre data/checkpoints.sqlite)
|
||||||
|
│ build_graph(agent_def, policy, provider, engine, checkpointer)
|
||||||
|
│ graph.ainvoke(estado_inicial, config={thread_id: trace_id})
|
||||||
|
▼
|
||||||
|
┌──── el grafo ────────────────────────────────────────────────────────────┐
|
||||||
|
│ validate_input → engine.validate_input(texto, policy, trace_id) │
|
||||||
|
│ recorre los input_validators de la política en paralelo │
|
||||||
|
│ (detect_pii, prompt_injection, ...) → 0 violaciones │
|
||||||
|
│ añade DecisionStep("validate_input", ...) │
|
||||||
|
│ llm_reason → provider.complete([system_prompt, user_input], ...) │
|
||||||
|
│ (MockProvider ve "sip" → respuesta canónica SIP) │
|
||||||
|
│ añade DecisionStep("llm_reason", model, tokens, ...) │
|
||||||
|
│ validate_output → parsea el JSON del LLM; engine.validate_output(...) │
|
||||||
|
│ (schema_match, pii_leakage, forbidden_action_keywords, │
|
||||||
|
│ telco_safety_rules) → 0 violaciones │
|
||||||
|
│ propose_actions → extrae proposed_actions del JSON: [act-1 (risk 4, │
|
||||||
|
│ requires_approval), act-2 (risk 3)] │
|
||||||
|
│ approve_gate → ¿alguna acción con risk ≥ 4 (umbral del agente) o │
|
||||||
|
│ requires_approval? SÍ (act-1) → interrupt({...}) │
|
||||||
|
│ ⇒ el grafo SE DETIENE. El estado se escribe en SQLite. │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
orchestrator._snapshot(...) → LangGraph reporta "hay un nodo pendiente"
|
||||||
|
⇒ status = "awaiting_approval"
|
||||||
|
⇒ needs_human_for = [act-1] (y devuelve un AgentExecution)
|
||||||
|
▼
|
||||||
|
router: status no es terminal → NO se escribe en executions.jsonl,
|
||||||
|
pero SÍ se registra en execution_index.json: { trace_id → (incident_analyzer, v2) }
|
||||||
|
▼
|
||||||
|
respuesta 200 { status: "awaiting_approval", trace_id, needs_human_for: [act-1], decision_path: [...], ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
En el dashboard, la página **Ejecutar** muestra el timeline y un aviso "ve a
|
||||||
|
Aprobaciones". La página **Aprobaciones** hace `GET /executions`, encuentra esta
|
||||||
|
ejecución (el endpoint reconstruye las `awaiting_approval` desde el índice + el
|
||||||
|
checkpointer) y muestra `act-1` con su risk_score, target y rollback_plan.
|
||||||
|
|
||||||
|
### Acto 2 — el humano decide; el grafo se reanuda
|
||||||
|
|
||||||
|
```
|
||||||
|
Humano (en la página Aprobaciones, o curl)
|
||||||
|
│ POST /executions/<trace_id>/approve { approved_action_ids: ["act-1"], comment: "ok rollback" }
|
||||||
|
▼
|
||||||
|
router approve_execution (api/executions.py)
|
||||||
|
│ _resolve(...) → lee execution_index.json → sabe que fue (incident_analyzer, v2)
|
||||||
|
│ reconstruye el AgentDefinition y la PolicyDefinition
|
||||||
|
│ _ensure_awaiting(...) → orchestrator.snapshot(...) confirma que sigue en awaiting_approval
|
||||||
|
▼
|
||||||
|
orchestrator.resume(agent_def, policy, trace_id, decision={approved_action_ids:["act-1"], rejected:false})
|
||||||
|
│ abre OTRA VEZ el checkpointer (mismo data_dir) — el estado pausado sigue ahí,
|
||||||
|
│ aunque hubiera habido un reinicio del proceso entremedias
|
||||||
|
│ graph.ainvoke(Command(resume=decision), config={thread_id: trace_id})
|
||||||
|
▼
|
||||||
|
┌──── el grafo continúa desde donde se quedó ──────────────────────────────┐
|
||||||
|
│ approve_gate → el interrupt() devuelve la `decision` del humano │
|
||||||
|
│ finalize → final_actions = acciones cuyo id está aprobado = [act-1] │
|
||||||
|
│ final_output = { ...salida del LLM..., approved_actions:[act-1] }│
|
||||||
|
│ status = "completed" │
|
||||||
|
└──────────────────────────────────────────────────────────────────────────┘
|
||||||
|
▼
|
||||||
|
router: status terminal → append_execution(...) escribe el AgentExecution en executions.jsonl
|
||||||
|
▼
|
||||||
|
respuesta 200 { status: "completed", final_output: { ..., approved_actions: [act-1] }, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
(Si en vez de `approve` se llama a `reject`, el nodo `finalize` ve `rejected: true` →
|
||||||
|
`status = "failed"`, `error = "rejected_by_human"`. También terminal → al JSONL.)
|
||||||
|
|
||||||
|
### El mismo recorrido, como diagrama de secuencia
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant U as Cliente / Dashboard
|
||||||
|
participant MW as TraceIdMiddleware
|
||||||
|
participant API as router (api/executions.py)
|
||||||
|
participant ORCH as AgentOrchestrator
|
||||||
|
participant G as Grafo LangGraph
|
||||||
|
participant CP as checkpoints.sqlite
|
||||||
|
participant LOG as executions.jsonl / index.json
|
||||||
|
|
||||||
|
U->>MW: POST /agents/incident_analyzer/invoke {input}
|
||||||
|
MW->>API: + trace_id
|
||||||
|
API->>API: registry.get_agent · policies.get_policy
|
||||||
|
API->>ORCH: invoke(agent, policy, input)
|
||||||
|
ORCH->>G: ainvoke(estado, thread_id=trace_id)
|
||||||
|
G->>G: validate_input → llm_reason → validate_output → propose_actions
|
||||||
|
G->>G: approve_gate: hay riesgo alto → interrupt()
|
||||||
|
G->>CP: persiste estado pausado
|
||||||
|
ORCH-->>API: AgentExecution(status=awaiting_approval, needs_human_for=[act-1])
|
||||||
|
API->>LOG: index.json[trace_id] = (incident_analyzer, v2)
|
||||||
|
API-->>U: 200 {status: awaiting_approval, trace_id, ...}
|
||||||
|
|
||||||
|
Note over U,CP: ...más tarde (incluso tras reiniciar el core)...
|
||||||
|
|
||||||
|
U->>API: POST /executions/{trace_id}/approve {approved_action_ids:[act-1]}
|
||||||
|
API->>LOG: lee index.json → (incident_analyzer, v2)
|
||||||
|
API->>ORCH: resume(agent, policy, trace_id, decision)
|
||||||
|
ORCH->>CP: reabre checkpointer (estado pausado sigue ahí)
|
||||||
|
ORCH->>G: ainvoke(Command(resume=decision), thread_id=trace_id)
|
||||||
|
G->>G: approve_gate (recibe decisión) → finalize → status=completed
|
||||||
|
ORCH-->>API: AgentExecution(status=completed, final_output)
|
||||||
|
API->>LOG: append a executions.jsonl
|
||||||
|
API-->>U: 200 {status: completed, final_output, ...}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Persistencia: cuatro formas, cuatro razones
|
||||||
|
|
||||||
|
AgentForge no usa una sola base de datos; usa la herramienta adecuada para cada cosa.
|
||||||
|
|
||||||
|
| Qué | Cómo | Por qué así |
|
||||||
|
|-----|------|-------------|
|
||||||
|
| Definiciones de agentes y políticas | **YAML** por versión + `index.yaml` | Lo escribe un humano (legible, comentable) y lo cataloga la máquina. |
|
||||||
|
| Identidad de versiones / comparación | **hash SHA-256** del contenido + `difflib` | Versiones inmutables identificables y comparables — "tipo Git". |
|
||||||
|
| Mapa `trace_id → agente/versión` | **JSON** (`execution_index.json`) | Necesario para reanudar un HITL sabiendo qué configuración lo ejecutó; debe sobrevivir a reinicios. |
|
||||||
|
| Log de ejecuciones terminales y de violaciones | **JSONL append-only** | Inmutable, auditable, trivial de "shipear" a un sistema de logs. |
|
||||||
|
| Estado intermedio del grafo (incl. pausas HITL) | **SQLite** (`checkpoints.sqlite`, vía LangGraph) | Es lo que LangGraph espera; permite reanudar una ejecución pausada entre reinicios del proceso. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Glosario
|
||||||
|
|
||||||
|
| Término | Significado en este proyecto |
|
||||||
|
|---------|------------------------------|
|
||||||
|
| **Agente** | Una configuración declarativa (YAML): prompt de sistema, modelo LLM, esquema de salida, lista de políticas de guardrails, umbral de riesgo para HITL. No es código. |
|
||||||
|
| **Política (de guardrails)** | Un YAML con la lista de validadores de entrada y de salida y su configuración, más `on_validator_error` (`fail_closed`/`fail_open`). |
|
||||||
|
| **Validador / guardrail** | Una función que mira el texto de entrada o la salida del LLM y devuelve cero o más `GuardrailViolation`. Ej.: `detect_pii`, `prompt_injection`, `schema_match`, `telco_safety_rules`. |
|
||||||
|
| **Violación** | El resultado de un validador: `stage` (input/output), `validator`, `severity` (`info`/`warning`/`block`), `message`, `blocked`. |
|
||||||
|
| **Ejecución (`AgentExecution`)** | El "expediente" de una invocación: `trace_id`, status, `decision_path`, `violations`, `proposed_actions`, `needs_human_for`, `final_output`, `error`. |
|
||||||
|
| **`decision_path`** | La lista de pasos por los que pasó el grafo, cada uno con su `step`, `timestamp`, `duration_ms` y un `detail`. La "caja negra" de la ejecución. |
|
||||||
|
| **HITL (Human-in-the-Loop)** | El patrón de pausar la ejecución cuando hay una acción arriesgada y esperar a que un humano apruebe o rechace. Implementado con `interrupt()` de LangGraph. |
|
||||||
|
| **Checkpointer** | El componente de LangGraph que persiste el estado del grafo (aquí, en SQLite). Lo que hace posible reanudar un HITL tras un reinicio. |
|
||||||
|
| **`trace_id`** | UUID que identifica una petición/ejecución y se propaga por middleware → logs → API → ficheros de auditoría. |
|
||||||
|
| **Orchestrator** | La única clase que habla con LangGraph (`invoke`/`resume`/`snapshot`); aísla al resto del sistema del runtime. |
|
||||||
|
| **Factory** | Función `build_X(settings)` que monta la implementación de `X` adecuada según la configuración. |
|
||||||
|
| **Status de ejecución** | `running`, `awaiting_approval` (pausada en HITL), `blocked_by_guardrail` (la frenó un validador), `completed`, `failed`. |
|
||||||
|
| **Estados de un agente** | `draft`, `active`, `deprecated` (metadato en su YAML). |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Mapa del repositorio y por dónde empezar a leer
|
||||||
|
|
||||||
|
```
|
||||||
|
agentforge/
|
||||||
|
├── core/
|
||||||
|
│ ├── src/agentforge_core/
|
||||||
|
│ │ ├── domain/ ← los modelos de datos (empieza por execution.py)
|
||||||
|
│ │ ├── config.py · observability/ ← cimientos transversales
|
||||||
|
│ │ ├── llm/ ← proveedores LLM (Protocol + impls + factory)
|
||||||
|
│ │ ├── registry/ ← catálogo y versionado (YAML/JSON, hash, diff)
|
||||||
|
│ │ ├── guardrails/ ← motor de validación (Protocol + composite + validators)
|
||||||
|
│ │ ├── runtime/ ← el grafo LangGraph (state, nodes, graph, checkpointer, orchestrator)
|
||||||
|
│ │ ├── api/ ← FastAPI (middlewares, deps, routers, persistence)
|
||||||
|
│ │ └── main.py ← create_app(): ensambla la app
|
||||||
|
│ ├── Dockerfile · requirements.txt
|
||||||
|
├── dashboard/
|
||||||
|
│ ├── src/agentforge_dashboard/ ← Streamlit (client + app + pages + components)
|
||||||
|
│ ├── Dockerfile · requirements.txt
|
||||||
|
├── agents/incident_analyzer/ ← el agente de ejemplo (YAMLs + escenarios .txt)
|
||||||
|
├── policies/default/ ← la política de guardrails de ejemplo
|
||||||
|
├── data/ ← estado runtime (gitignored)
|
||||||
|
├── tests/ ← pytest: tests/unit/ y tests/integration/ (88 tests en total; el demo Streamlit no se testea con unit tests)
|
||||||
|
├── docs/ ← este documento, manual_qa.md, futuro.md, superpowers/
|
||||||
|
├── docker-compose.yml ← levanta core + dashboard
|
||||||
|
├── Makefile ← install / test / test-all / lint / smoke / up / down
|
||||||
|
├── README.md · ARCHITECTURE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ruta de lectura sugerida (1 hora):**
|
||||||
|
1. `domain/execution.py` y `domain/agent.py` — qué datos hay.
|
||||||
|
2. `policies/default/versions/v1.yaml` y `agents/incident_analyzer/versions/v2.yaml` — cómo se declara todo.
|
||||||
|
3. `runtime/nodes.py` y `runtime/graph.py` — el flujo de ejecución.
|
||||||
|
4. `api/executions.py` — cómo se expone (invoke / approve / reject).
|
||||||
|
5. `tests/integration/test_invoke_hitl.py` — el ciclo completo, en ~40 líneas.
|
||||||
|
6. Levanta `docker compose up` y haz clic por las cinco páginas del dashboard.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Lo que aún no hace (a propósito)
|
||||||
|
|
||||||
|
Es un MVP. La detección de PII más fina (modelos grandes, español), autenticación,
|
||||||
|
OpenTelemetry, multi-tenant, persistencia en Postgres, evaluadores LLM-as-judge,
|
||||||
|
una cola de aprobaciones con SLA... están en el roadmap: [`docs/futuro.md`](futuro.md).
|
||||||
|
El estado actual y cómo verificarlo: [`README.md`](../README.md) y [`docs/manual_qa.md`](manual_qa.md).
|
||||||
Reference in New Issue
Block a user