añadido recorrido codificación a walkthrough

This commit is contained in:
2026-05-19 18:13:13 +02:00
parent dd5cfd9799
commit 4244b00ce7
+123
View File
@@ -304,6 +304,7 @@ details.dd .body{padding:4px 16px 14px}
<li><a href="#glosario"><span class="n">20</span>Glosario</a></li> <li><a href="#glosario"><span class="n">20</span>Glosario</a></li>
<li><a href="#roadmap"><span class="n">21</span>Qué no hace (a propósito)</a></li> <li><a href="#roadmap"><span class="n">21</span>Qué no hace (a propósito)</a></li>
<li><a href="#leer"><span class="n">22</span>Por dónde empezar a leer</a></li> <li><a href="#leer"><span class="n">22</span>Por dónde empezar a leer</a></li>
<li><a href="#codificar"><span class="n">23</span>Ruta de codificación manual</a></li>
</ul> </ul>
</nav> </nav>
</aside> </aside>
@@ -1597,6 +1598,128 @@ open docs/walkthrough.html <span class="c"># macOS</span>
</section> </section>
<!-- ============================================================= -->
<section id="codificar">
<h2><span class="kicker">23</span> Ruta de codificación manual — de cero a sistema funcionando</h2>
<p class="lead">Si quieres <strong>entender el proyecto escribiéndolo</strong>, este es el orden que maximiza la comprensión. La lógica: sigue el grafo de dependencias hacia afuera desde el centro — empieza por lo que no depende de nada, termina por lo que depende de todo.</p>
<div class="callout key">
<div class="ct">Principio rector</div>
<p><code>domain/</code> no importa nada del proyecto. El grafo no sabe que existe una API. La API no sabe que existe el dashboard. Escribe en esa dirección: <strong>adentro → afuera, abstracto → concreto, sin dependencias → con dependencias</strong>.</p>
</div>
<h3>Fase 1 — El vocabulario: <code>domain/</code> <em class="muted">(~30 min)</em></h3>
<p>Estos cuatro ficheros no importan nada del proyecto — solo Pydantic. Son el vocabulario de todo lo demás. Escribirlos primero te obliga a responder <em>"¿qué maneja este sistema?"</em> antes de pensar en cómo lo procesa.</p>
<ol>
<li><code>domain/agent.py</code><code>AgentDefinition</code>, <code>LLMConfig</code>, <code>AgentVersionMeta</code>. Al terminar sabrás exactamente qué es "un agente" en este sistema: una declaración Pydantic, no código Python.</li>
<li><code>domain/guardrail.py</code><code>GuardrailViolation</code>. Qué devuelve un guardrail cuando detecta un problema: una violación tipada, no una excepción.</li>
<li><code>domain/policy.py</code><code>PolicyDefinition</code>. La lista de reglas que rigen un agente concreto.</li>
<li><code>domain/execution.py</code> — el modelo más rico: <code>AgentExecution</code>, <code>ProposedAction</code>, <code>ExecutionStatus</code>, <code>DecisionStep</code>. Contiene el estado completo de una ejecución. Fíjate en que <code>AgentExecution</code> tiene <em>definición</em> del agente separada de su <em>instancia en runtime</em>.</li>
</ol>
<div class="callout tip">
<div class="ct">Momento clave #1</div>
<p>Al escribir <code>AgentExecution</code> te das cuenta de que el sistema separa <em>definir</em> un agente (YAML → Pydantic) de <em>ejecutarlo</em> (runtime → JSONL). Son dos ciclos de vida distintos.</p>
</div>
<h3>Fase 2 — Las costuras: los dos <code>Protocol</code>s <em class="muted">(~15 min)</em></h3>
<p>Los <code>Protocol</code>s son las interfaces estructurales del sistema — el contrato que separa <em>"qué hace"</em> de <em>"cómo lo hace"</em>. El grafo solo importa estos dos ficheros, nunca OpenAI ni guardrails-ai directamente.</p>
<ol>
<li><code>llm/base.py</code><code>LLMProvider</code> (un método <code>async complete()</code>), <code>Message</code>, <code>CompletionResult</code>. Dos clases Pydantic y un Protocol de cuatro líneas.</li>
<li><code>guardrails/base.py</code><code>GuardrailEngine</code>: dos métodos (<code>validate_input</code> y <code>validate_output</code>), ambos devuelven <code>list[GuardrailViolation]</code>.</li>
</ol>
<div class="callout tip">
<div class="ct">Momento clave #2</div>
<p>Escribir estos dos Protocols antes que cualquier implementación te revela por qué cambiar el proveedor LLM de <code>mock</code> a <code>azure</code> no requiere tocar el grafo: el grafo nunca ve la implementación concreta.</p>
</div>
<h3>Fase 3 — Primera ejecución posible: mocks + YAML del agente <em class="muted">(~45 min)</em></h3>
<p>Con dominios e interfaces ya puedes ejecutar el sistema sin ninguna clave de API. Los mocks son tu andamio.</p>
<ol>
<li><code>llm/mock.py</code> — implementa <code>LLMProvider</code> devolviendo un JSON hardcodeado. ~30 líneas. Permite correr el grafo completo sin tocar OpenAI ni Azure.</li>
<li><code>guardrails/validators.py</code> — validadores keyword-based simples. Implementan <code>GuardrailEngine</code> sin dependencias externas.</li>
<li>El primer YAML del agente: <code>agents/incident_analyzer/versions/v1.yaml</code>. Escribe la definición declarativa (nombre, owner, purpose, guardrails, config LLM, system_prompt, output_schema).</li>
</ol>
<div class="callout tip">
<div class="ct">Momento clave #3</div>
<p>Al escribir el YAML del agente entiendes que un agente es <strong>pura configuración</strong>: no tiene código Python. Cambiar su comportamiento — modelo, temperatura, guardrails, threshold de HITL — es editar texto, no redeployar.</p>
</div>
<h3>Fase 4 — El corazón del sistema: estado → nodos → grafo <em class="muted">(~1.5 h)</em></h3>
<p>Esta es la parte más densa e importante. El grafo LangGraph es la máquina de estados que orquesta todo. El orden dentro de la fase también importa.</p>
<ol>
<li>
<strong><code>runtime/state.py</code></strong> primero — <code>AgentState</code>: un <code>TypedDict</code> con todos los campos del estado mutable que fluye por el grafo.
El detalle a entender: <code>decision_path: Annotated[list, operator.add]</code> — no se sobreescribe, se acumula; LangGraph llama al reducer automáticamente cada vez que un nodo devuelve un <code>decision_path</code>.
</li>
<li>
<strong><code>runtime/nodes.py</code></strong> — los seis nodos, en el orden en que aparecerán en el grafo:
<ul>
<li><code>build_node_validate_input</code> — llama al engine, acumula violations, decide si poner <code>blocked_by_guardrail</code>.</li>
<li><code>build_node_llm_reason</code> — llama al provider, captura el fallo y lo transforma en <code>status="failed"</code>.</li>
<li><code>build_node_validate_output</code> — parsea el JSON del LLM y valida el output contra la política.</li>
<li><code>build_node_propose_actions</code> — extrae <code>proposed_actions</code> del output parseado.</li>
<li><code>build_node_approve_gate</code> — el nodo HITL: llama a <code>interrupt({"awaiting_actions": risky})</code> si hay acciones con riesgo ≥ threshold. La ejecución se pausa aquí hasta que llegue un <code>Command(resume=...)</code>.</li>
<li><code>build_node_finalize</code> — filtra acciones por las aprobadas, construye <code>final_output</code>.</li>
</ul>
<p>Nota el patrón: cada builder <em>captura sus dependencias por clausura</em> y devuelve una función <code>async (state) → dict</code>. Las dependencias no viajan en el estado — están fijas en el cierre.</p>
</li>
<li>
<strong><code>runtime/graph.py</code></strong> — conecta los nodos. Los tres routers condicionales (<code>_after_validate_input</code>, <code>_after_llm</code>, <code>_after_validate_output</code>) hacen que cualquier error cortocircuite hacia <code>END</code> sin pasar por nodos posteriores.
</li>
</ol>
<div class="callout tip">
<div class="ct">Momento clave #4</div>
<p>Al implementar <code>approve_gate</code> entiendes cómo funciona HITL en LangGraph: <code>interrupt()</code> no lanza excepción — serializa el estado en el checkpoint SQLite y devuelve el control. La ejecución se reanuda en el mismo nodo cuando llega <code>Command(resume=...)</code>. El estado nunca se pierde entre las dos llamadas HTTP.</p>
</div>
<h3>Fase 5 — Orquestador y checkpointer <em class="muted">(~30 min)</em></h3>
<ol>
<li><code>runtime/checkpointer.py</code> — configura SQLite como backend de checkpoints LangGraph. Sin esto, <code>interrupt()</code> no puede persistir el estado entre la llamada a <code>invoke</code> y la llamada a <code>approve</code>.</li>
<li><code>runtime/orchestrator.py</code> — el objeto que la API usará. Recibe <code>AgentDefinition</code> + <code>PolicyDefinition</code> + providers ya construidos, compila el grafo y expone dos métodos: <code>invoke()</code> (nueva ejecución) y <code>approve()</code> (reanudar tras HITL).</li>
</ol>
<p>Punto de control: en este momento tienes un sistema funcionando de extremo a extremo. Puedes llamar directamente al orquestador en un test y observar el <code>decision_path</code> completo, sin API ni dashboard.</p>
<h3>Fase 6 — La API: de adentro hacia afuera <em class="muted">(~1 h)</em></h3>
<p>Dentro de <code>api/</code>, el orden también importa.</p>
<ol>
<li><code>api/deps.py</code> — el armario de cableado: los <code>@lru_cache</code> que construyen el registry, el policy store, el LLM provider y el guardrail engine. <strong>Escríbelo después de tener todo lo anterior</strong> — solo entonces sabrás exactamente qué estás conectando.</li>
<li><code>api/executions.py</code> — los tres endpoints críticos: <code>POST /executions/invoke</code>, <code>POST /executions/{trace_id}/approve</code>, <code>GET /executions/{trace_id}</code>.</li>
<li><code>api/agents.py</code> — CRUD del registry (listar agentes, leer versión concreta).</li>
<li><code>api/middlewares.py</code> — inyecta <code>X-Trace-Id</code> y bind-ea el contexto de structlog.</li>
<li><code>main.py</code> — monta los routers, configura CORS, arranca la app FastAPI.</li>
</ol>
<h3>Fase 7 — Registry y versionado <em class="muted">(~45 min)</em></h3>
<p>Si has estado usando el mock registry, ahora añades la persistencia real basada en YAML.</p>
<ol>
<li><code>registry/repository.py</code> — carga definiciones de agentes desde YAML, cachea en memoria.</li>
<li><code>registry/versioning.py</code> — hash SHA-256 + <code>index.yaml</code>: el sistema de versionado tipo-Git.</li>
<li><code>registry/policy_store.py</code> — análogo al repository, pero para políticas.</li>
<li><code>registry/factory.py</code> — el factory que elige qué implementación de registry usar según <code>Settings</code>.</li>
</ol>
<h3>Fase 8 — El dashboard <em class="muted">(~1.5 h)</em></h3>
<p>El dashboard es un cliente HTTP del core — puedes empezarlo en cualquier momento después de tener los endpoints.</p>
<ol>
<li><code>dashboard/client.py</code> — primero. Te fuerza a pensar en qué necesita la UI antes de diseñar las páginas. Es el contrato del core visto desde el consumidor.</li>
<li>Las páginas por complejidad creciente:
<ul>
<li><code>pages/1_Registro.py</code> — solo lectura: lista agentes y versiones.</li>
<li><code>pages/5_Politicas.py</code> — solo lectura: muestra políticas activas.</li>
<li><code>pages/4_Historial.py</code> — lectura + filtros: lista ejecuciones pasadas.</li>
<li><code>pages/2_Ejecutar.py</code> — escritura: invoke + polling de estado + visualización del <code>decision_path</code>.</li>
<li><code>pages/3_Aprobaciones.py</code> — la más compleja: muestra acciones pendientes, permite aprobar o rechazar individual o masivamente.</li>
</ul>
</li>
</ol>
<div class="callout ok">
<div class="ct">Punto de control mínimo viable (tras Fase 5)</div>
<p>Con las fases 15 completas y el mock LLM tienes un sistema funcionando sin Docker ni credenciales externas. Llama directamente al orquestador desde un test de integración, observa el <code>decision_path</code> completo, y pausa-reanuda en el nodo HITL. No necesitas la API ni el dashboard para validar el núcleo.</p>
</div>
</section>
<div class="footer"> <div class="footer">
<p><strong>AgentForge · Walkthrough.</strong> Documento autocontenido (sin recursos externos). Acompaña a <code>README.md</code>, <code>ARCHITECTURE.md</code>, <code>docs/explicacion.md</code> (narrativa) y <code>docs/componentes.md</code> (referencia de cableado). Refleja el repo en <code>v0.1.0</code>.</p> <p><strong>AgentForge · Walkthrough.</strong> Documento autocontenido (sin recursos externos). Acompaña a <code>README.md</code>, <code>ARCHITECTURE.md</code>, <code>docs/explicacion.md</code> (narrativa) y <code>docs/componentes.md</code> (referencia de cableado). Refleja el repo en <code>v0.1.0</code>.</p>
<p class="muted">Documento HTML autocontenido (<code>docs/walkthrough.html</code>) — sin recursos externos: ábrelo en cualquier navegador.</p> <p class="muted">Documento HTML autocontenido (<code>docs/walkthrough.html</code>) — sin recursos externos: ábrelo en cualquier navegador.</p>