🛡️ AgentForge — Walkthrough
+Catalogación y versionado de agentes y políticas, guardrails en runtime, ejecución stateful con Human-in-the-Loop, observabilidad y trazabilidad de extremo a extremo. Aquí está todo: de la vista de pájaro al cableado de cada módulo, con diagramas.
+00 Qué es y por qué
+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.
+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. Las acciones de riesgo alto quedan pausadas esperando aprobación humana. Todo queda registrado con un trace_id.
Este documento va de lo general a lo concreto: primero las ideas y la forma del sistema, luego cada módulo y sus interrelaciones, y por último el cableado de bajo nivel (grafo, persistencia, API). Si solo quieres arrancarlo, ve al README.md; si quieres la narrativa completa, docs/explicacion.md; si quieres firmas exactas y cadenas de llamada, docs/componentes.md.
01 Las seis ideas grandes
+Si entiendes estas seis ideas, entiendes el proyecto. Todo lo demás son detalles de implementación.
+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 SHA-256 y diff legible. Sin redeploy.
+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, no código.
+La ejecución del agente es un grafo de estados con checkpoints
+No es "llama al LLM y ya": validar entrada → razonar → validar salida → proponer acciones → puerta de aprobación → finalizar. Cada paso se persiste.
+Human-in-the-Loop de verdad
+Si el agente propone algo arriesgado, el grafo se pausa a mitad de 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.
+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.
Todo lo "intercambiable" está detrás de una interfaz + un factory
+El proveedor de LLM, el motor de guardrails, el registry… son Protocols con varias implementaciones. Un factory elige cuál según la configuración. Cambias .env, no el código.
02 Vista de pájaro: dos servicios
+AgentForge son dos procesos que se hablan por HTTP/JSON, levantados por docker-compose:
-
+
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 con cinco páginas. No contiene lógica de negocio: es un cliente HTTP del core.
+
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. (Y este documento es otra cara más: el HTML que estás leyendo.)
+ +03 Las capas del core, de fuera hacia dentro
+Una petición HTTP atraviesa estas capas en orden. Cada una solo conoce a la de dentro; nada de dentro conoce a las de fuera (eso es lo que mantiene el sistema desacoplado y testeable).
+ +Solo api/ importa de runtime.orchestrator (y deps.py lo construye). El resto de api/ no toca LangGraph; runtime/ no toca api/. Solo los factory.py y main.py conocen Settings. domain/ no importa nada del proyecto.
04 El grafo de módulos (quién depende de quién)
+El código del core vive bajo core/src/agentforge_core/. Es un DAG: las capas de abajo no importan nada de las de arriba. El "nivel" es la profundidad topológica. Lee de abajo hacia arriba: el vocabulario primero, la composición de la app al final.
create_app(): instancia FastAPI, añade TraceIdMiddleware, expone /health, monta los 5 routers. Construye Settings() para el logging.
+ deps.py construye y cachea los 5 objetos del dominio (@lru_cache); los routers solo los piden por parámetro.
+ StateSnapshot de LangGraph a un AgentExecution. Único punto de entrada al runtime.
+ CompositeGuardrailEngine.
+ composite no conoce a sus sub-engines (solo el Protocol).
+ domain/ es el idioma común; todo lo demás depende de él.
+ El paquete dashboard/ no aparece aquí: no comparte código con el core, solo lo llama por HTTP.
05 El patrón que se repite: Protocol + factory + Settings
+ Tres veces (LLM, guardrails, registry) verás la misma estructura. 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. Es el principio de "configuración antes que código".
Protocol → N implementaciones → un factory que elige según Settings). Derecha: las tres instancias concretas. Todo se enchufa una sola vez al arrancar, en api/deps.py (los @lru_cache).06 El dominio — el vocabulario del sistema
+Modelos Pydantic puros bajo domain/. No dependen de nada del proyecto; todo lo demás depende de ellos. Si quieres entender el sistema rápido, empieza leyendo domain/execution.py: te dice exactamente qué información se produce y se guarda.
AgentDefinition referencia políticas por nombre; el AgentRegistry resuelve la versión activa vía index.yaml. AgentExecution es el expediente que se acumula durante la ejecución y se persiste al terminar; sus violaciones se escriben además, una a una, en violations.jsonl.| Fichero | Qué define |
|---|---|
domain/agent.py | AgentDefinition (prompt, modelo, output_schema, guardrails, risk_threshold_for_hitl…), LLMConfig, AgentVersionMeta. |
domain/policy.py | PolicyDefinition (listas de PolicyValidator de entrada y salida, on_validator_error), PolicyVersionMeta. |
domain/guardrail.py | GuardrailViolation (trace_id, stage input/output, validator, severity, message, blocked). |
domain/execution.py | AgentExecution (status, decision_path, violations, proposed_actions, needs_human_for, final_output, error), ProposedAction, DecisionStep, AgentExecutionSummary. |
07 Guardrails — el motor de validación
+Aquí está el corazón del "gobierno". La estructura sigue otra vez Protocol + implementaciones + composite + factory. Una política lista qué validar; el motor lo aplica. Por defecto, fail-closed: si un validador peta, cuenta como bloqueo (seguridad antes que disponibilidad).
+ +GuardrailsAIEngine recorre esa lista, busca cada type en su registro de funciones de validators.py, las llama con (payload, config, trace_id, kind), y junta las GuardrailViolation que devuelvan. CompositeGuardrailEngine corre los sub-engines en paralelo y agrega.Los validadores concretos (guardrails/validators.py)
+ | Validador | Etapa | Qué comprueba | Severidad típica |
|---|---|---|---|
detect_pii | input | PII vía Presidio (modelo spaCy en_core_web_sm) o, si no está, regex de EMAIL / PHONE / ES_NIF / IP. La política default pide solo recognizers de patrón fiables (EMAIL_ADDRESS, ES_NIF, IP_ADDRESS, IBAN_CODE) para evitar falsos positivos del NER. | block |
prompt_injection | input | Heurísticas de patrones conocidos ("ignore previous instructions", "you are now", "reveal the system prompt"…). | block |
toxic_language | input | Lista negra con umbral (MVP). | warning |
forbidden_topics | input | Coincidencia de substring con temas prohibidos (instrucciones de explotación, credenciales, código malicioso). | block |
schema_match | output | Valida el JSON del LLM contra un JSON Schema (severity, root_cause_hypothesis, proposed_actions con id/action/target/risk_score/rollback_plan/requires_approval). | block |
pii_leakage | output | Re-aplica detect_pii sobre la salida serializada. | block |
forbidden_action_keywords | output | Que las acciones propuestas no contengan comandos peligrosos (DROP TABLE, rm -rf, shutdown -h now, delete production, format c:). | block |
telco_safety_rules | output | Reglas declarativas: nunca acción sobre prod sin rollback; nunca acción masiva (all/todos/*) sin canary en el plan. | block |
Así se conecta una política con un validador (extracto de policies/default/versions/v1.yaml):
name: default +version: v1 +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 } + - type: toxic_language + config: { threshold: 0.7, severity_on_match: warning } + - type: forbidden_topics + config: { topics: ["instrucciones de explotación", "credenciales", "código malicioso"], severity_on_match: block } +output_validators: + - type: schema_match # valida el JSON del LLM contra un JSON Schema + config: { severity_on_mismatch: block, schema: { … } } + - type: pii_leakage + - type: forbidden_action_keywords + config: { keywords: ["DROP TABLE", "rm -rf", "shutdown -h now", "delete production", "format c:"] } + - type: telco_safety_rules + config: { rules: [never_propose_action_targeting_production_without_rollback, never_propose_mass_action_without_canary] } +on_validator_error: fail_closed # si un validador lanza una excepción → cuenta como bloqueo+
08 El grafo de ejecución (LangGraph)
+Una ejecución del agente se modela como un grafo dirigido de estados con checkpoints. No es "llama al LLM y ya": es un flujo con aristas condicionales y una pausa real para Human-in-the-Loop. Cada nodo añade un DecisionStep con su duración al decision_path.
START → validate_input → llm_reason → validate_output → propose_actions → approve_gate → finalize → END. Salidas tempranas (rojo) cuando un guardrail bloquea o algo falla. approve_gate bifurca: si no hay riesgo, va directo a finalize; si lo hay, llama a interrupt() y el grafo se detiene, con el estado en SQLite, hasta que llega un resume(decision) — que puede ocurrir tras un reinicio del proceso.Los nodos (runtime/nodes.py) y el estado (runtime/state.py)
+ | Nodo (factory) | Hace | Puede saltar a |
|---|---|---|
validate_inputbuild_node_validate_input(engine, policy) | Corre los input_validators de la política sobre el texto del usuario; acumula violaciones. | END si alguna blocked → status = blocked_by_guardrail |
llm_reasonbuild_node_llm_reason(provider, agent_def) | provider.complete([Message("system", system_prompt), Message("user", input)], temperature, max_tokens). Guarda raw_llm_output y un step con modelo/tokens/latencia. | END si el provider lanza → status = failed, error = llm_unavailable |
validate_outputbuild_node_validate_output(engine, policy) | json.loads del output; corre los output_validators (schema_match, pii_leakage, …). | END si no parsea (output_schema_mismatch) o hay blocked |
propose_actionsbuild_node_propose_actions() | Extrae parsed_output["proposed_actions"] al estado. | siempre → approve_gate |
approve_gatebuild_node_approve_gate(agent_def) | Filtra acciones arriesgadas (risk_score ≥ risk_threshold_for_hitl o requires_approval). Si las hay → interrupt({"awaiting_actions": risky}) (pausa). Al reanudar, recibe la decision humana. | siempre → finalize (tras la pausa, si la hubo) |
finalizebuild_node_finalize() | Si human_decision.rejected → status = failed, error = rejected_by_human. Si no → final_output = {**parsed, "approved_actions": , status = completed. | END |
El estado que fluye por el grafo es un TypedDict. decision_path usa un reducer (Annotated[list, operator.add]) para que cada nodo añada pasos en vez de sobrescribir:
class AgentState(TypedDict, total=False): + 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] # ← reducer: cada nodo AÑADE pasos + status: str; error: str | None + human_decision: dict | None; final_output: dict | None+
Mirar dentro: el cableado del grafo y el nodo que pausa
+El grafo se cablea con aristas fijas y condicionales (runtime/graph.py): tras validate_input, si status == "blocked_by_guardrail" → END; tras llm_reason, si status == "failed" → END; etc. propose_actions → approve_gate → finalize → END son aristas fijas.
g.add_edge(START, "validate_input") + +def _after_validate_input(state): + return END if state.get("status") == "blocked_by_guardrail" else "llm_reason" +g.add_conditional_edges("validate_input", _after_validate_input, {END: END, "llm_reason": "llm_reason"}) + +# … _after_llm → END si status=="failed" · _after_validate_output → END si status in {blocked, failed} … +g.add_edge("propose_actions", "approve_gate") +g.add_edge("approve_gate", "finalize") +g.add_edge("finalize", END) +return g.compile(checkpointer=checkpointer)+
def build_node_approve_gate(agent_def): + async def approve_gate(state): + actions = state.get("proposed_actions", []) + risky = [a for a in actions + if int(a.get("risk_score", 1)) >= agent_def.risk_threshold_for_hitl + or bool(a.get("requires_approval"))] + if not risky: + return {"decision_path": [_step("approve_gate", started, hitl=False)]} + decision = interrupt({"awaiting_actions": risky}) # ← pausa; reanuda con Command(resume=decision) + return {"human_decision": decision, + "decision_path": [_step("approve_gate", started, hitl=True, resumed=True)]} + return approve_gate+
El AgentOrchestrator abre su propio AsyncSqliteSaver en cada operación (invoke / resume / snapshot) sobre data_dir/checkpoints.sqlite — por eso un awaiting_approval sobrevive a un reinicio: basta crear otro orchestrator apuntando al mismo data_dir y llamar a resume. El orchestrator traduce el StateSnapshot de LangGraph a un AgentExecution: si state.next (hay un nodo pendiente) y el status no es terminal ⇒ es el interrupt() ⇒ status = "awaiting_approval" y needs_human_for = las acciones arriesgadas.
09 El viaje de UNA petición, de principio a fin
+Esta es la sección que conviene leer despacio: aquí se ve cómo encaja todo. Seguimos POST /agents/incident_analyzer/invoke con el escenario SIP — el que dispara HITL — y luego la aprobación.
trace_id, el router resuelve agente+política, el orchestrator lanza el grafo, éste recorre cuatro nodos y se pausa en approve_gate con un interrupt() — el estado va a SQLite, se anota el índice y se responde awaiting_approval (sin escribir aún en el log append-only). Acto 2 (puede ser tras un reinicio): llega el approve, el router reconstruye la config desde el índice, el orchestrator reabre el checkpointer y reanuda con Command(resume=...); el grafo termina, y como el status ya es terminal se escribe en executions.jsonl. Si en vez de approve llega reject → finalize ve rejected: true → status = failed, error = rejected_by_human.El mismo recorrido, con curl (provider mock, sin claves):
# 1) Invocar — el escenario SIP dispara HITL +TRACE=$(curl -s localhost:8000/agents/incident_analyzer/invoke \ + -H 'content-type: application/json' \ + -d '{"input": "Caída de registros SIP tras desplegar la imagen 4.7.2 en CSCF aravaca-01"}' \ + | python -c 'import sys,json; print(json.load(sys.stdin)["trace_id"])') +# → status: "awaiting_approval", needs_human_for: [act-1] + +# 2) Revisar la cola HITL +curl -s localhost:8000/executions | python -m json.tool # incluye las awaiting_approval reconstruidas del índice + checkpointer + +# 3) Aprobar la acción segura act-1 +curl -s localhost:8000/executions/$TRACE/approve \ + -H 'content-type: application/json' \ + -d '{"approved_action_ids": ["act-1"], "comment": "rollback ok"}' +# → status: "completed", final_output: { ..., "approved_actions": [act-1] } · ya escrito en executions.jsonl + +# (el dashboard hace exactamente esto desde las páginas "Ejecutar" y "Aprobaciones")+
10 Versionado tipo Git
+Un agente no es una fila en una BD: es una carpeta con un index.yaml (el "catálogo de commits") y versions/v1.yaml, versions/v2.yaml… (los "commits"). Cada versión tiene un hash SHA-256 del contenido normalizado, y dos versiones son comparables con un diff unificado (difflib). Lo mismo para las políticas. Cambias un YAML → nueva versión; promueves cambiando active_version. Sin redeploy.
El índice (agents/incident_analyzer/index.yaml)
+ name: incident_analyzer +versions: + - id: v1 + hash: pending + author: Juan + message: Versión inicial; SIP/IMS y MOS básicos. + created_at: 2026-04-12T10:00:00Z + - id: v2 + hash: pending + author: Juan + message: Añade codec mismatch y refuerza prompts. + created_at: 2026-05-01T12:00:00Z +active_version: v2 # ← invoke usa esta si no pides otra+
El "commit" activo (versions/v2.yaml)
+ name: incident_analyzer +version: v2 +owner: Juan +state: active # draft | active | deprecated +guardrails: [default] # nombres de política +llm: + provider: mock + model: gpt-4o + temperature: 0.1 + max_tokens: 2000 +system_prompt: | + Eres un analista senior de operaciones de plataforma de voz + virtualizada (IMS, CSCF, SBC, HSS). Devuelves SIEMPRE un JSON + con severity, root_cause_hypothesis y proposed_actions. … +output_schema: + type: object + required: [severity, root_cause_hypothesis, proposed_actions] + … +risk_threshold_for_hitl: 4 # risk_score ≥ 4 (o requires_approval) → HITL+
registry/versioning.py tiene compute_hash(yaml_text) (SHA-256 del YAML con espacios al final recortados) y unified_diff(a, b, label_a, label_b). El endpoint GET /agents/{name}/versions/{v_from}/diff/{v_to} devuelve un DiffResult con el unified_diff; el dashboard lo pinta coloreado (+ verde, - rojo, @@ azul) en la página Registro. (En los index.yaml de ejemplo el hash es "pending" y nadie lo valida al cargar — es un MVP.)
11 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); 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) | /approve y /reject solo reciben el trace_id: hay que reconstruir qué config lo ejecutó, y 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. |
12 Trazabilidad: el hilo del trace_id
+ Cada petición lleva un trace_id (UUID) que recorre todo el sistema. structlog emite JSON, una línea por evento, con ese trace_id en el contexto. Y cada nodo del grafo deja un DecisionStep en el decision_path con su duration_ms — la "caja negra" de la ejecución.
Detalles del logging: observability/logging.py configura structlog con JSONRenderer, TimeStamper(iso), niveles, y merge_contextvars. bind_trace_id / clear_trace_id manejan el contexto por request.
13 Estados de una ejecución
+El status de un AgentExecution es uno de cinco. Tres son terminales (completed, failed, blocked_by_guardrail); awaiting_approval es el único que persiste indefinidamente esperando a un humano.
AgentExecution. running → puede acabar directamente en completed (si no hay acciones arriesgadas), o en blocked_by_guardrail / failed, o quedarse en awaiting_approval hasta que un humano hace /approve (→ completed) o /reject (→ failed). El estado awaiting_approval sobrevive a un reinicio del proceso. (Estados de un agente, en su YAML: draft → active → deprecated — concepto distinto.)14 Referencia de la API REST
+El core expone una API limpia. Todos los endpoints pasan por TraceIdMiddleware (lee/crea X-Trace-Id, lo devuelve en la respuesta). Los response_model son los modelos del dominio. Es exactamente lo que llama el dashboard (vía CoreClient).
| Método · ruta | Qué hace | Errores |
|---|---|---|
GET /health | {"status": "ok"} — usado por los healthchecks de Docker. | — |
GET /agents | Lista de AgentDefinition (la versión activa de cada agente). | — |
GET /agents/{name} | El agente (versión activa). | 404 no existe |
GET /agents/{name}/versions | Lista de AgentVersionMeta (id, hash, author, message, created_at). | 404 |
GET /agents/{name}/versions/{version} | El AgentDefinition de esa versión concreta. | 404 |
GET /agents/{name}/versions/{v_from}/diff/{v_to} | DiffResult con el unified_diff entre dos versiones. | — |
POST /agents/{name}/invoke | Lanza una ejecución. Body {input: str, version?: str}. Resuelve agente+política, llama a orchestrator.invoke, anota el índice; si el status es terminal escribe en executions.jsonl; escribe las violaciones en violations.jsonl. Devuelve un AgentExecution. | 404 agente · 422 sin política |
GET /executions | Lista de AgentExecutionSummary: las terminales del JSONL más las awaiting_approval reconstruidas desde execution_index.json + el checkpointer. | — |
GET /executions/{trace_id} | El AgentExecution completo (vía orchestrator.snapshot). | 404 · 500 si la config referida falta |
POST /executions/{trace_id}/approve | Reanuda un HITL. Body {approved_action_ids: list[str], comment?: str}. resume(decision={...rejected:false}) → finalize filtra a las aprobadas → completed. Escribe en executions.jsonl. | 404 · 409 si no está en awaiting_approval |
POST /executions/{trace_id}/reject | Rechaza un HITL. Body {reason: str}. resume(decision={...rejected:true}) → status = failed, error = rejected_by_human. Escribe en executions.jsonl. | 404 · 409 |
GET /policies | Lista de PolicyDefinition (validadores de entrada/salida y su config). | — |
GET /policies/{name}/versions | Lista de PolicyVersionMeta. | 404 |
GET /violations?trace_id=&severity= | Lista de GuardrailViolation desde violations.jsonl, con filtros opcionales por trace_id y severity (info|warning|block). | — |
Además, FastAPI sirve /docs (Swagger UI) y /openapi.json automáticamente. El walkthrough que estás leyendo no es un endpoint: es un fichero (docs/walkthrough.html) — ver §19. | ||
Helpers internos de executions.py alrededor del índice (data/execution_index.json): _record_execution (en cada invoke), _resolve (reconstruye agent_def/policy; 404/500), _ensure_awaiting (404/409).
15 El agente de ejemplo: incident_analyzer
+ El repo trae un agente de operaciones de telco y un proveedor LLM mock determinista para que el demo funcione out-of-the-box, sin API keys ni red. MockProvider mira el texto de entrada y elige una de tres respuestas canónicas buscando subcadenas ("sip", "mos", "hss", en ese orden); si no encuentra ninguna, una respuesta genérica (o, en tests con texto aleatorio, un fallback por hash). Cada respuesta es un JSON con severity, root_cause_hypothesis y proposed_actions.
Escenario (examples/*.txt) | Keyword | Respuesta del mock — acciones propuestas | ¿HITL? |
|---|---|---|---|
01_sip_registration_dropcaída de registros SIP tras desplegar imagen en CSCF |
+ "sip" |
+ act-1 rollback_image en cscf-cluster-aravaca-01 · risk 4 · requires_approval | act-2 drain_traffic_to_standby · risk 3 |
+ sí — act-1 (risk 4 ≥ umbral 4 + requires_approval) |
+
02_mos_degradation_pool_sbcdegradación de MOS en pool SBC en pico horario |
+ "mos" |
+ act-1 scale_out_sbc_pool en sbc-pool-borde-norte · risk 2 · no requiere aprobación |
+ no — completa directo | +
03_hss_capacity_active_activesaturación replicada en HSS active-active |
+ "hss" |
+ act-1 isolate_replication_link en hss-pair-madrid · risk 5 · requires_approval |
+ sí — act-1 (risk 5) |
+
"sip" en el input selecciona la respuesta canónica; act-1 tiene risk_score = 4, igual al umbral del agente (risk_threshold_for_hitl = 4) y además requires_approval = true, así que approve_gate pausa la ejecución.Lo que no es código: agents/incident_analyzer/ (el YAML del agente + examples/*.txt), policies/default/ (la política), y data/ (estado runtime gitignored: checkpoints.sqlite, executions.jsonl, violations.jsonl, execution_index.json — se crea sola). Para usar Azure OpenAI o OpenAI reales: edita .env (LLM_PROVIDER=azure|openai + las claves). Los providers reales tienen retry exponencial 1s/2s/4s.
16 El dashboard (Streamlit)
+Una consola visual con cinco páginas. No contiene lógica de negocio: client.py (CoreClient, httpx síncrono con 2 retries) envuelve todos los endpoints del core y mapea 404/409/422 a {"api_error": …} para que las páginas puedan distinguir "la API rechazó la petición" de "ejecución sin error". app.py es la raíz: sidebar de branding + health-check del core. Nadie del core depende del dashboard.
| Página | Qué muestra | Endpoints que usa |
|---|---|---|
🏛️ Registropages/1_…_Registro.py | Catálogo de agentes: detalle (prompt, esquema, LLM, guardrails), tabla de versiones, diff coloreado v1↔v2. | /agents, /agents/{n}, /agents/{n}/versions, /agents/{n}/versions/{a}/diff/{b} |
▶️ Ejecutarpages/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. | /agents, /agents/{n}/invoke |
🤝 Aprobacionespages/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. | /executions (filtra), /executions/{t}, /executions/{t}/approve, /executions/{t}/reject |
📜 Historialpages/4_…_Historial.py | Pestaña de ejecuciones (tabla + detalle por trace_id) y pestaña de violaciones (filtrable por severidad). | /executions, /executions/{t}, /violations |
📐 Politicaspages/5_…_Politicas.py | Inventario de políticas: validadores de entrada/salida (cada uno expandible con su config) y versiones. | /policies, /policies/{n}/versions |
Componentes reutilizables de UI: components/diff_view (pinta +/-/@@), components/trace_view (el timeline del decision_path), components/violation_view (badges de severidad). El dashboard no tiene tests unitarios (mal coste/beneficio para Streamlit); su verificación es la checklist manual de docs/manual_qa.md. Demo guiada en 3 pasos: Registro → comparar v1 vs v2 · Ejecutar → escenario SIP → pausa en HITL · Aprobaciones → aprobar las seguras → ejecución completa.
17 Arranque y configuración
+Out-of-the-box, sin claves: cp .env.example .env y docker compose up → dashboard en localhost:8501 y API en localhost:8000. (Este documento se consulta abriendo docs/walkthrough.html en el navegador — no requiere servidor.)
El ciclo de vida del proceso core
+-
+
- Importar
agentforge_core.mainejecutaapp = create_app():Settings()→configure_logging(level)→FastAPI(...)→add_middleware(TraceIdMiddleware)→ registraGET /health→ importa los routers →include_router(agents, executions ×2, policies, violations).
+ - Las dependencias (
get_registry,get_policy_store,get_llm_provider,get_guardrail_engine,get_orchestrator) no se construyen aún; se construyen y cachean en la primera request que las inyecta.
+ - Cada request:
TraceIdMiddleware.dispatch→ router → resuelveDepends(...)(que puede disparar la construcción perezosa) → handler → respuesta conX-Trace-Id.
+
En contenedores: uvicorn agentforge_core.main:app --host 0.0.0.0 --port 8000; HEALTHCHECK → curl /health; monta ./agents:ro, ./policies:ro, ./data:rw; DATA_DIR=/app/data, etc. El dashboard depende de core: service_healthy y usa AGENTFORGE_CORE_URL=http://core:8000. La imagen del core instala en_core_web_sm de spaCy para Presidio.
Variables de entorno (config.py · .env)
+ | Var | Default | Lo usa |
|---|---|---|
LLM_PROVIDER | mock | build_llm_provider (mock|azure|openai) |
LLM_FALLBACK_PROVIDER | "" | declarado; la factory aún no lo aplica |
AZURE_OPENAI_* | "" / 2024-08-01-preview | AzureOpenAIProvider |
OPENAI_API_KEY · OPENAI_MODEL | "" · gpt-4o | OpenAIProvider |
GUARDRAILS_NEMO_ENABLED | false | build_guardrail_engine (añade NeMo al composite) |
LOG_LEVEL | INFO | configure_logging |
DATA_DIR | ./data | checkpointer · índice · JSONL |
AGENTS_DIR | ./agents | FileSystemAgentRegistry |
POLICIES_DIR | ./policies | FileSystemPolicyStore |
AGENTFORGE_CORE_URL | http://core:8000 | el dashboard (CoreClient) |
detect_pii se comporta distinto según el entorno: con presidio-analyzer instalado (imagen Docker) usa Presidio + en_core_web_sm; sin él (venv local típico), regex fallback. · AsyncSqliteSaver liga su conexión al event loop activo: por eso el checkpointer es un async context manager que el orchestrator abre y cierra en cada operación; requiere aiosqlite<0.21. · data/ debe existir y ser escribible por el usuario del contenedor (agent, uid 1000). · build_llm_provider usa match sin case _ (exhaustivo sobre el Literal de 3 valores). · NeMo solo entra al composite si GUARDRAILS_NEMO_ENABLED=true.
18 Tests
+Hay una suite de unit + integration (~90 tests). make test corre solo los unit; make test-all añade los de integración; make lint es ruff + mypy (estricto); make smoke levanta docker compose y hace curl /health.
-
+
tests/unit/— dominio (agent,execution,guardrail,policy),config,llm(base, mock, factory),guardrails(ai, composite),registry(repository, policy_store, versioning, factory),runtime(state-graph, nodes, orchestrator),observability/logging, y los routers de la API (agents,executions,policies/violations,health) — estos usantests/fixtures/(versiones mínimas) y hacendeps.*.cache_clear()tras apuntarDATA_DIRa untmp_path.
+ tests/integration/— montan la app conTestClientcontra los assets reales del repo (agents/,policies/):test_invoke_happy_path,test_invoke_hitl(el ciclo completo en ~40 líneas — el mejor sitio para entenderlo),test_invoke_pii_block,test_invoke_resume_after_restart(prueba que unawaiting_approvalsobrevive a recrear el orchestrator).
+ - El dashboard Streamlit no se prueba con unit tests; se verifica con la checklist de
docs/manual_qa.md.
+
19 Este documento — un HTML autocontenido
+Lo que estás leyendo es un único fichero HTML autocontenido — docs/walkthrough.html en el repositorio. Todo está embebido: los estilos, los nueve diagramas SVG, la navegación con scrollspy, el tema claro/oscuro y los bloques de código. No depende de ninguna red ni de ningún CDN, y no necesita que ningún servidor esté corriendo: se abre directamente en cualquier navegador.
xdg-open docs/walkthrough.html # Linux +open docs/walkthrough.html # macOS +# o arrástralo al navegador · o file://<ruta-del-repo>/docs/walkthrough.html+
Sería trivial si se quisiera: un router de FastAPI de ~10 líneas que devuelva este fichero como HTMLResponse (p. ej. bajo GET /walkthrough), montado aparte de los routers de negocio. En esta versión del repo no está cableado — el documento se consulta como fichero.
20 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 salida y su config, 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 step, timestamp, duration_ms y un detail. La "caja negra" de la ejecución. |
| HITL | Human-in-the-Loop: 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, completed, failed. |
| Estados de un agente | draft, active, deprecated (metadato en su YAML). |
21 Lo que aún no hace (a propósito)
+Es un MVP. En el roadmap (docs/futuro.md): NeMo Guardrails completo (Colang + KB embedding + dialog rails) · autenticación (OAuth2/OIDC, MSAL para Azure AD) · OpenTelemetry (spans por nodo, métricas de violaciones por validador) · multi-tenant (tenant_id aislado) · evaluadores LLM-as-judge (regression suite sobre escenarios canónicos, mide deriva) · UI de aprobación con SLA (cola Kanban, reasignación, alertas) · persistencia migrable a Postgres + pgvector · esquema de prompts mejorado (variables tipadas tipo Jinja, valores por entorno) · promoción explícita draft → active vía PR/aprobación. La detección de PII más fina (modelos grandes, español), también pendiente.
22 Por dónde empezar a leer (ruta de ~1 hora)
+-
+
domain/execution.pyydomain/agent.py— qué datos hay. (§06)
+ policies/default/versions/v1.yamlyagents/incident_analyzer/versions/v2.yaml— cómo se declara todo. (§07, §10)
+ runtime/nodes.pyyruntime/graph.py— el flujo de ejecución. (§08)
+ api/executions.py— cómo se expone (invoke / approve / reject). (§14)
+ tests/integration/test_invoke_hitl.py— el ciclo completo, en ~40 líneas. (§18)
+ - Levanta
docker compose up, haz clic por las cinco páginas del dashboard (§16), y abredocs/walkthrough.htmlen el navegador (§19).
+
README.md — quickstart y estado. · ARCHITECTURE.md — decisiones técnicas en bruto. · docs/explicacion.md — la narrativa de extremo a extremo (en prosa). · docs/componentes.md — la referencia de cableado a bajo nivel (firmas exactas, grafos de dependencias, cadenas de llamada por endpoint). · docs/futuro.md — roadmap. · docs/manual_qa.md — checklist de verificación manual del dashboard.