From dd5cfd979977b18fda885e8eddb5aa504dc61a93 Mon Sep 17 00:00:00 2001 From: Juan Marquez Date: Tue, 12 May 2026 15:04:47 +0200 Subject: [PATCH] added walkthrough --- README.md | 3 + docs/walkthrough.html | 1662 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1665 insertions(+) create mode 100644 docs/walkthrough.html diff --git a/README.md b/README.md index 924142f..7fe98e5 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,9 @@ necesita antes de operar agentes con impacto real. ``` Detalles completos en [`ARCHITECTURE.md`](ARCHITECTURE.md). Además: +- [`docs/walkthrough.html`](docs/walkthrough.html) — **walkthrough HTML autocontenido** + (de alto a bajo nivel, con diagramas). Ábrelo directamente en el navegador + (`xdg-open docs/walkthrough.html`); no requiere servidor. - [`docs/explicacion.md`](docs/explicacion.md) — 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). diff --git a/docs/walkthrough.html b/docs/walkthrough.html new file mode 100644 index 0000000..45e0027 --- /dev/null +++ b/docs/walkthrough.html @@ -0,0 +1,1662 @@ + + + + + + +AgentForge · Walkthrough + + + + + +
+ + 🛡️ AgentForge · Walkthrough + +
+
+ +
+ + + + +
+ +
+
Plataforma de gobernanza de agentes IA · documento generado para entender el proyecto completo
+

🛡️ 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.

+
+ 🐍 Python 3.11+ + FastAPI · core :8000 + 📊 Streamlit · dashboard :8501 + 🔀 LangGraph · runtime stateful + 🛡️ Guardrails-AI + Presidio + 📦 Pydantic v2 · dominio + 🗃️ YAML · JSON · JSONL · SQLite + 🐳 docker-compose +
+ +
+ + +
+

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.

+
+
🧭 El caso de ejemplo del repo
+

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.

+
+
+
IDEA 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 SHA-256 y diff legible. Sin redeploy.

+
agents/ · policies/ · registry/
+
+
+
IDEA 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, no código.

+
policies/ · guardrails/
+
+
+
IDEA 3
+

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.

+
runtime/ (LangGraph)
+
+
+
IDEA 4
+

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.

+
nodo approve_gate · /approve · /reject
+
+
+
IDEA 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
+
+
+
IDEA 6
+

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.

+
llm/ · guardrails/ · registry/ (los factory.py)
+
+
+
+ + +
+

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

+ +
+ servicio / módulo + componente de entrada + componente interno clave + añadido por este walkthrough + → flujo de datos / dependencia +
+ +
+ + + + docker-compose · dos contenedores + + + + agentforge-dashboard + Streamlit · :8501 · "la consola" + 5 páginas · sin lógica de negocio + CoreClient (httpx) → habla solo HTTP + + + + agentforge-core + FastAPI · :8000 · "el cerebro" + dominio · runtime · guardrails · persistencia + /health · /agents · /executions · /policies · /violations + + + + HTTP / JSON + el dashboard es cliente del core + + + + docs/walkthrough.html + este documento — HTML autocontenido, sin servidor + + + Dentro del core · todo enchufado una vez en api/deps.py + + + + capa LLM + Strategy + factory + mock · azure · openai + ¿qué dice el LLM? + + + capa Guardrails + Strategy + factory + Composite( GuardrailsAI [, NeMo] ) + ¿pasa los filtros? + + + runtime LangGraph + grafo + checkpointer + orchestrator: invoke · resume · snapshot + la ejecución, paso a paso + + + + + + + + + Persistencia — la herramienta adecuada para cada cosa + YAML (definiciones) · JSON (índice trace_id→agente) · JSONL append-only (logs de auditoría) · SQLite (checkpoints HITL) + agents/*.yaml · policies/*.yaml · data/execution_index.json · data/executions.jsonl · data/violations.jsonl · data/checkpoints.sqlite + + + + + +
Figura 1. Los dos servicios y, dentro del core, las tres capas intercambiables (LLM, Guardrails, Runtime) más la persistencia. Cada flecha es "depende de / fluye hacia". Este documento (verde) es un HTML autocontenido — se consulta como fichero, no requiere servidor.
+
+
+ + +
+

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

+ +
+
① HTTP request
POST /agents/{name}/invoke · {input: "…"}
+
+
② TraceIdMiddleware
api/middlewares.py · genera/lee X-Trace-Id, lo bind-ea a structlog
+
+
③ Router
api/agents.py · api/executions.py · api/policies.py · api/violations.py
+
+
④ Dependencias (DI)
api/deps.py · @lru_cache: Registry, PolicyStore, LLMProvider, GuardrailEngine, Orchestrator (singletons)
+
+
⑤ AgentOrchestrator
runtime/orchestrator.py · única puerta al runtime: invoke / resume / snapshot
+
+
⑥ Grafo LangGraph
runtime/graph.py · cablea nodos + aristas condicionales, compila con checkpointer
+
+
⑦ Nodos
runtime/nodes.py · validate_input → llm_reason → validate_output → propose_actions → approve_gate → finalize
+
+
⑧ Adentro: llm/ · guardrails/ · domain/
¿qué dice el LLM? · ¿pasa los filtros? · ¿qué forma tienen los datos?
+
+
⑨ Persistencia
checkpointer (SQLite) + api/persistence.py (JSONL) + registry (YAML/JSON)
+
+ +
+
🔒 Regla de oro
+

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.

+
+
+
6app
+
+ main + create_app(): instancia FastAPI, añade TraceIdMiddleware, expone /health, monta los 5 routers. Construye Settings() para el logging. +
+
+
+
5HTTP
+
+ api.depsapi.agentsapi.executionsapi.policiesapi.violations + deps.py construye y cachea los 5 objetos del dominio (@lru_cache); los routers solo los piden por parámetro. +
+
+
+
4runtime
+
+ runtime.orchestrator + Compone el grafo + el checkpointer; traduce el StateSnapshot de LangGraph a un AgentExecution. Único punto de entrada al runtime. +
+
+
+
3
+
+ guardrails.factoryruntime.nodesruntime.graph + Las funciones-nodo (parametrizadas con engine/policy/provider/agent) y el cableado del grafo; la factory que ensambla el CompositeGuardrailEngine. +
+
+
+
2
+
+ llm.factoryregistry.factoryguardrails.guardrails_aiguardrails.compositeguardrails.nemo + Las factories de LLM y registry; los engines concretos de guardrails. composite no conoce a sus sub-engines (solo el Protocol). +
+
+
+
1
+
+ domain.executionllm.mockllm.azurellm.openairegistry.repositoryregistry.policy_storeguardrails.validatorsguardrails.baseapi.persistenceapi.middlewares + Implementaciones e interfaces concretas que dependen solo del nivel 0. +
+
+
+
0vocab
+
+ configobservability.loggingdomain.agentdomain.policydomain.guardrailregistry.versioningllm.baseruntime.stateruntime.checkpointer + No importan nada del proyecto. 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".

+ +
+ + + + La forma genérica + + base.py + un Protocol (la interfaz) + + impl_a + impl_b + impl_c + + + factory.py + build_X(settings) → X + + + Settings (pydantic-settings ← .env) — el único que sabe de env vars + + + + + + + elige y monta uno + + + + + + Las tres instancias reales + + LLM — llm/base.py: LLMProvider + MockProvider · AzureOpenAIProvider · OpenAIProvider + build_llm_provider(settings) → match settings.llm_provider · usado por el nodo llm_reason + + + Guardrails — guardrails/base.py: GuardrailEngine + GuardrailsAIEngine · NeMoGuardrailsEngine (opcional) · vía CompositeGuardrailEngine + build_guardrail_engine(settings) → NeMo solo si GUARDRAILS_NEMO_ENABLED + + + Registry — registry/repository.py · registry/policy_store.py + FileSystemAgentRegistry · FileSystemPolicyStore (hoy ficheros; mañana ¿BD?) + build_agent_registry(settings) · build_policy_store(settings) + +
Figura 2. Izquierda: la forma genérica (interfaz 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 · agent.py + + name · version · owner · purpose + state: draft | active | deprecated + guardrails: list[str] → nombres de política + llm: LLMConfig + system_prompt: str + output_schema: dict (JSON Schema) + risk_threshold_for_hitl: int (1..5) + updated_at + + + + LLMConfig + providermodel + temperaturemax_tokens + + + + AgentVersionMeta + id · hashauthor + messagecreated_at + + + + index.yaml + + + + PolicyDefinition · policy.py + + name · version · description + input_validators: list[PolicyValidator] + output_validators: list[PolicyValidator] + on_validator_error: + fail_open | fail_closed (def: fail_closed) + + + PolicyValidator + type: strconfig: dict + + + PolicyVersionMeta + id · hash · authormessage · created_at + + + + por nombre + + + + AgentExecution · execution.py + el "expediente" de una invocación · va a JSONL al terminar + + trace_id: UUID · agent_name · agent_version + status: running | awaiting_approval | + blocked_by_guardrail | completed | failed + started_at · finished_at + decision_path: list[DecisionStep] + violations: list[GuardrailViolation] + proposed_actions: list[ProposedAction] + needs_human_for: list[ProposedAction] | None + final_output: dict | None · error: str | None + + + DecisionStep + step · timestampduration_ms · detail + + + ProposedAction + id · action · targetrisk_score(1-5) · rollback_plan · requires_approval + + + + GuardrailViolation · guardrail.py + + trace_id: UUID · timestamp + stage: input | output · validator: str + severity: info | warning | block + message: str · blocked: bool + + + + + acumula + + + + AgentExecutionSummary — versión ligera para listados + + +
Figura 3. Los modelos del dominio. 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.
+
+ +
+ + + + + + + + +
FicheroQué define
domain/agent.pyAgentDefinition (prompt, modelo, output_schema, guardrails, risk_threshold_for_hitl…), LLMConfig, AgentVersionMeta.
domain/policy.pyPolicyDefinition (listas de PolicyValidator de entrada y salida, on_validator_error), PolicyVersionMeta.
domain/guardrail.pyGuardrailViolation (trace_id, stage input/output, validator, severity, message, blocked).
domain/execution.pyAgentExecution (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).

+ +
+ + + + + + PolicyDefinition + policies/default/v1.yaml + + input_validators[…] + output_validators[…] + on_validator_error: fail_closed + + + + CompositeGuardrailEngine + corre N sub-engines en paralelo (asyncio.gather) + y aplana las listas de violaciones + + + + GuardrailsAIEngine + el real: aplica los validadores de la política + integra Presidio para PII (con fallback a regex) + + + NeMoGuardrailsEngine + stub · solo si GUARDRAILS_NEMO_ENABLED=true + + + + INPUT_VALIDATORS (type → fn) + detect_pii · prompt_injection + toxic_language · forbidden_topics + payload = str (texto del usuario) + + + OUTPUT_VALIDATORS (type → fn) + schema_match · pii_leakage + forbidden_action_keywords + telco_safety_rules + payload = dict (JSON del LLM) + + + + + qué validar + + + + + busca cada type + + + + → list[GuardrailViolation] · cualquier blocked=True frena el grafo + + + agrega + + fail_closed: si una fn validadora lanza → se añade una violación severity=block, blocked=True. type desconocido → log warning, no bloquea. + +
Figura 4. El motor de guardrails. La política dice qué validar; 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)

+
+ + + + + + + + + + + + +
ValidadorEtapaQué compruebaSeveridad típica
detect_piiinputPII 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_injectioninputHeurísticas de patrones conocidos ("ignore previous instructions", "you are now", "reveal the system prompt"…).block
toxic_languageinputLista negra con umbral (MVP).warning
forbidden_topicsinputCoincidencia de substring con temas prohibidos (instrucciones de explotación, credenciales, código malicioso).block
schema_matchoutputValida 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_leakageoutputRe-aplica detect_pii sobre la salida serializada.block
forbidden_action_keywordsoutputQue las acciones propuestas no contengan comandos peligrosos (DROP TABLE, rm -rf, shutdown -h now, delete production, format c:).block
telco_safety_rulesoutputReglas 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):

+
+
policies/default/versions/v1.yamlyaml
+
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.

+ +
+ nodo del grafo + pausa (estado persistido) + salida temprana (status terminal) + — camino normal · - - camino condicional +
+ +
+ + + + + + START + + + + validate_input + engine.validate_input(...) + + + + llm_reason + provider.complete([sys, user]) + + + + validate_output + json.loads + engine.validate_output + + + + propose_actions + extrae proposed_actions[] + + + + approve_gate + ¿alguna acción con risk_score ≥ umbral + o requires_approval = True? + + + + finalize + filtra a las acciones aprobadas + + + + END + + + + ok + ok + ok + + + no hay riesgo + + + + END + + bloqueada — PII, injection, topic… → status = blocked_by_guardrail + + END + + LLM no disponible / error → status = failed (llm_unavailable) + + END + + salida no parsea / no cumple esquema / PII en la salida → blocked_by_guardrail | failed + + + + interrupt({ awaiting_actions: [...] }) + el grafo SE PAUSA aquí. + LangGraph persiste el estado en + data/checkpoints.sqlite + status del snapshot = awaiting_approval + + + hay riesgo → sí + + + + …más tarde (¡incluso tras reiniciar!)… + resume(Command(resume=decision)) + POST /executions/{trace_id}/approve | /reject + + + + interrupt() devuelve la decisión → approve_gate continúa + + + END + + human_decision.rejected → status = failed (rejected_by_human) + si no hubo HITL: todas las acciones + +
Figura 5. El grafo. Columna izquierda: el camino feliz 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)HacePuede saltar a
validate_input
build_node_validate_input(engine, policy)
Corre los input_validators de la política sobre el texto del usuario; acumula violaciones.END si alguna blockedstatus = blocked_by_guardrail
llm_reason
build_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_output
build_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_actions
build_node_propose_actions()
Extrae parsed_output["proposed_actions"] al estado.siempre → approve_gate
approve_gate
build_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)
finalize
build_node_finalize()
Si human_decision.rejectedstatus = 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:

+
+
core/src/agentforge_core/runtime/state.pypython
+
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.

+
+
runtime/graph.py — aristas condicionalespython
+
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)
+
+
+
runtime/nodes.py — build_node_approve_gatepython
+
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.

+ +
+ + + + + Clientedashboard / curl + routerapi/executions.py · +middleware + AgentOrchestratorruntime/orchestrator.py + Grafo LangGraphnodes + checkpointer + checkpointsdata/checkpoints.sqlite + logs / índiceexecutions.jsonl · index.json + + + + + + + + + + + ACTO 1 · invoke → awaiting_approval + + + + POST /agents/incident_analyzer/invoke { input } + TraceIdMiddleware genera trace_id = UUID y lo bind-ea al log + + + registry.get_agent → AgentDefinition (v2 activa) + policies.get_policy(agent.guardrails[0]) → PolicyDefinition + + + orchestrator.invoke(agent_def, policy, user_input) + + + graph.ainvoke(estado_inicial, config={thread_id: trace_id}) + + + validate_input → llm_reason → validate_output → propose_actions + 0 violaciones · MockProvider ve "sip" → respuesta canónica + + + approve_gate: act-1 (risk 4, requires_approval) → interrupt() + ⇒ el grafo SE DETIENE aquí + + + persiste el estado pausado + + + aget_state → hay nodo pendiente + + + AgentExecution(status=awaiting_approval, needs_human_for=[act-1]) + + + execution_index.json[trace_id] = (incident_analyzer, v2) · (no es terminal → NO se escribe en executions.jsonl) + + + 200 { status: awaiting_approval, trace_id, decision_path, … } + + + + + · · · más tarde — incluso tras reiniciar agentforge-core: el estado pausado sigue en checkpoints.sqlite · · · + + + ACTO 2 · el humano decide → completed + + + + POST /executions/{trace_id}/approve { approved_action_ids: [act-1], comment } + + + _resolve: lee execution_index.json → reconstruye agent_def + policy + + + _ensure_awaiting: snapshot → status == awaiting_approval ✓ (409 si no) + + + orchestrator.resume(trace_id, decision={approved_action_ids:[act-1], rejected:false}) + + + reabre el checkpointer (mismo data_dir) — el estado pausado sigue ahí + + + graph.ainvoke(Command(resume=decision), config={thread_id: trace_id}) + + + approve_gate (interrupt() devuelve la decisión) → finalize + final_output={…, approved_actions:[act-1]} · status = completed + + + + AgentExecution(status=completed, final_output) + + + append → executions.jsonl · (status terminal) + + + 200 { status: completed, final_output, … } + +
Figura 6. Diagrama de secuencia. Acto 1: la petición entra, el middleware pone un 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 rejectfinalize ve rejected: truestatus = failed, error = rejected_by_human.
+
+ +

El mismo recorrido, con curl (provider mock, sin claves):

+
+
demo end-to-end con curlbash
+
# 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)

+
+
index.yamlyaml
+
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)

+
+
versions/v2.yamlyaml
+
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
+
+
+
+ +
+
🔎 La "magia tipo Git"
+

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.

+ +
+ + + + + + agentforge-core + registry · orchestrator + api/persistence · runtime/checkpointer + + + + YAML — agents/*.yaml · policies/*.yaml + lo escribe un humano (legible, comentable) + lo cataloga la máquina (index.yaml + versiones) + + lee · diff · hash + + + + SQLite — data/checkpoints.sqlite + estado intermedio del grafo, incluida la pausa HITL + vía LangGraph AsyncSqliteSaver · permite reanudar + + escribe/lee en cada transición + + + + JSON — data/execution_index.json + { trace_id: { agent_name, version } } + para reconstruir la config al reanudar un HITL + + _record_execution en cada invoke · _resolve lo lee + + + + JSONL append-only — data/executions.jsonl + un AgentExecution por línea · inmutable, auditable + se escribe si el status es terminal (y siempre tras approve/reject) + + append_execution + + + + JSONL append-only — data/violations.jsonl + una GuardrailViolation por línea + se escribe una por violación de la ejecución + + append_violation + + + GET /executions lee executions.jsonl + reconstruye las awaiting_approval del índice + checkpointer. GET /violations lee violations.jsonl. + +
Figura 7. Quién escribe y quién lee cada artefacto. Las flechas con doble cabeza indican lectura+escritura. El núcleo: definiciones en YAML (humano), índice en JSON (para reanudar), logs en JSONL (auditoría inmutable), estado intermedio en SQLite (lo que hace posible el HITL persistente).
+
+ +
+ + + + + + + + + +
QuéCómoPor qué así
Definiciones de agentes y políticasYAML por versión + index.yamlLo escribe un humano (legible, comentable); lo cataloga la máquina.
Identidad de versiones / comparaciónhash SHA-256 del contenido + difflibVersiones inmutables identificables y comparables — "tipo Git".
Mapa trace_id → agente/versiónJSON (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 violacionesJSONL append-onlyInmutable, 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.

+ +
+
Request entra
¿trae X-Trace-Id? si no, uuid4()
+
+
TraceIdMiddleware
bind_trace_id(...) en structlog contextvars
+
+
Routers · Orchestrator
todos los logs llevan trace_id; thread_id del grafo = trace_id
+
+
Nodos del grafo
cada uno añade un DecisionStep {step, timestamp, duration_ms, detail}
+
+
Guardrails
cada GuardrailViolation lleva el trace_id
+
+
Persistencia
executions.jsonl · violations.jsonl · execution_index.json
+
+
Response sale
cabecera X-Trace-Id (= el mismo)
+
+

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.

+ +
+ + + + + + + + running + recorriendo el grafo + + + + blocked_by_guardrail + un validador con blocked=True + + validate_input / validate_output + + + + failed + llm_unavailable · output_schema_mismatch · internal_error · rejected_by_human + + error del LLM / parseo / crash + + + + awaiting_approval + pausado en interrupt() · estado en SQLite + + approve_gate: hay riesgo + + + + sobrevive a reinicios + + + + completed + final_output con approved_actions + + + + /reject → rejected_by_human + + + + /approve + + + + sin acciones arriesgadas → finalize directo + + + + + +
Figura 8. Estados de un 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 · rutaQué haceErrores
GET /health{"status": "ok"} — usado por los healthchecks de Docker.
GET /agentsLista de AgentDefinition (la versión activa de cada agente).
GET /agents/{name}El agente (versión activa).404 no existe
GET /agents/{name}/versionsLista 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}/invokeLanza 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 /executionsLista 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}/approveReanuda 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}/rejectRechaza un HITL. Body {reason: str}. resume(decision={...rejected:true})status = failed, error = rejected_by_human. Escribe en executions.jsonl.404 · 409
GET /policiesLista de PolicyDefinition (validadores de entrada/salida y su config).
GET /policies/{name}/versionsLista 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)KeywordRespuesta del mock — acciones propuestas¿HITL?
01_sip_registration_drop
caí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 3act-1 (risk 4 ≥ umbral 4 + requires_approval)
02_mos_degradation_pool_sbc
degradació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ónno — completa directo
03_hss_capacity_active_active
saturación replicada en HSS active-active
"hss"act-1 isolate_replication_link en hss-pair-madrid · risk 5 · requires_approvalact-1 (risk 5)
+
+ +
+ + + input del incidente"…caída de registros SIP…" + MockProvider¿contiene "sip"? → sí + respuesta canónica SIPJSON: severity=high, hypothesis,proposed_actions=[act-1, act-2] + propose_actions[act-1 risk4, act-2 risk3] + approve_gateact-1.risk_score(4) ≥ risk_threshold_for_hitl(4)→ interrupt() → awaiting_approval + + + + + +
Figura 9. El escenario SIP de punta a punta a través del mock. La palabra "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áginaQué muestraEndpoints que usa
🏛️ Registro
pages/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}
▶️ Ejecutar
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./agents, /agents/{n}/invoke
🤝 Aprobaciones
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./executions (filtra), /executions/{t}, /executions/{t}/approve, /executions/{t}/reject
📜 Historial
pages/4_…_Historial.py
Pestaña de ejecuciones (tabla + detalle por trace_id) y pestaña de violaciones (filtrable por severidad)./executions, /executions/{t}, /violations
📐 Politicas
pages/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

+
    +
  1. Importar agentforge_core.main ejecuta app = create_app(): Settings()configure_logging(level)FastAPI(...)add_middleware(TraceIdMiddleware) → registra GET /health → importa los routers → include_router (agents, executions ×2, policies, violations).
  2. +
  3. 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.
  4. +
  5. Cada request: TraceIdMiddleware.dispatch → router → resuelve Depends(...) (que puede disparar la construcción perezosa) → handler → respuesta con X-Trace-Id.
  6. +
+

En contenedores: uvicorn agentforge_core.main:app --host 0.0.0.0 --port 8000; HEALTHCHECKcurl /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)

+
+ + + + + + + + + + + + + + +
VarDefaultLo usa
LLM_PROVIDERmockbuild_llm_provider (mock|azure|openai)
LLM_FALLBACK_PROVIDER""declarado; la factory aún no lo aplica
AZURE_OPENAI_*"" / 2024-08-01-previewAzureOpenAIProvider
OPENAI_API_KEY · OPENAI_MODEL"" · gpt-4oOpenAIProvider
GUARDRAILS_NEMO_ENABLEDfalsebuild_guardrail_engine (añade NeMo al composite)
LOG_LEVELINFOconfigure_logging
DATA_DIR./datacheckpointer · índice · JSONL
AGENTS_DIR./agentsFileSystemAgentRegistry
POLICIES_DIR./policiesFileSystemPolicyStore
AGENTFORGE_CORE_URLhttp://core:8000el dashboard (CoreClient)
+
+
+
+
+
⚠️ Gotchas
+

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 usan tests/fixtures/ (versiones mínimas) y hacen deps.*.cache_clear() tras apuntar DATA_DIR a un tmp_path.
  • +
  • tests/integration/ — montan la app con TestClient contra 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 un awaiting_approval sobrevive 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 autocontenidodocs/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.

+
+
abrir el walkthrough — no hace falta docker ni el corebash
+
xdg-open docs/walkthrough.html        # Linux
+open docs/walkthrough.html            # macOS
+# o arrástralo al navegador · o file://<ruta-del-repo>/docs/walkthrough.html
+
+
+
🔌 ¿Servirlo desde la API?
+

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érminoSignificado en este proyecto
AgenteUna 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 / guardrailUna 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ónEl 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_pathLa 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.
HITLHuman-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.
CheckpointerEl componente de LangGraph que persiste el estado del grafo (aquí, en SQLite). Lo que hace posible reanudar un HITL tras un reinicio.
trace_idUUID que identifica una petición/ejecución y se propaga por middleware → logs → API → ficheros de auditoría.
OrchestratorLa única clase que habla con LangGraph (invoke/resume/snapshot); aísla al resto del sistema del runtime.
FactoryFunción build_X(settings) que monta la implementación de X adecuada según la configuración.
Status de ejecuciónrunning, awaiting_approval (pausada en HITL), blocked_by_guardrail, completed, failed.
Estados de un agentedraft, 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)

+
    +
  1. domain/execution.py y domain/agent.py — qué datos hay. 06)
  2. +
  3. policies/default/versions/v1.yaml y agents/incident_analyzer/versions/v2.yaml — cómo se declara todo. 07, §10)
  4. +
  5. runtime/nodes.py y runtime/graph.py — el flujo de ejecución. 08)
  6. +
  7. api/executions.py — cómo se expone (invoke / approve / reject). 14)
  8. +
  9. tests/integration/test_invoke_hitl.py — el ciclo completo, en ~40 líneas. 18)
  10. +
  11. Levanta docker compose up, haz clic por las cinco páginas del dashboard 16), y abre docs/walkthrough.html en el navegador 19).
  12. +
+
+
📚 Documentos hermanos
+

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.

+
+
+ + + +
+
+ + + + + +