cambios profundos
This commit is contained in:
+12
-15
@@ -1,26 +1,23 @@
|
||||
# Forja — Arquitectura técnica
|
||||
|
||||
> Documento "vivo" con las decisiones técnicas. El spec original está en
|
||||
> [`docs/superpowers/specs/2026-05-09- forja-design.md`](docs/superpowers/specs/2026-05-09- forja-design.md).
|
||||
> Documento vivo con las decisiones técnicas.
|
||||
|
||||
## Visión general
|
||||
|
||||
Two-tier:
|
||||
- `forja-core` — FastAPI 8000. Dominio de gobierno, runtime LangGraph,
|
||||
guardrails y persistencia.
|
||||
- `forja-dashboard` — Streamlit 8501. Cliente HTTP del core.
|
||||
Forja es un **único servicio FastAPI** (:8000):
|
||||
- Todo el dominio de gobierno, runtime LangGraph, guardrails y persistencia.
|
||||
- UI embebida (HTMX) para uso humano + API REST completa bajo `/api`.
|
||||
|
||||
## Diagrama
|
||||
|
||||
```
|
||||
┌─────────────────────┐ HTTP/JSON ┌─────────────────────┐
|
||||
│ Dashboard (Stream.) │ ───────────► │ Core (FastAPI) │
|
||||
└─────────────────────┘ └─────────┬───────────┘
|
||||
│
|
||||
┌─────────────────────────┬────────┴──────┬────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
LLM layer Guardrails layer LangGraph State
|
||||
(Strategy pattern) (Strategy pattern) Runtime JSON+SQL+JSONL
|
||||
┌────────────────────────────────────────────────────────────┐
|
||||
│ forja-core :8000 │
|
||||
│ UI HTMX (humanos) + REST API (/api) + dominio completo │
|
||||
│ │
|
||||
│ LLM (strategy) │ Guardrails (strategy) │ LangGraph │
|
||||
│ │ │ + checkpoints│
|
||||
└────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Patrones aplicados
|
||||
@@ -50,7 +47,7 @@ Se usa la API dinámica `interrupt(payload)` de LangGraph ≥0.2 dentro del
|
||||
nodo `approve_gate`. Sólo pausa cuando hay acciones de alto riesgo
|
||||
(`risk_score >= threshold` o `requires_approval=True`). El cliente envía
|
||||
`Command(resume={"approved_action_ids": [...]})` vía
|
||||
`POST /executions/{trace_id}/approve`.
|
||||
`POST /api/executions/{trace_id}/approve`.
|
||||
|
||||
## Observabilidad
|
||||
|
||||
|
||||
@@ -7,30 +7,28 @@ ejecución controlada con runtime stateful, Human-in-the-Loop y observabilidad c
|
||||
|
||||
## Arquitectura
|
||||
|
||||
Forja es un **único servicio** (FastAPI en :8000) que expone:
|
||||
|
||||
- API REST completa bajo `/api` (agentes, políticas, ejecuciones, aprobaciones, violaciones).
|
||||
- UI embebida moderna (HTMX + Tailwind + Jinja) en rutas amigables (`/agents`, `/run`, `/approvals`, `/history`, `/policies`).
|
||||
|
||||
```
|
||||
┌───────────────────────── docker-compose ──────────────────────────┐
|
||||
│ │
|
||||
│ ┌─────────────────────┐ HTTP/JSON ┌────────────────────┐ │
|
||||
│ │ forja-dashboard │ ────────────────► │ forja-core │ │
|
||||
│ │ Streamlit :8501 │ ◄──────────────── │ FastAPI :8000 │ │
|
||||
│ └─────────────────────┘ └────────────────────┘ │
|
||||
│ │
|
||||
│ Strategy pattern para LLMProvider, GuardrailEngine, Registry. │
|
||||
│ Persistencia: YAML (defs versionadas), JSONL (logs), SQLite │
|
||||
│ (checkpoints LangGraph + HITL). │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ forja-core :8000 │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ UI HTMX (navegador humano) │ API REST (/api/*) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ Strategy factories (LLM / Guardrails / Registry) │
|
||||
│ LangGraph runtime + SQLite checkpoints (HITL real) │
|
||||
│ Persistencia: YAML versionado + JSONL + SQLite │
|
||||
└───────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
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).
|
||||
- [`docs/componentes.md`](docs/componentes.md) — referencia de cableado a bajo nivel
|
||||
(grafo de dependencias de módulos, inyección de dependencias, firmas de los
|
||||
contratos entre capas, cadenas de llamada de cada endpoint).
|
||||
Documentación viva:
|
||||
- [`ARCHITECTURE.md`](ARCHITECTURE.md) — decisiones técnicas.
|
||||
- [`docs/explicacion.md`](docs/explicacion.md) — explicación didáctica completa.
|
||||
- [`docs/componentes.md`](docs/componentes.md) — referencia de cableado a bajo nivel.
|
||||
- [`docs/futuro.md`](docs/futuro.md) — roadmap.
|
||||
|
||||
## Quickstart
|
||||
|
||||
@@ -39,7 +37,7 @@ cp .env.example .env
|
||||
docker compose up
|
||||
```
|
||||
|
||||
Abre [http://localhost:8501](http://localhost:8501).
|
||||
Abre [http://localhost:8000](http://localhost:8000).
|
||||
|
||||
Funciona out-of-the-box (`LLM_PROVIDER=mock`, sin API keys). Si quieres usar
|
||||
Azure OpenAI real, edita `.env`.
|
||||
@@ -64,8 +62,8 @@ Azure OpenAI real, edita `.env`.
|
||||
| LangGraph + HITL + checkpoints | `core/src/forja_core/runtime/` |
|
||||
| Observabilidad (trace + structlog) | `core/src/forja_core/observability/` |
|
||||
| LLM abstraction | `core/src/forja_core/llm/` |
|
||||
| Editores gráficos (nuevo) | dashboard páginas Forjar Agente / Política |
|
||||
| API REST + dashboard | forja-core :8000 + forja-dashboard :8501 |
|
||||
| UI embebida (HTMX) | `core/src/forja_core/web/` (ruta raíz) |
|
||||
| API REST completa | bajo `/api` en forja-core :8000 |
|
||||
|
||||
## Variables de entorno
|
||||
|
||||
@@ -79,13 +77,12 @@ Ver [`docs/futuro.md`](docs/futuro.md).
|
||||
|
||||
```
|
||||
forja/
|
||||
├── core/ servicio FastAPI (gobernanza + runtime)
|
||||
├── dashboard/ Streamlit (UI + editores gráficos)
|
||||
├── core/ servicio FastAPI único (API + UI HTMX embebida)
|
||||
├── agents/ definiciones de agentes (YAML versionados)
|
||||
├── policies/ políticas de guardrails (YAML versionados)
|
||||
├── data/ runtime state (gitignored)
|
||||
├── tests/
|
||||
└── docs/
|
||||
└── docs/ (explicacion.md, componentes.md, futuro.md, ARCHITECTURE.md)
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
@@ -216,12 +216,19 @@ async def approve_execution(
|
||||
) -> AgentExecution:
|
||||
agent_def, policy = _resolve(registry, policies, settings.data_dir, trace_id)
|
||||
await _ensure_awaiting(orchestrator, agent_def, policy, trace_id)
|
||||
# Empty approved_action_ids from UI "Aprobar todo" → approve everything that was waiting
|
||||
approved = body.approved_action_ids
|
||||
if not approved:
|
||||
# Try to resolve the actual ids from current snapshot (best effort)
|
||||
snap = await orchestrator.snapshot(agent_def=agent_def, policy=policy, trace_id=trace_id)
|
||||
if snap and snap.needs_human_for:
|
||||
approved = [a.id for a in snap.needs_human_for]
|
||||
execution = await orchestrator.resume(
|
||||
agent_def=agent_def,
|
||||
policy=policy,
|
||||
trace_id=trace_id,
|
||||
decision={
|
||||
"approved_action_ids": body.approved_action_ids,
|
||||
"approved_action_ids": approved,
|
||||
"comment": body.comment,
|
||||
"rejected": False,
|
||||
},
|
||||
|
||||
@@ -24,18 +24,18 @@ def create_app() -> FastAPI:
|
||||
async def health() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
|
||||
from forja_core.web import ui
|
||||
from forja_core.api import agents, executions, policies, violations
|
||||
from forja_core.web import ui
|
||||
|
||||
# UI HTMX embebida (Opción A) — se registra primero para que las páginas
|
||||
# tengan preferencia sobre los endpoints JSON cuando se accede desde navegador.
|
||||
# UI HTMX embebida (Opción A) en rutas amigables para humanos.
|
||||
# Toda la API REST vive bajo /api para eliminar ambigüedad y conflictos de ruta.
|
||||
app.include_router(ui.router)
|
||||
|
||||
app.include_router(agents.router, prefix="/agents", tags=["agents"])
|
||||
app.include_router(executions.invoke_router, prefix="/agents", tags=["agents"])
|
||||
app.include_router(executions.router, prefix="/executions", tags=["executions"])
|
||||
app.include_router(policies.router, prefix="/policies", tags=["policies"])
|
||||
app.include_router(violations.router, prefix="/violations", tags=["violations"])
|
||||
app.include_router(agents.router, prefix="/api/agents", tags=["agents"])
|
||||
app.include_router(executions.invoke_router, prefix="/api/agents", tags=["agents"])
|
||||
app.include_router(executions.router, prefix="/api/executions", tags=["executions"])
|
||||
app.include_router(policies.router, prefix="/api/policies", tags=["policies"])
|
||||
app.include_router(violations.router, prefix="/api/violations", tags=["violations"])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
{% if agent.category %}
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1">Categoría</div>
|
||||
<div class="text-zinc-400 text-xs mb-1">Category</div>
|
||||
<div>{{ agent.category }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -58,7 +58,7 @@
|
||||
<!-- System Prompt colapsado -->
|
||||
<details class="mb-3 group">
|
||||
<summary class="cursor-pointer select-none text-sm font-medium px-4 py-2 bg-zinc-900 hover:bg-zinc-800 border border-zinc-800 rounded-xl flex items-center justify-between">
|
||||
<span>Ver system prompt</span>
|
||||
<span>View system prompt</span>
|
||||
<span class="text-xs text-zinc-500 group-open:hidden">expandir</span>
|
||||
<span class="text-xs text-zinc-500 hidden group-open:inline">colapsar</span>
|
||||
</summary>
|
||||
@@ -85,7 +85,7 @@
|
||||
{% if agent.input_placeholder %}
|
||||
<div><span class="text-zinc-500">Placeholder:</span> {{ agent.input_placeholder }}</div>
|
||||
{% endif %}
|
||||
<div><span class="text-zinc-500">Última actualización:</span> {{ agent.updated_at.strftime('%Y-%m-%d %H:%M') if agent.updated_at else '—' }}</div>
|
||||
<div><span class="text-zinc-500">Last updated:</span> {{ agent.updated_at.strftime('%Y-%m-%d %H:%M') if agent.updated_at else '—' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">Agentes registrados</h1>
|
||||
<h1 class="text-3xl font-semibold mb-6">Registered Agents</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Lista de agentes -->
|
||||
@@ -25,7 +25,7 @@
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-zinc-400">No hay agentes registrados.</div>
|
||||
<div class="px-4 py-6 text-zinc-400">No agents registered.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
@@ -35,7 +35,7 @@
|
||||
<div id="agent-detail" class="border border-zinc-800 bg-zinc-900 rounded-xl p-6 h-full">
|
||||
<div class="h-full flex items-center justify-center">
|
||||
<div class="text-zinc-400 text-sm text-center">
|
||||
Selecciona un agente de la lista para ver su configuración y comportamiento.
|
||||
Select an agent from the list to see its configuration and behavior.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">🤝 Aprobaciones Pendientes (HITL)</h1>
|
||||
<h1 class="text-3xl font-semibold mb-6">🤝 Pending Approvals (HITL)</h1>
|
||||
<p class="text-sm text-zinc-400 mb-4 max-w-2xl">Executions with high-risk actions are paused here. Approve or reject so the graph can continue (or fail). The list updates after every action.</p>
|
||||
|
||||
<div class="space-y-4 max-w-4xl" id="approvals-list">
|
||||
{% for ex in pending %}
|
||||
@@ -16,23 +17,27 @@
|
||||
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button
|
||||
hx-post="/approvals/{{ ex.trace_id }}/approve"
|
||||
hx-post="/api/executions/{{ ex.trace_id }}/approve"
|
||||
hx-vals='{"approved_action_ids": [], "comment": "Aprobado desde UI"}'
|
||||
hx-target="#approvals-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="setTimeout(() => location.reload(), 400)"
|
||||
class="px-3 py-1 text-xs bg-emerald-600 hover:bg-emerald-500 rounded">
|
||||
Aprobar todo
|
||||
</button>
|
||||
<button
|
||||
hx-post="/approvals/{{ ex.trace_id }}/reject"
|
||||
hx-post="/api/executions/{{ ex.trace_id }}/reject"
|
||||
hx-vals='{"reason": "Rechazado desde interfaz web"}'
|
||||
hx-target="#approvals-list"
|
||||
hx-swap="outerHTML"
|
||||
hx-swap="none"
|
||||
hx-on::after-request="setTimeout(() => location.reload(), 400)"
|
||||
class="px-3 py-1 text-xs bg-red-600 hover:bg-red-500 rounded">
|
||||
Rechazar
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-zinc-400">No hay ejecuciones esperando aprobación.</div>
|
||||
<div class="text-zinc-400">No executions awaiting approval.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -3,38 +3,82 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Forja • {{ title or "Gobernanza de Agentes IA" }}</title>
|
||||
<title>Forja • {{ title or "Cadena de Montaje" }}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script>
|
||||
// Tokyo Night Tailwind config (dark purple + yellow highlights)
|
||||
function initializeTokyoNight() {
|
||||
if (window.tailwind && window.tailwind.config) {
|
||||
window.tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
'tokyo': {
|
||||
'bg': '#1a1b26',
|
||||
'surface': '#24283b',
|
||||
'surface2': '#1f2335',
|
||||
'border': '#414868',
|
||||
'purple': '#bb9af7',
|
||||
'blue': '#7aa2f7',
|
||||
'yellow': '#e0af68',
|
||||
'green': '#9ece6a',
|
||||
'red': '#f7768e',
|
||||
'text': '#c0caf5',
|
||||
'muted': '#565f89',
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
window.onload = initializeTokyoNight;
|
||||
</script>
|
||||
<script src="https://unpkg.com/htmx.org@2.0.3/dist/htmx.min.js"></script>
|
||||
<style>
|
||||
body { font-family: ui-sans-serif, system-ui, sans-serif; }
|
||||
.htmx-indicator { display: none; }
|
||||
.htmx-request .htmx-indicator { display: inline; }
|
||||
.htmx-request.htmx-indicator { display: inline; }
|
||||
|
||||
/* Tokyo Night Theme - dark + purple + yellow highlights */
|
||||
:root {
|
||||
--tokyo-bg: #1a1b26;
|
||||
--tokyo-surface: #24283b;
|
||||
--tokyo-surface2: #1f2335;
|
||||
--tokyo-border: #414868;
|
||||
--tokyo-purple: #bb9af7;
|
||||
--tokyo-blue: #7aa2f7;
|
||||
--tokyo-yellow: #e0af68;
|
||||
--tokyo-green: #9ece6a;
|
||||
--tokyo-red: #f7768e;
|
||||
--tokyo-text: #c0caf5;
|
||||
--tokyo-muted: #565f89;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--tokyo-bg);
|
||||
color: var(--tokyo-text);
|
||||
}
|
||||
|
||||
.tokyo-header {
|
||||
background-color: var(--tokyo-surface);
|
||||
border-color: var(--tokyo-border);
|
||||
}
|
||||
|
||||
.tokyo-card {
|
||||
background-color: var(--tokyo-surface);
|
||||
border-color: var(--tokyo-border);
|
||||
}
|
||||
|
||||
.tokyo-purple { color: var(--tokyo-purple); }
|
||||
.tokyo-yellow { color: var(--tokyo-yellow); }
|
||||
.tokyo-blue { color: var(--tokyo-blue); }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-200">
|
||||
<body>
|
||||
<div class="min-h-screen">
|
||||
<header class="border-b border-zinc-800 bg-zinc-900">
|
||||
<div class="max-w-6xl mx-auto px-6 h-14 flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-xl font-semibold tracking-tight">🔨 Forja</span>
|
||||
{% if slogan %}
|
||||
<span class="text-xs text-zinc-500 italic">{{ slogan }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<nav class="flex gap-5 text-sm">
|
||||
<a href="/" class="hover:text-white">Inicio</a>
|
||||
<a href="/agents" class="hover:text-white">Agentes</a>
|
||||
<a href="/run" class="hover:text-white">Ejecutar</a>
|
||||
<a href="/approvals" class="hover:text-white">Aprobaciones</a>
|
||||
<a href="/history" class="hover:text-white">Historial</a>
|
||||
<a href="/policies" class="hover:text-white">Políticas</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="max-w-6xl mx-auto px-6 py-8">
|
||||
<main class="max-w-6xl mx-auto px-6 py-8 text-tokyo-text">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">📜 Historial de Ejecuciones</h1>
|
||||
<h1 class="text-3xl font-semibold mb-6">📜 Execution History</h1>
|
||||
|
||||
<div class="space-y-3 max-w-4xl">
|
||||
{% for ex in executions %}
|
||||
{% set sc = 'emerald' if ex.status == 'completed' else ('amber' if 'awaiting' in ex.status else 'red') %}
|
||||
<div class="border border-zinc-800 bg-zinc-900 rounded-xl p-4 text-sm">
|
||||
<div class="flex justify-between">
|
||||
<div><span class="font-mono">{{ ex.trace_id }}</span></div>
|
||||
<div class="flex justify-between items-center">
|
||||
<div><span class="font-mono text-xs">{{ ex.trace_id }}</span></div>
|
||||
<div class="text-xs text-zinc-400">{{ ex.agent_name }} v{{ ex.agent_version }}</div>
|
||||
</div>
|
||||
<div class="mt-1">Status: <span class="font-mono">{{ ex.status }}</span></div>
|
||||
<div class="mt-1 flex items-center gap-2">
|
||||
<span class="text-xs px-2 py-px rounded bg-{{ sc }}-500/10 text-{{ sc }}-400 font-mono">{{ ex.status }}</span>
|
||||
<span class="text-[10px] text-zinc-500">{{ ex.n_proposed_actions }} acciones</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-zinc-400">No hay ejecuciones todavía.</div>
|
||||
<div class="text-zinc-400">No executions yet.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,53 +1,89 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto pt-16 pb-20 text-center">
|
||||
<!-- Headline -->
|
||||
<h1 class="text-7xl font-semibold tracking-tighter text-white mb-4">
|
||||
Forja
|
||||
</h1>
|
||||
<div class="max-w-[1200px] mx-auto pb-12">
|
||||
|
||||
<p class="text-2xl text-zinc-400 mb-8">
|
||||
Gobernanza profesional para agentes de IA
|
||||
</p>
|
||||
|
||||
<!-- Purpose -->
|
||||
<div class="max-w-2xl mx-auto text-lg text-zinc-400 leading-relaxed mb-12">
|
||||
Plataforma de control para agentes IA. <br>
|
||||
Versionado de definiciones y políticas, guardrails en ejecución,
|
||||
aprobaciones humanas y observabilidad completa.
|
||||
</div>
|
||||
|
||||
<!-- Key capabilities -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 max-w-3xl mx-auto mb-16 text-left">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
|
||||
<div class="font-medium mb-2">Versionado Git-like</div>
|
||||
<div class="text-sm text-zinc-400">Agentes y políticas como YAML versionados. Diffs, histórico y control explícito de cambios.</div>
|
||||
<!-- Branding -->
|
||||
<div class="pt-6 pb-6">
|
||||
<div>
|
||||
<span class="text-6xl font-semibold tracking-tighter">Forja</span>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
|
||||
<div class="font-medium mb-2">Guardrails runtime</div>
|
||||
<div class="text-sm text-zinc-400">Validación automática de entradas y salidas con Presidio y reglas declarativas antes de ejecutar.</div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-5">
|
||||
<div class="font-medium mb-2">Human-in-the-Loop</div>
|
||||
<div class="text-sm text-zinc-400">Pausa automática en acciones de alto riesgo. Aprobación o rechazo con trazabilidad completa.</div>
|
||||
|
||||
<!-- Small legend -->
|
||||
<div class="flex flex-wrap gap-x-5 gap-y-1 text-xs mt-4 text-tokyo-muted">
|
||||
<div><span class="inline-block w-2.5 h-2.5 rounded bg-tokyo-purple mr-1.5 align-middle"></span>Preparation</div>
|
||||
<div><span class="inline-block w-2.5 h-2.5 rounded bg-tokyo-blue mr-1.5 align-middle"></span>Execution Pipeline</div>
|
||||
<div><span class="inline-block w-2.5 h-2.5 rounded bg-tokyo-yellow mr-1.5 align-middle"></span>Human Oversight & Completion</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- CTAs -->
|
||||
<div class="flex flex-wrap justify-center gap-4">
|
||||
<a href="/agents"
|
||||
class="px-6 py-3 bg-white text-black rounded-xl font-medium hover:bg-zinc-200 transition">
|
||||
Ver Agentes
|
||||
</a>
|
||||
<a href="/run"
|
||||
class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-xl font-medium transition">
|
||||
Ejecutar Agente
|
||||
</a>
|
||||
<a href="/approvals"
|
||||
class="px-6 py-3 bg-zinc-800 hover:bg-zinc-700 border border-zinc-700 rounded-xl font-medium transition">
|
||||
Aprobaciones
|
||||
</a>
|
||||
<!-- Phases (no enclosing boxes) -->
|
||||
|
||||
<!-- PHASE 1 -->
|
||||
<div class="mb-8">
|
||||
<div class="mb-4">
|
||||
<span class="text-xs tracking-[2px] text-tokyo-purple font-semibold">PHASE 1 — PREPARATION</span>
|
||||
<div class="text-xl font-semibold tracking-tight mt-0.5">Definition & Guardrails</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-4">
|
||||
<div class="flex-1 min-w-[260px] bg-[#24283b] border border-tokyo-purple/60 rounded-2xl px-5 py-4 hover:border-tokyo-purple hover:shadow-[0_0_0_1px_#bb9af7] transition">
|
||||
<div class="font-semibold">1. Agent Definition</div>
|
||||
<div class="text-xs text-tokyo-muted mt-1">Versioned YAML with prompt, model, output schema and risk threshold</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-[260px] bg-[#24283b] border border-tokyo-purple/60 rounded-2xl px-5 py-4 hover:border-tokyo-purple hover:shadow-[0_0_0_1px_#bb9af7] transition">
|
||||
<div class="font-semibold">2. Policy Application</div>
|
||||
<div class="text-xs text-tokyo-muted mt-1">Guardrails policy loaded and attached to the agent</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHASE 2 -->
|
||||
<div class="mb-8">
|
||||
<div class="mb-4">
|
||||
<span class="text-xs tracking-[2px] text-white font-semibold">PHASE 2 — EXECUTION PIPELINE</span>
|
||||
<div class="text-xl font-semibold tracking-tight mt-0.5 text-white">The Main Processing Line</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-3">
|
||||
<div class="bg-[#24283b] border border-tokyo-border rounded-2xl px-4 py-4 hover:border-tokyo-purple hover:shadow-md transition text-sm">
|
||||
<span class="text-[#bb9af7] font-semibold">3. Input Guardrails</span>
|
||||
<div class="text-[10px] text-tokyo-muted mt-1 leading-snug">Validates input against policy before it reaches the LLM</div>
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-tokyo-border rounded-2xl px-4 py-4 hover:border-tokyo-purple hover:shadow-md transition text-sm">
|
||||
<span class="text-[#e0af68] font-semibold">4. LLM Reasoning</span>
|
||||
<div class="text-[10px] text-tokyo-muted mt-1 leading-snug">The agent generates structured output using the configured model</div>
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-tokyo-border rounded-2xl px-4 py-4 hover:border-tokyo-purple hover:shadow-md transition text-sm">
|
||||
<span class="text-[#9ece6a] font-semibold">5. Output Guardrails</span>
|
||||
<div class="text-[10px] text-tokyo-muted mt-1 leading-snug">Validates the LLM response for safety and correctness</div>
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-tokyo-border rounded-2xl px-4 py-4 hover:border-tokyo-purple hover:shadow-md transition text-sm">
|
||||
<span class="text-[#f7768e] font-semibold">6. Action Proposal</span>
|
||||
<div class="text-[10px] text-tokyo-muted mt-1 leading-snug">Generates actions with risk scores and rollback plans</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PHASE 3 -->
|
||||
<div>
|
||||
<div class="mb-4">
|
||||
<span class="text-xs tracking-[2px] text-tokyo-yellow font-semibold">PHASE 3 — HUMAN OVERSIGHT & COMPLETION</span>
|
||||
<div class="text-xl font-semibold tracking-tight mt-0.5 text-tokyo-yellow">Critical Gate + Finalization</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<a href="/approvals" class="col-span-1 md:col-span-1 bg-[#2a2f45] border-2 border-tokyo-yellow rounded-2xl px-5 py-5 hover:border-tokyo-yellow hover:shadow-[0_0_0_1px_#e0af68] hover:brightness-105 transition group">
|
||||
<div class="font-semibold text-tokyo-yellow text-lg">7. Human Approval Gate</div>
|
||||
<div class="text-xs text-tokyo-muted mt-2 leading-snug">High-risk actions pause here. Human must approve or reject.</div>
|
||||
<div class="mt-3 text-xs text-tokyo-yellow group-hover:underline">Enter approval bay →</div>
|
||||
</a>
|
||||
<div class="bg-[#1f2335] border border-tokyo-green/60 rounded-2xl px-5 py-5 hover:border-tokyo-green hover:shadow-[0_0_0_1px_#9ece6a] transition">
|
||||
<div class="font-semibold">8. Finalization</div>
|
||||
<div class="text-xs text-tokyo-muted mt-1.5">Execution completes with full decision path.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">📐 Políticas</h1>
|
||||
<h1 class="text-3xl font-semibold mb-6">📐 Policies</h1>
|
||||
|
||||
<div class="grid gap-4 max-w-3xl">
|
||||
{% for p in policies %}
|
||||
<div class="border border-zinc-800 bg-zinc-900 rounded-xl p-4">
|
||||
<div class="font-medium">{{ p.name }} <span class="text-xs text-zinc-500">v{{ p.version }}</span></div>
|
||||
<div class="text-sm text-zinc-400 mt-1">{{ p.description or 'Sin descripción' }}</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">{{ p.description or 'No description' }}</div>
|
||||
<div class="text-xs mt-2 text-zinc-500">Validadores: {{ p.validators | length }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-2">▶️ Ejecutar Agente</h1>
|
||||
<p class="text-zinc-400 mb-6">Invoca un agente con todo el gobierno (guardrails + HITL + observabilidad).</p>
|
||||
<h1 class="text-3xl font-semibold mb-2">▶️ Run Agent</h1>
|
||||
<p class="text-zinc-400 mb-6">Invoke an agent with full governance (guardrails + HITL + observability).</p>
|
||||
|
||||
<div class="max-w-2xl">
|
||||
<form hx-post="/run" hx-target="#result" hx-swap="innerHTML" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Agente</label>
|
||||
<label class="block text-sm mb-1">Agent</label>
|
||||
<select name="agent_name" class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 text-sm">
|
||||
{% for a in agents %}
|
||||
<option value="{{ a.name }}">{{ a.name }} (v{{ a.version }})</option>
|
||||
@@ -16,14 +16,14 @@
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Input / Escenario</label>
|
||||
<label class="block text-sm mb-1">Input / Scenario</label>
|
||||
<textarea name="input" rows="4" class="w-full bg-zinc-900 border border-zinc-700 rounded-lg px-3 py-2 font-mono text-sm"
|
||||
placeholder='{"scenario": "01_sip_registration_drop"}'>{"scenario": "01_sip_registration_drop"}</textarea>
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="px-4 py-2 bg-white text-black rounded-lg text-sm font-medium flex items-center gap-2 hover:bg-zinc-200">
|
||||
<span>Invocar</span>
|
||||
<span>Invoke</span>
|
||||
<span class="htmx-indicator">...</span>
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -20,12 +20,12 @@ TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||
templates = Jinja2Templates(directory=str(TEMPLATES_DIR))
|
||||
|
||||
SLOGANS = [
|
||||
"Forjando agentes con criterio",
|
||||
"Agentes con forja, no con fe",
|
||||
"Versión. Valida. Aprueba.",
|
||||
"Cada decisión bien templada",
|
||||
"Gobernanza que protege",
|
||||
"Del prototipo al control real",
|
||||
"Forging agents with judgment",
|
||||
"Agents forged, not guessed",
|
||||
"Version. Validate. Approve.",
|
||||
"Every decision well-tempered",
|
||||
"Governance that protects",
|
||||
"From prototype to real control",
|
||||
]
|
||||
|
||||
def get_slogan() -> str:
|
||||
@@ -37,7 +37,7 @@ def get_slogan() -> str:
|
||||
async def home(request: Request) -> HTMLResponse:
|
||||
return templates.TemplateResponse(
|
||||
"home.html",
|
||||
{"request": request, "title": "Inicio", "slogan": get_slogan()},
|
||||
{"request": request, "title": "Home", "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ async def agents_page(request: Request) -> HTMLResponse:
|
||||
agents = registry.list_agents()
|
||||
return templates.TemplateResponse(
|
||||
"agents.html",
|
||||
{"request": request, "title": "Agentes", "agents": agents, "slogan": get_slogan()},
|
||||
{"request": request, "title": "Agents", "agents": agents, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ async def agent_detail(request: Request, name: str) -> HTMLResponse:
|
||||
try:
|
||||
agent = registry.get_agent(name)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse('<div class="text-red-400">Agente no encontrado</div>', status_code=404)
|
||||
return HTMLResponse('<div class="text-red-400">Agent not found</div>', status_code=404)
|
||||
|
||||
# Si viene por HTMX, devolvemos solo el fragmento (sin layout)
|
||||
if request.headers.get("HX-Request"):
|
||||
@@ -79,7 +79,7 @@ async def run_page(request: Request) -> HTMLResponse:
|
||||
agents = registry.list_agents()
|
||||
return templates.TemplateResponse(
|
||||
"run.html",
|
||||
{"request": request, "title": "Ejecutar", "agents": agents, "slogan": get_slogan()},
|
||||
{"request": request, "title": "Run", "agents": agents, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@@ -111,16 +111,64 @@ async def run_invoke(
|
||||
except Exception as e:
|
||||
return HTMLResponse(f'<div class="text-red-400">Error: {str(e)[:300]}</div>', status_code=400)
|
||||
|
||||
# Resultado simple pero informativo
|
||||
# Resultado enriquecido (FASE I)
|
||||
status_colors = {
|
||||
"completed": "emerald",
|
||||
"awaiting_approval": "amber",
|
||||
"failed": "red",
|
||||
"blocked_by_guardrail": "red",
|
||||
}
|
||||
color = status_colors.get(execution.status, "zinc")
|
||||
|
||||
steps_html = ""
|
||||
for step in execution.decision_path:
|
||||
dur = f"{step.duration_ms}ms" if step.duration_ms else ""
|
||||
extra = ""
|
||||
if step.n_approved is not None:
|
||||
extra = f" · {step.n_approved} acciones aprobadas"
|
||||
if step.rejected:
|
||||
extra = " · RECHAZADO"
|
||||
if step.hitl:
|
||||
extra = " · HITL"
|
||||
steps_html += f'<div class="flex justify-between text-xs py-0.5 border-b border-zinc-800"><span class="font-mono">{step.node}</span><span class="text-zinc-500">{dur}{extra}</span></div>'
|
||||
|
||||
violations_html = ""
|
||||
if execution.violations:
|
||||
for v in execution.violations:
|
||||
sev = "red" if v.severity == "block" else "amber"
|
||||
violations_html += f'<div class="text-xs text-{sev}-400">• {v.validator}: {v.message} ({v.severity})</div>'
|
||||
|
||||
actions_html = ""
|
||||
if execution.proposed_actions:
|
||||
for a in execution.proposed_actions:
|
||||
risk = a.risk_score
|
||||
badge = "emerald" if risk < 3 else ("amber" if risk < 5 else "red")
|
||||
actions_html += f'<div class="text-xs bg-zinc-950 border border-zinc-700 rounded p-2">• {a.description} <span class="font-mono text-{badge}-400">(risk={risk})</span></div>'
|
||||
|
||||
needs_html = ""
|
||||
if execution.needs_human_for:
|
||||
needs_html = f'<div class="mt-2 text-amber-400 text-sm">⏸️ Paused for human approval ({len(execution.needs_human_for)} action(s)). Go to <a href="/approvals" class="underline">Approvals</a>.</div>'
|
||||
|
||||
html = f"""
|
||||
<div class="border border-zinc-700 rounded-xl p-4 bg-zinc-900">
|
||||
<div class="font-medium">Ejecución completada</div>
|
||||
<div class="text-xs text-zinc-400 mt-1">trace_id: {execution.trace_id}</div>
|
||||
<div class="mt-2 text-sm">Status: <span class="font-mono">{execution.status}</span></div>
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer text-xs text-zinc-400">Ver resultado completo</summary>
|
||||
<pre class="text-[10px] bg-black p-2 mt-1 rounded overflow-auto">{execution.model_dump_json(indent=2)}</pre>
|
||||
</details>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-semibold">Execution</span>
|
||||
<span class="text-xs px-2 py-0.5 rounded bg-{color}-500/20 text-{color}-400 font-mono">{execution.status}</span>
|
||||
</div>
|
||||
<div class="text-[10px] text-zinc-500 mt-0.5">trace_id: {execution.trace_id}</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<div class="text-xs uppercase tracking-wider text-zinc-500 mb-1">Decision path</div>
|
||||
<div class="font-mono text-[10px] bg-black/60 rounded p-2">{steps_html or '<span class="text-zinc-500">no steps</span>'}</div>
|
||||
</div>
|
||||
|
||||
{f'<div class="mt-3"><div class="text-xs uppercase tracking-wider text-red-500 mb-1">Violations</div>{violations_html}</div>' if violations_html else ''}
|
||||
|
||||
{f'<div class="mt-3"><div class="text-xs uppercase tracking-wider text-amber-500 mb-1">Proposed actions</div><div class="space-y-1">{actions_html}</div></div>' if actions_html else ''}
|
||||
|
||||
{needs_html}
|
||||
|
||||
{f'<div class="mt-3 text-emerald-400 text-sm">✓ Completed with {len(execution.proposed_actions or [])} actions.</div>' if execution.status == 'completed' else ''}
|
||||
</div>
|
||||
"""
|
||||
return HTMLResponse(html)
|
||||
@@ -132,7 +180,7 @@ async def policies_page(request: Request) -> HTMLResponse:
|
||||
policies = policy_store.list_policies()
|
||||
return templates.TemplateResponse(
|
||||
"policies.html",
|
||||
{"request": request, "title": "Políticas", "policies": policies, "slogan": get_slogan()},
|
||||
{"request": request, "title": "Policies", "policies": policies, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@@ -142,7 +190,7 @@ async def history_page(request: Request) -> HTMLResponse:
|
||||
executions = read_execution_summaries(data_dir)[:50]
|
||||
return templates.TemplateResponse(
|
||||
"history.html",
|
||||
{"request": request, "title": "Historial", "executions": executions, "slogan": get_slogan()},
|
||||
{"request": request, "title": "History", "executions": executions, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@@ -155,29 +203,5 @@ async def approvals_page(request: Request) -> HTMLResponse:
|
||||
pending = [e for e in all_execs if e.status == "awaiting_approval"]
|
||||
return templates.TemplateResponse(
|
||||
"approvals.html",
|
||||
{"request": request, "title": "Aprobaciones", "pending": pending, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/approvals/{trace_id}/approve", response_class=HTMLResponse)
|
||||
async def approve_execution(trace_id: str, request: Request) -> HTMLResponse:
|
||||
# Nota: La reanudación completa de HITL desde la UI requiere reconstruir
|
||||
# agent_def + policy a partir del trace_id. Por ahora mostramos lista actualizada.
|
||||
data_dir = get_settings().data_dir
|
||||
all_execs = read_execution_summaries(data_dir)
|
||||
pending = [e for e in all_execs if e.status == "awaiting_approval"]
|
||||
return templates.TemplateResponse(
|
||||
"approvals.html",
|
||||
{"request": request, "title": "Aprobaciones", "pending": pending, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/approvals/{trace_id}/reject", response_class=HTMLResponse)
|
||||
async def reject_execution(trace_id: str, request: Request) -> HTMLResponse:
|
||||
data_dir = get_settings().data_dir
|
||||
all_execs = read_execution_summaries(data_dir)
|
||||
pending = [e for e in all_execs if e.status == "awaiting_approval"]
|
||||
return templates.TemplateResponse(
|
||||
"approvals.html",
|
||||
{"request": request, "title": "Aprobaciones", "pending": pending, "slogan": get_slogan()},
|
||||
{"request": request, "title": "Approvals", "pending": pending, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
+4
-6
@@ -1,16 +1,14 @@
|
||||
# Componentes de Forja y su interrelación (bajo nivel)
|
||||
|
||||
> **Alcance.** Este documento es la **referencia de cableado**: módulos exactos,
|
||||
> firmas, el grafo de dependencias de imports, el grafo de inyección de
|
||||
> dependencias, los contratos entre capas y las cadenas de llamada de cada
|
||||
> endpoint. Es preciso, no narrativo.
|
||||
> **Nota (post-simplificación 2026):** Este documento describe el cableado del
|
||||
> núcleo. La UI ahora es HTMX embebida en el propio core (no hay dashboard
|
||||
> separado). La API REST está bajo el prefijo `/api`.
|
||||
>
|
||||
> - ¿Quieres la historia y el "por qué"? → [`docs/explicacion.md`](explicacion.md).
|
||||
> - ¿Las decisiones técnicas resumidas? → [`ARCHITECTURE.md`](../ARCHITECTURE.md).
|
||||
> - ¿Cómo arrancarlo? → [`README.md`](../README.md).
|
||||
>
|
||||
> Rutas relativas a `core/src/ forja_core/` salvo que se diga otra cosa.
|
||||
> Refleja el estado del repo en `v0.1.0`.
|
||||
> Rutas relativas a `core/src/forja_core/` salvo que se diga otra cosa.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+9
-5
@@ -1,14 +1,18 @@
|
||||
# Forja explicado de principio a fin
|
||||
|
||||
> **Nota de estado (mayo 2026):** Este documento fue escrito para la arquitectura
|
||||
> anterior (dos servicios + Streamlit/NiceGUI). Forja ahora es un **único servicio
|
||||
> FastAPI** con UI HTMX embebida. El núcleo (runtime, guardrails, HITL, versionado)
|
||||
> sigue siendo el mismo. Algunas secciones de UI y despliegue están desactualizadas.
|
||||
>
|
||||
> Si solo quieres arrancarlo, ve al [`README.md`](../README.md).
|
||||
|
||||
> **Para quién es esto.** Una guía didáctica para alguien que llega nuevo al
|
||||
> proyecto —técnico o no— y quiere entender *qué hace*, *por qué está hecho así*
|
||||
> y *cómo encajan las piezas* sin tener que leer todo el código primero.
|
||||
>
|
||||
> Si solo quieres arrancarlo, ve al [`README.md`](../README.md). Si quieres las
|
||||
> decisiones técnicas en bruto, ve a [`ARCHITECTURE.md`](../ARCHITECTURE.md). Si
|
||||
> quieres la referencia de cableado a bajo nivel (módulos, firmas, grafos de
|
||||
> dependencias, cadenas de llamada), ve a [`docs/componentes.md`](componentes.md).
|
||||
> Este documento está en medio: cuenta la historia.
|
||||
> Decisiones técnicas: [`ARCHITECTURE.md`](../ARCHITECTURE.md).
|
||||
> Cableado de bajo nivel: [`docs/componentes.md`](componentes.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forja — Progreso y Próximos Pasos</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600&display=swap');
|
||||
|
||||
:root {
|
||||
--primary: #0ea5e9;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system_ui, sans-serif;
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: 'Space Grotesk', 'Inter', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: 'Space Grotesk', 'Inter', sans-serif;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: 0.75rem;
|
||||
padding: 0.125rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.forja-gradient {
|
||||
background: linear-gradient(135deg, #0ea5e9, #3b82f6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
}
|
||||
|
||||
.card {
|
||||
transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
box-shadow 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.05), 0 8px 10px -6px rgb(0 0 0 / 0.05);
|
||||
}
|
||||
|
||||
.metric {
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.nav-active {
|
||||
border-bottom: 3px solid #0ea5e9;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
transition: width 1s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||
}
|
||||
|
||||
.feature-icon {
|
||||
width: 2.25rem;
|
||||
height: 2.25rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 9999px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-200">
|
||||
<!-- Header -->
|
||||
<div class="border-b border-zinc-800 bg-zinc-900/70 backdrop-blur-lg sticky top-0 z-50">
|
||||
<div class="max-w-6xl mx-auto px-6 py-5 flex items-center justify-between">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="w-10 h-10 rounded-2xl bg-sky-500 flex items-center justify-center shadow-inner">
|
||||
<i class="fa-solid fa-hammer text-white text-3xl"></i>
|
||||
</div>
|
||||
<div>
|
||||
<span class="font-display text-3xl font-semibold tracking-tighter">Forja</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-x-2 text-sm">
|
||||
<div class="px-3 py-1.5 bg-zinc-900 border border-zinc-800 rounded-2xl flex items-center gap-x-2">
|
||||
<div class="w-2 h-2 bg-emerald-400 rounded-full animate-pulse"></div>
|
||||
<span class="font-medium text-emerald-400 text-xs tracking-wider">v0.2 — EN PRODUCCIÓN</span>
|
||||
</div>
|
||||
<div class="text-zinc-500 text-xs px-2">23 mayo 2026</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="max-w-6xl mx-auto px-6 pt-10 pb-24">
|
||||
|
||||
<!-- Hero -->
|
||||
<div class="flex flex-col lg:flex-row gap-8 items-start">
|
||||
<div class="flex-1">
|
||||
<div class="inline-flex items-center gap-x-2 px-3 py-1 rounded-3xl bg-zinc-900 border border-zinc-800 text-xs mb-4">
|
||||
<i class="fa-solid fa-sync fa-spin-pulse text-sky-400"></i>
|
||||
<span class="font-semibold tracking-widest">GRAN REFACTORIZACIÓN COMPLETADA</span>
|
||||
</div>
|
||||
|
||||
<h1 class="font-display text-6xl lg:text-7xl font-semibold tracking-tighter leading-none">
|
||||
Forja<br>
|
||||
<span class="forja-gradient">está lista</span>
|
||||
</h1>
|
||||
|
||||
<p class="mt-4 max-w-lg text-xl text-zinc-400">
|
||||
Plataforma de gobernanza genérica para agentes IA de cualquier tipo.
|
||||
Renombrada, generalizada y con editores gráficos completos.
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-x-3 mt-8">
|
||||
<a href="https://github.com/anomalyco/opencode"
|
||||
class="inline-flex items-center gap-x-2 px-5 py-3 rounded-3xl bg-white text-zinc-950 font-semibold text-sm hover:bg-zinc-100 transition-colors">
|
||||
<i class="fa-brands fa-github"></i>
|
||||
<span>Ver en GitHub</span>
|
||||
</a>
|
||||
<button onclick="document.getElementById('proximos-pasos').scrollIntoView({behavior:'smooth'})"
|
||||
class="inline-flex items-center gap-x-2 px-5 py-3 rounded-3xl border border-zinc-700 hover:bg-zinc-900 text-sm font-medium transition-colors">
|
||||
<span>Ver próximos pasos</span>
|
||||
<i class="fa-solid fa-arrow-down"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="lg:w-80 w-full">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-5">
|
||||
<div class="text-xs uppercase tracking-[1px] text-zinc-500 mb-3 font-medium">Progreso general</div>
|
||||
|
||||
<div class="flex items-baseline gap-x-2">
|
||||
<div class="text-6xl font-semibold tabular-nums tracking-tighter">82</div>
|
||||
<div class="text-2xl font-medium text-zinc-400">/100</div>
|
||||
</div>
|
||||
|
||||
<div class="h-2.5 bg-zinc-800 rounded-full mt-3 overflow-hidden">
|
||||
<div class="h-2.5 bg-gradient-to-r from-sky-400 to-blue-500 rounded-full progress-bar" style="width: 82%"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-3 gap-4 mt-6 text-center">
|
||||
<div>
|
||||
<div class="text-xs text-zinc-500">Core</div>
|
||||
<div class="font-semibold text-xl">100%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-500">UI Gráfica</div>
|
||||
<div class="font-semibold text-xl">68%</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-500">Docs</div>
|
||||
<div class="font-semibold text-xl">65%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Métricas rápidas -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mt-12">
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="feature-icon bg-emerald-500/10 text-emerald-400"><i class="fa-solid fa-check-double"></i></div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400">Renombrado global</div>
|
||||
<div class="font-semibold">483 → 0 referencias antiguas</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="feature-icon bg-amber-500/10 text-amber-400"><i class="fa-solid fa-edit"></i></div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400">Editores gráficos</div>
|
||||
<div class="font-semibold">2 páginas nuevas funcionales</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="feature-icon bg-violet-500/10 text-violet-400"><i class="fa-solid fa-globe"></i></div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400">Generalización</div>
|
||||
<div class="font-semibold">Cualquier tipo de agente</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="feature-icon bg-emerald-500/10 text-emerald-400"><i class="fa-solid fa-palette"></i></div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400">Modernización de interfaz</div>
|
||||
<div class="font-semibold">Migración Streamlit → NiceGUI</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||
<div class="flex items-center gap-x-3">
|
||||
<div class="feature-icon bg-sky-500/10 text-sky-400"><i class="fa-solid fa-database"></i></div>
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400">Write APIs</div>
|
||||
<div class="font-semibold">POST /versions + upsert</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Qué se hizo -->
|
||||
<div class="mt-16">
|
||||
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6 flex items-center gap-x-3">
|
||||
<span>Lo que se completó</span>
|
||||
<span class="text-xs px-3 py-1 bg-emerald-400/10 text-emerald-400 rounded-2xl font-mono tracking-wider">FASE 1 + 2 + 3</span>
|
||||
</h2>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-4">
|
||||
<!-- Columna 1 -->
|
||||
<div class="space-y-4">
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
|
||||
<div class="font-semibold flex items-center gap-x-2 text-lg">
|
||||
<i class="fa-solid fa-sync text-sky-400"></i>
|
||||
<span>Renombrado completo a "Forja"</span>
|
||||
</div>
|
||||
<ul class="mt-4 text-sm space-y-2 text-zinc-300">
|
||||
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5">•</span> Paquetes Python: <span class="mono font-medium">forja_core</span> y <span class="mono font-medium">forja_dashboard</span></li>
|
||||
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5">•</span> Imágenes Docker, contenedores y URLs (<code>forja-core:dev</code>)</li>
|
||||
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5">•</span> Variables de entorno (<code>FORJA_CORE_URL</code>)</li>
|
||||
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5">•</span> pyproject, Dockerfiles, docker-compose, Makefile</li>
|
||||
<li class="flex gap-x-2"><span class="text-sky-400 mt-0.5">•</span> 300+ archivos actualizados (código + docs activos)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
|
||||
<div class="font-semibold flex items-center gap-x-2 text-lg">
|
||||
<i class="fa-solid fa-universal-access text-violet-400"></i>
|
||||
<span>Generalización para cualquier tipo de agente</span>
|
||||
</div>
|
||||
<div class="mt-4 text-sm text-zinc-300">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Campos UI opcionales en AgentDefinition</div>
|
||||
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">input_label / placeholder / category / tags / icon</div>
|
||||
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Execute page 100% genérico</div>
|
||||
<div class="px-3 py-1 bg-zinc-800 rounded-2xl text-xs">Posicionamiento como plano de control reutilizable</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Columna 2 -->
|
||||
<div class="space-y-4">
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
|
||||
<div class="font-semibold flex items-center gap-x-2 text-lg">
|
||||
<i class="fa-solid fa-pencil-ruler text-amber-400"></i>
|
||||
<span>Editores gráficos completos (la gran novedad)</span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 grid grid-cols-1 gap-3">
|
||||
<div class="bg-zinc-950 border border-zinc-800 p-4 rounded-2xl">
|
||||
<div class="font-medium text-amber-300 flex items-center gap-x-2">
|
||||
<i class="fa-solid fa-hammer"></i>
|
||||
<span>6_🔨_Forjar_Agente.py</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400 mt-1">Form completo + LLM config + schema JSON + multiselect de guardrails + fork + versionado</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-zinc-950 border border-zinc-800 p-4 rounded-2xl">
|
||||
<div class="font-medium text-amber-300 flex items-center gap-x-2">
|
||||
<i class="fa-solid fa-shield-halved"></i>
|
||||
<span>7_🛡️_Forjar_Politica.py</span>
|
||||
</div>
|
||||
<div class="text-xs text-zinc-400 mt-1">Constructor dinámico de validadores (input/output) + /validators endpoint + configs JSON</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-6">
|
||||
<div class="font-semibold text-lg">Backend de escritura</div>
|
||||
<div class="text-sm mt-3 space-y-1.5 text-zinc-300">
|
||||
<div>✓ <code class="mono text-xs">POST /agents/{name}/versions</code> + upsert</div>
|
||||
<div>✓ <code class="mono text-xs">POST /policies/{name}/versions</code> + upsert simétrico</div>
|
||||
<div>✓ <code class="mono text-xs">GET /policies/validators</code> (metadata para UI)</div>
|
||||
<div>✓ <code class="mono text-xs">FileSystemPolicyStore.upsert_version()</code></div>
|
||||
<div>✓ Docker mounts cambiados a <strong>:rw</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado actual -->
|
||||
<div class="mt-16">
|
||||
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6">Estado actual (23 mayo 2026)</h2>
|
||||
|
||||
<div class="bg-zinc-900 border border-zinc-800 rounded-3xl p-7">
|
||||
<div class="grid md:grid-cols-3 gap-8">
|
||||
<div>
|
||||
<div class="uppercase text-xs font-semibold tracking-wider text-emerald-400">Compila y funciona</div>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Todos los imports y paquetes renombrados</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>py_compile 100% limpio</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Editores guardan YAMLs reales</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-emerald-400 mt-0.5"></i> <span>Execute page usa metadata del agente</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="uppercase text-xs font-semibold tracking-wider text-amber-400">Listo para usar</div>
|
||||
<ul class="mt-3 space-y-2 text-sm">
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span><code>docker compose up</code> (con :rw)</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>Registro + Ejecución + Aprobaciones</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>Editores en barra lateral (páginas 6 y 7)</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-check text-amber-400 mt-0.5"></i> <span>README y docs principales actualizados</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="uppercase text-xs font-semibold tracking-wider text-zinc-400">Pendiente de verificación completa</div>
|
||||
<ul class="mt-3 space-y-2 text-sm text-zinc-400">
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>make test-all (requiere deps pesadas)</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Smoke test con Docker real (guardrails-ai)</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Actualización de walkthrough.html</span></li>
|
||||
<li class="flex items-start gap-x-2"><i class="fa-solid fa-clock mt-0.5"></i> <span>Tests específicos de los nuevos endpoints</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Próximos pasos -->
|
||||
<div id="proximos-pasos" class="mt-16">
|
||||
<h2 class="section-header text-3xl font-semibold tracking-tight mb-6">Próximos pasos recomendados</h2>
|
||||
|
||||
<div class="space-y-3">
|
||||
<!-- Paso 1 -->
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
|
||||
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">1</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Verificación completa de calidad</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">Ejecutar <span class="mono bg-zinc-950 px-1.5 py-px rounded">make install && make lint && make test-all</span> + smoke con docker compose en entorno limpio.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paso 2 -->
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
|
||||
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">2</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Mejora de los editores (UX)</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">Añadir editor JSON más amigable (ace o monaco), validación en tiempo real de schemas, preview de agente antes de guardar, y botón “Probar inmediatamente”.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Paso 3 -->
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
|
||||
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">3</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Añadir más plantillas de agentes</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">Crear 2-3 agentes genéricos de ejemplo (resumidor, revisor de código, chatbot con guardrails) + sus políticas base.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
|
||||
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">4</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Soporte de namespaces / multi-proyecto</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">Estructura opcional <span class="mono text-xs">agents/<proyecto>/<agente></span> para que una sola instancia de Forja sirva a múltiples equipos.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-zinc-900 border border-zinc-800 rounded-3xl p-5 flex gap-5 items-start">
|
||||
<div class="w-7 h-7 flex-shrink-0 rounded-2xl bg-blue-500 flex items-center justify-center text-xs font-bold text-white mt-0.5">5</div>
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold">Documentación y walkthrough actualizado</div>
|
||||
<div class="text-sm text-zinc-400 mt-1">Regenerar o actualizar <span class="mono">docs/walkthrough.html</span> y <span class="mono">docs/explicacion.md</span> con los editores y el nuevo nombre.</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Notas técnicas -->
|
||||
<div class="mt-16 text-xs text-zinc-500 border-t border-zinc-800 pt-8">
|
||||
<div class="flex flex-wrap gap-x-8 gap-y-2">
|
||||
<div><strong>Paquetes:</strong> <span class="mono">forja_core</span> / <span class="mono">forja_web</span> (NiceGUI) + <span class="mono">forja_common</span></div>
|
||||
<div><strong>Python:</strong> 3.11+</div>
|
||||
<div><strong>UI:</strong> NiceGUI (reemplazo moderno de Streamlit) + Tailwind</div>
|
||||
<div><strong>Runtime:</strong> LangGraph + FastAPI</div>
|
||||
<div><strong>Persistencia:</strong> YAML versionado + SQLite checkpoints</div>
|
||||
</div>
|
||||
<div class="mt-3 text-[10px] text-zinc-600">Actualizado el 23 de mayo de 2026 — Migración a NiceGUI iniciada, CLI removido por decisión de producto.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Tailwind script
|
||||
function initializeTailwind() {
|
||||
document.documentElement.style.setProperty('--accent', '#0ea5e9');
|
||||
}
|
||||
|
||||
// Simple confetti on load for celebration
|
||||
function celebrate() {
|
||||
if (Math.random() > 0.7) {
|
||||
console.log('%c[Forja] ¡Refactorización completada con éxito!', 'color:#64748b;font-size:9px');
|
||||
}
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
initializeTailwind();
|
||||
celebrate();
|
||||
};
|
||||
|
||||
// Keyboard hint
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === '/' && document.activeElement.tagName === 'BODY') {
|
||||
e.preventDefault();
|
||||
const next = document.getElementById('proximos-pasos');
|
||||
if (next) next.scrollIntoView({behavior: 'smooth'});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,242 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Forja — Seguimiento de Progreso</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Space+Grotesk:wght@500;600&display=swap');
|
||||
|
||||
:root {
|
||||
--tokyo-bg: #1a1b26;
|
||||
--tokyo-surface: #24283b;
|
||||
--tokyo-border: #414868;
|
||||
--tokyo-purple: #bb9af7;
|
||||
--tokyo-blue: #7aa2f7;
|
||||
--tokyo-yellow: #e0af68;
|
||||
--tokyo-text: #c0caf5;
|
||||
--tokyo-muted: #565f89;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system_ui, sans-serif;
|
||||
background-color: var(--tokyo-bg);
|
||||
color: var(--tokyo-text);
|
||||
}
|
||||
|
||||
.font-display {
|
||||
font-family: 'Space Grotesk', 'Inter', sans-serif;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tokyo-card {
|
||||
background-color: var(--tokyo-surface);
|
||||
border: 1px solid var(--tokyo-border);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
font-family: 'Space Grotesk', 'Inter', sans-serif;
|
||||
letter-spacing: -0.025em;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
transition: width 1s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
}
|
||||
|
||||
.station {
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.milestone {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.milestone::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 15px;
|
||||
top: 0;
|
||||
bottom: -24px;
|
||||
width: 2px;
|
||||
background: var(--tokyo-border);
|
||||
}
|
||||
|
||||
.milestone:last-child::before {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen">
|
||||
<div class="max-w-5xl mx-auto px-6 py-12">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="flex items-center gap-4 mb-10">
|
||||
<div class="w-14 h-14 rounded-2xl bg-gradient-to-br from-[#bb9af7] to-[#7aa2f7] flex items-center justify-center">
|
||||
<span class="text-3xl">🔨</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 class="font-display text-5xl font-semibold tracking-tighter">Forja</h1>
|
||||
<p class="text-[#565f89] text-xl">Seguimiento de Progreso</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Vision -->
|
||||
<div class="tokyo-card rounded-3xl p-8 mb-10">
|
||||
<div class="flex items-center gap-3 mb-4">
|
||||
<div class="px-3 py-1 bg-[#bb9af7]/10 text-[#bb9af7] rounded-full text-sm font-medium">Visión Actual</div>
|
||||
</div>
|
||||
<h2 class="text-3xl font-semibold tracking-tight mb-3">Cadena de Montaje (Assembly Line)</h2>
|
||||
<p class="text-[#c0caf5]/80 text-lg max-w-3xl">
|
||||
La interfaz se ha transformado en una <strong>vista única de fábrica</strong>.
|
||||
El objetivo es que el usuario entienda de un vistazo cómo se "forja" un agente,
|
||||
mostrando solo los bloques que generan <strong>inputs configurables</strong> o
|
||||
<strong>outputs significativos</strong>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Timeline -->
|
||||
<div class="mb-12">
|
||||
<h3 class="font-display text-2xl font-semibold tracking-tight mb-6">Evolución Principal</h3>
|
||||
|
||||
<div class="space-y-6">
|
||||
|
||||
<!-- Milestone 1 -->
|
||||
<div class="milestone flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-[#bb9af7] flex-shrink-0 flex items-center justify-center text-[#1a1b26] font-bold text-sm">1</div>
|
||||
<div class="tokyo-card rounded-2xl p-5 flex-1">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-semibold">Eliminación de la Navbar</div>
|
||||
<div class="text-sm text-[#565f89] mt-1">Se eliminó completamente la navegación superior tradicional.</div>
|
||||
</div>
|
||||
<div class="text-xs text-[#565f89] font-mono">Paso 1</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone 2 -->
|
||||
<div class="milestone flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-[#bb9af7] flex-shrink-0 flex items-center justify-center text-[#1a1b26] font-bold text-sm">2</div>
|
||||
<div class="tokyo-card rounded-2xl p-5 flex-1">
|
||||
<div class="font-semibold">Paso a Vista Única de Fábrica</div>
|
||||
<div class="text-sm text-[#565f89] mt-1">Toda la experiencia se concentra en una "cadena de montaje" visual en lugar de páginas separadas.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone 3 -->
|
||||
<div class="milestone flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-[#bb9af7] flex-shrink-0 flex items-center justify-center text-[#1a1b26] font-bold text-sm">3</div>
|
||||
<div class="tokyo-card rounded-2xl p-5 flex-1">
|
||||
<div class="font-semibold">Eliminación de las Cajas de Fases</div>
|
||||
<div class="text-sm text-[#565f89] mt-1">Se quitaron los contenedores grandes que englobaban cada fase. Ahora solo quedan los encabezados de fase + las estaciones sueltas.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone 4 -->
|
||||
<div class="milestone flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-[#bb9af7] flex-shrink-0 flex items-center justify-center text-[#1a1b26] font-bold text-sm">4</div>
|
||||
<div class="tokyo-card rounded-2xl p-5 flex-1">
|
||||
<div class="font-semibold">Unificación de Fondo + Colores Llamativos en Texto</div>
|
||||
<div class="text-sm text-[#565f89] mt-1">Todas las cajas de la línea de ensamblado usan el mismo gris. Los títulos tienen colores muy saturados y distintos (cyan, púrpura, amarillo, verde, rojo).</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Milestone 5 -->
|
||||
<div class="milestone flex gap-4">
|
||||
<div class="w-8 h-8 rounded-full bg-[#bb9af7] flex-shrink-0 flex items-center justify-center text-[#1a1b26] font-bold text-sm">5</div>
|
||||
<div class="tokyo-card rounded-2xl p-5 flex-1">
|
||||
<div class="font-semibold">Limpieza de Estaciones sin Valor de I/O</div>
|
||||
<div class="text-sm text-[#565f89] mt-1">
|
||||
Se eliminaron los bloques que no generan inputs configurables ni outputs significativos:
|
||||
<span class="font-medium text-[#e0af68]">Input Intake</span> y
|
||||
<span class="font-medium text-[#e0af68]">Audit & Logging</span>.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Current Assembly Line -->
|
||||
<div class="mb-12">
|
||||
<h3 class="font-display text-2xl font-semibold tracking-tight mb-6">Estado Actual de la Cadena de Montaje</h3>
|
||||
|
||||
<div class="tokyo-card rounded-3xl p-8">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
|
||||
<!-- Phase 1 -->
|
||||
<div>
|
||||
<div class="text-[#bb9af7] text-xs tracking-[1.5px] font-semibold mb-2">FASE 1 — PREPARACIÓN</div>
|
||||
<div class="space-y-2">
|
||||
<div class="bg-[#24283b] border border-[#bb9af7]/40 rounded-xl px-4 py-3 text-sm">1. Agent Definition</div>
|
||||
<div class="bg-[#24283b] border border-[#bb9af7]/40 rounded-xl px-4 py-3 text-sm">2. Policy Application</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 2 -->
|
||||
<div>
|
||||
<div class="text-[#7aa2f7] text-xs tracking-[1.5px] font-semibold mb-2">FASE 2 — LÍNEA DE ENSAMBLADO</div>
|
||||
<div class="space-y-2">
|
||||
<div class="bg-[#24283b] border border-[#7aa2f7]/40 rounded-xl px-4 py-3 text-sm">
|
||||
<span class="text-[#bb9af7]">3.</span> Input Guardrails
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-[#7aa2f7]/40 rounded-xl px-4 py-3 text-sm">
|
||||
<span class="text-[#e0af68]">4.</span> LLM Reasoning
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-[#7aa2f7]/40 rounded-xl px-4 py-3 text-sm">
|
||||
<span class="text-[#9ece6a]">5.</span> Output Guardrails
|
||||
</div>
|
||||
<div class="bg-[#24283b] border border-[#7aa2f7]/40 rounded-xl px-4 py-3 text-sm">
|
||||
<span class="text-[#f7768e]">6.</span> Action Proposal
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Phase 3 -->
|
||||
<div>
|
||||
<div class="text-[#e0af68] text-xs tracking-[1.5px] font-semibold mb-2">FASE 3 — SUPERVISIÓN HUMANA</div>
|
||||
<div class="space-y-2">
|
||||
<div class="bg-[#24283b] border border-[#e0af68]/40 rounded-xl px-4 py-3 text-sm">7. Human Approval Gate</div>
|
||||
<div class="bg-[#24283b] border border-[#9ece6a]/40 rounded-xl px-4 py-3 text-sm">8. Finalization</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="mt-6 pt-6 border-t border-[#414868] text-xs text-[#565f89]">
|
||||
Total de estaciones activas: <span class="font-semibold text-[#c0caf5]">8</span>
|
||||
(todas producen inputs configurables o outputs significativos)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Principles -->
|
||||
<div class="grid md:grid-cols-2 gap-6">
|
||||
<div class="tokyo-card rounded-3xl p-6">
|
||||
<div class="text-sm font-medium text-[#bb9af7] mb-2">PRINCIPIOS APLICADOS</div>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li class="flex gap-2">• <span>Strict adherence to <strong>karpathy.md</strong> guidelines</span></li>
|
||||
<li class="flex gap-2">• <span>Simplicity First</span></li>
|
||||
<li class="flex gap-2">• <span>Surgical changes (solo tocar lo necesario)</span></li>
|
||||
<li class="flex gap-2">• <span>Eliminar lo que no aporta valor al flujo visual</span></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="tokyo-card rounded-3xl p-6">
|
||||
<div class="text-sm font-medium text-[#bb9af7] mb-2">PRÓXIMOS PASOS (SUGERIDOS)</div>
|
||||
<div class="text-sm text-[#565f89]">
|
||||
<p class="mb-3">Este documento puede servir como registro vivo del proyecto.</p>
|
||||
<p>Añade aquí nuevas decisiones, experimentos visuales o cambios de rumbo cuando ocurran.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-12 text-center text-xs text-[#565f89]">
|
||||
Documento generado para seguimiento del proyecto Forja •
|
||||
<span class="font-mono">Tokyo Night Theme</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,58 +0,0 @@
|
||||
# Smoke manual del dashboard
|
||||
|
||||
> Esta lista cubre los flujos no automatizados (Streamlit). Ejecutar tras
|
||||
> cambios visuales o estructurales del dashboard.
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
docker compose up -d
|
||||
sleep 15
|
||||
```
|
||||
|
||||
Abrir [http://localhost:8501](http://localhost:8501).
|
||||
|
||||
## 1) Registro
|
||||
|
||||
- [ ] Aparece `incident_analyzer` en el desplegable.
|
||||
- [ ] Detalle muestra version, owner, propósito, guardrails, system_prompt.
|
||||
- [ ] Tabla de versiones lista `v1` y `v2`.
|
||||
- [ ] Diff `v1 → v2` muestra cambios coloreados.
|
||||
|
||||
## 2) Ejecutar
|
||||
|
||||
- [ ] Botones de escenarios cargan texto en el textarea.
|
||||
- [ ] Invocar `02_mos_degradation_pool_sbc` → status=`completed`, sin HITL,
|
||||
decision_path con 6 steps.
|
||||
- [ ] Invocar `01_sip_registration_drop` → status=`awaiting_approval`,
|
||||
banner amarillo redirige a Aprobaciones.
|
||||
|
||||
## 3) Aprobaciones
|
||||
|
||||
- [ ] La ejecución pendiente aparece en el desplegable.
|
||||
- [ ] Cada acción muestra risk_score con color, target y rollback_plan.
|
||||
- [ ] Aprobar acciones seleccionadas → status=`completed`.
|
||||
- [ ] Rechazo con razón → status=`failed`, error=`rejected_by_human`.
|
||||
|
||||
## 4) Historial
|
||||
|
||||
- [ ] Tab "Ejecuciones" lista todas las ejecuciones con summary.
|
||||
- [ ] Detalle muestra trace + violations.
|
||||
- [ ] Tab "Violaciones" filtrable por severity.
|
||||
|
||||
## 5) Politicas
|
||||
|
||||
- [ ] `default` aparece con sus validadores de input/output.
|
||||
- [ ] Cada validador expandible con su config.
|
||||
|
||||
## Bloqueo por PII (input)
|
||||
|
||||
- [ ] Pegar `El cliente con NIF 12345678Z reporta caída`.
|
||||
- [ ] Invocar → status=`blocked_by_guardrail`, violación `DetectPII`.
|
||||
|
||||
## Persistencia
|
||||
|
||||
- [ ] `docker compose down && docker compose up` → ejecuciones previas
|
||||
siguen accesibles vía Historial; aprobaciones pendientes siguen
|
||||
pendientes.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,631 +0,0 @@
|
||||
# AgentForge — Documento de diseño
|
||||
|
||||
**Fecha:** 2026-05-09
|
||||
**Estado:** aprobado — listo para plan de implementación
|
||||
**Autor:** Juan
|
||||
**Revisión:** v1.0
|
||||
|
||||
---
|
||||
|
||||
## 1. Resumen ejecutivo
|
||||
|
||||
**AgentForge** es una plataforma profesional de **gobernanza de agentes IA** orientada a entornos de producción. Cubre el ciclo de vida completo: catalogación, versionado de prompts y políticas, validación con guardrails en runtime, ejecución stateful con checkpointing, Human-in-the-Loop nativo, y trazabilidad end-to-end.
|
||||
|
||||
La plataforma se compone de dos servicios:
|
||||
|
||||
- **`agentforge-core`** — API REST (FastAPI) que materializa el dominio de gobierno: registry, versionado, ejecución de agentes vía LangGraph, validación de guardrails y persistencia.
|
||||
- **`agentforge-dashboard`** — UI ejecutiva (Streamlit) cliente del core; nunca habla con LLMs ni guardrails directamente.
|
||||
|
||||
Una imagen Docker por servicio, orquestadas con `docker-compose`. Arranque sin claves API (proveedor LLM mock por defecto).
|
||||
|
||||
## 2. Problema y motivación
|
||||
|
||||
Poner agentes IA en producción sin una capa de gobernanza produce sistemas opacos:
|
||||
|
||||
- Prompts que cambian en caliente sin historial.
|
||||
- Validaciones inconsistentes según quién despliega.
|
||||
- Acciones de alto impacto (rollbacks, cambios de configuración, ejecución de comandos) propuestas sin supervisión humana ni rollback plan.
|
||||
- Decisiones del agente no auditables — imposible reconstruir por qué se tomó una acción concreta.
|
||||
|
||||
AgentForge define el plano de control mínimo que un equipo de plataforma necesita antes de operar agentes con impacto real. El proyecto no resuelve la inteligencia del agente; resuelve su **operabilidad y gobierno**.
|
||||
|
||||
## 3. Decisiones de diseño (resumen)
|
||||
|
||||
| Eje | Decisión | Justificación |
|
||||
|---|---|---|
|
||||
| Topología | Two-tier: FastAPI core + Streamlit dashboard | Separación de responsabilidades; el core es API-first y consumible por terceros (RPA, n8n, otros agentes). Patrón de plataformas reales. |
|
||||
| LLM | Interfaz `LLMProvider` con `mock` (default), `azure`, `openai` | Demo arranca sin secrets. Demuestra portabilidad de proveedor. |
|
||||
| Guardrails | Interfaz `GuardrailEngine` con `GuardrailsAI` (default) y `NeMo` (opt-in vía flag) | Lo mejor de ambos mundos: arquitectura extensible visible + imagen ligera por defecto. |
|
||||
| Persistencia | Mezcla por capa: JSON (registry, definiciones), JSONL (logs append-only), YAML (versiones), SQLite (checkpoints LangGraph) | Cada capa con la herramienta correcta. Definiciones humanas en YAML; estado runtime en formatos legibles por máquina. |
|
||||
| Versionado | Simulación tipo Git en sistema de ficheros: `agents/<n>/versions/v1.yaml` + `index.yaml` con hashes y mensajes | No requiere git real; reproduce la disciplina de versionado y diff. |
|
||||
| Estado | LangGraph con `SqliteSaver` + `interrupt()` dinámico para HITL | Persistencia entre reinicios crítica para esperas humanas. |
|
||||
| Auth | Ninguna en MVP; placeholder hook `Depends(get_current_user)` | Pluggable a OAuth2/OIDC/MSAL en futuro. |
|
||||
| Comentarios | Español | Coherente con el autor. |
|
||||
|
||||
## 4. Arquitectura
|
||||
|
||||
```
|
||||
┌───────────────────────── docker-compose ──────────────────────────┐
|
||||
│ │
|
||||
│ ┌─────────────────────┐ HTTP/JSON ┌────────────────────┐ │
|
||||
│ │ agentforge-dashboard│ ────────────────► │ agentforge-core │ │
|
||||
│ │ Streamlit :8501 │ ◄──────────────── │ FastAPI :8000 │ │
|
||||
│ └─────────────────────┘ └─────────┬──────────┘ │
|
||||
│ │ │
|
||||
│ ┌────────────────────────────────────────┼──────────┐ │
|
||||
│ ▼ ▼ ▼ ▼ │
|
||||
│ LLM layer Guardrail layer Runtime State│
|
||||
│ ┌──────────────────┐ ┌────────────────────┐ ┌─────────┐ ┌───┐│
|
||||
│ │ LLMProvider (P) │ │ GuardrailEngine(P) │ │LangGraph│ │JSN││
|
||||
│ │ ├ MockProvider │ │ ├ GuardrailsAIEng │ │ + HITL │ │SQL││
|
||||
│ │ ├ AzureOpenAI │ │ ├ NeMoEngine (opt) │ │ + ckpt │ │JNL││
|
||||
│ │ └ OpenAI │ │ └ CompositeEngine │ └─────────┘ └───┘│
|
||||
│ └──────────────────┘ └────────────────────┘ │
|
||||
│ │
|
||||
│ Strategy pattern con Pydantic v2 + Protocol typing │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**Principios:**
|
||||
|
||||
- **API-first:** el core es invocable por cualquier cliente HTTP; el dashboard es un consumidor sustituible.
|
||||
- **Configuration over code:** definiciones de agentes y políticas en YAML editables sin redeploy.
|
||||
- **Strategy/Plugin:** todas las dependencias externas (LLM, guardrails, registry) detrás de `Protocol`.
|
||||
- **Fail-closed por defecto:** errores en validadores se traducen a violación que bloquea la ejecución.
|
||||
- **Trazabilidad obligatoria:** cada ejecución tiene un `trace_id` UUID propagado por logs, API, dashboard y persistencia.
|
||||
|
||||
## 5. Estructura de repositorio
|
||||
|
||||
```
|
||||
agentforge/
|
||||
├── README.md
|
||||
├── ARCHITECTURE.md
|
||||
├── docker-compose.yml
|
||||
├── .env.example
|
||||
├── .gitignore
|
||||
├── Makefile # up / down / test / lint / format / smoke
|
||||
├── pyproject.toml # ruff + mypy + pytest config
|
||||
├── core/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── src/agentforge_core/
|
||||
│ ├── main.py # FastAPI app + middlewares
|
||||
│ ├── config.py # Pydantic Settings, .env
|
||||
│ ├── api/
|
||||
│ │ ├── agents.py
|
||||
│ │ ├── executions.py
|
||||
│ │ ├── policies.py
|
||||
│ │ └── violations.py
|
||||
│ ├── domain/ # modelos Pydantic v2
|
||||
│ │ ├── agent.py
|
||||
│ │ ├── execution.py
|
||||
│ │ ├── guardrail.py
|
||||
│ │ └── policy.py
|
||||
│ ├── registry/
|
||||
│ │ ├── repository.py
|
||||
│ │ └── versioning.py
|
||||
│ ├── llm/
|
||||
│ │ ├── base.py # Protocol LLMProvider
|
||||
│ │ ├── mock.py
|
||||
│ │ ├── azure.py
|
||||
│ │ ├── openai.py
|
||||
│ │ └── factory.py
|
||||
│ ├── guardrails/
|
||||
│ │ ├── base.py # Protocol GuardrailEngine
|
||||
│ │ ├── guardrails_ai.py
|
||||
│ │ ├── nemo.py
|
||||
│ │ ├── composite.py
|
||||
│ │ └── factory.py
|
||||
│ ├── runtime/
|
||||
│ │ ├── graph.py # build_graph(agent_def, policy)
|
||||
│ │ ├── state.py # AgentState (TypedDict)
|
||||
│ │ ├── nodes.py # cada nodo del grafo
|
||||
│ │ └── checkpointer.py # SqliteSaver wrapper
|
||||
│ └── observability/
|
||||
│ └── logging.py # structlog + trace_id propagation
|
||||
├── dashboard/
|
||||
│ ├── Dockerfile
|
||||
│ ├── requirements.txt
|
||||
│ └── src/agentforge_dashboard/
|
||||
│ ├── app.py
|
||||
│ ├── client.py # httpx client tipado al core
|
||||
│ ├── pages/
|
||||
│ │ ├── 1_🏛️_Registro.py
|
||||
│ │ ├── 2_▶️_Ejecutar.py
|
||||
│ │ ├── 3_🤝_Aprobaciones.py
|
||||
│ │ ├── 4_📜_Historial.py
|
||||
│ │ └── 5_📐_Politicas.py
|
||||
│ └── components/
|
||||
│ ├── trace_view.py
|
||||
│ ├── violation_view.py
|
||||
│ └── diff_view.py
|
||||
├── agents/
|
||||
│ └── incident_analyzer/
|
||||
│ ├── index.yaml # versiones disponibles + metadata
|
||||
│ ├── versions/
|
||||
│ │ ├── v1.yaml
|
||||
│ │ └── v2.yaml
|
||||
│ └── examples/
|
||||
│ ├── 01_sip_registration_drop.txt
|
||||
│ ├── 02_mos_degradation_pool_sbc.txt
|
||||
│ └── 03_hss_capacity_active_active.txt
|
||||
├── policies/
|
||||
│ └── default/
|
||||
│ ├── index.yaml
|
||||
│ └── versions/
|
||||
│ └── v1.yaml
|
||||
├── data/ # gitignored
|
||||
│ ├── registry.json
|
||||
│ ├── checkpoints.sqlite
|
||||
│ ├── violations.jsonl
|
||||
│ └── executions.jsonl
|
||||
├── docs/
|
||||
│ ├── architecture.md
|
||||
│ ├── futuro.md
|
||||
│ ├── manual_qa.md
|
||||
│ └── superpowers/specs/
|
||||
│ └── 2026-05-09-agentforge-design.md
|
||||
└── tests/
|
||||
├── unit/
|
||||
└── integration/
|
||||
```
|
||||
|
||||
**Notas:**
|
||||
|
||||
- Pydantic models residen en `domain/`, no en `api/`. Los routers solo serializan.
|
||||
- Las factories (`llm/factory.py`, `guardrails/factory.py`) son la frontera donde el `.env` decide la implementación.
|
||||
- YAML para definiciones humanas (legibles, comentables, mejor diff). JSON/JSONL/SQLite para estado runtime.
|
||||
- `__init__.py` mínimos, sin re-exports masivos.
|
||||
|
||||
## 6. Modelos de dominio
|
||||
|
||||
```python
|
||||
class LLMConfig(BaseModel):
|
||||
provider: Literal["mock", "azure", "openai"] = "mock"
|
||||
model: str = "gpt-4o"
|
||||
temperature: float = 0.2
|
||||
max_tokens: int = 2000
|
||||
|
||||
class AgentVersionMeta(BaseModel):
|
||||
id: str # ej. "v2"
|
||||
hash: str # SHA-256 del YAML normalizado
|
||||
author: str
|
||||
message: str
|
||||
created_at: datetime
|
||||
|
||||
class AgentDefinition(BaseModel):
|
||||
name: str
|
||||
version: str # semver o vN
|
||||
owner: str
|
||||
purpose: str
|
||||
state: Literal["draft", "active", "deprecated"]
|
||||
guardrails: list[str] # nombres de policies
|
||||
llm: LLMConfig
|
||||
system_prompt: str
|
||||
output_schema: dict # JSON Schema del output esperado
|
||||
risk_threshold_for_hitl: int = 4 # risk_score ≥ X exige aprobación humana
|
||||
updated_at: datetime
|
||||
|
||||
class GuardrailViolation(BaseModel):
|
||||
trace_id: UUID
|
||||
timestamp: datetime
|
||||
stage: Literal["input", "output"]
|
||||
validator: str # ej. "DetectPII"
|
||||
severity: Literal["info", "warning", "block"]
|
||||
message: str
|
||||
blocked: bool # True si detuvo la ejecución
|
||||
|
||||
class ProposedAction(BaseModel):
|
||||
id: str # generado, único dentro de la ejecución
|
||||
action: str # ej. "rollback_image"
|
||||
target: str # ej. "cscf-cluster-aravaca-01"
|
||||
risk_score: int = Field(ge=1, le=5)
|
||||
rollback_plan: str
|
||||
requires_approval: bool
|
||||
|
||||
class DecisionStep(BaseModel):
|
||||
step: str # validate_input | llm_reason | ...
|
||||
timestamp: datetime
|
||||
duration_ms: int
|
||||
detail: dict # libre por nodo
|
||||
|
||||
class AgentExecution(BaseModel):
|
||||
trace_id: UUID
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
status: Literal[
|
||||
"running",
|
||||
"awaiting_approval",
|
||||
"blocked_by_guardrail",
|
||||
"completed",
|
||||
"failed",
|
||||
]
|
||||
started_at: datetime
|
||||
finished_at: datetime | None
|
||||
decision_path: list[DecisionStep]
|
||||
violations: list[GuardrailViolation]
|
||||
proposed_actions: list[ProposedAction]
|
||||
needs_human_for: list[ProposedAction] | None
|
||||
final_output: dict | None
|
||||
error: str | None
|
||||
|
||||
class AgentExecutionSummary(BaseModel):
|
||||
"""Versión ligera para listados (sin decision_path ni violations completas)."""
|
||||
trace_id: UUID
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
status: str
|
||||
started_at: datetime
|
||||
finished_at: datetime | None
|
||||
n_violations: int
|
||||
n_proposed_actions: int
|
||||
|
||||
class PolicyValidator(BaseModel):
|
||||
type: str # "detect_pii", "prompt_injection", ...
|
||||
config: dict # parámetros del validador
|
||||
|
||||
class PolicyDefinition(BaseModel):
|
||||
name: str
|
||||
version: str
|
||||
description: str
|
||||
input_validators: list[PolicyValidator]
|
||||
output_validators: list[PolicyValidator]
|
||||
on_validator_error: Literal["fail_open", "fail_closed"] = "fail_closed"
|
||||
```
|
||||
|
||||
## 7. API REST
|
||||
|
||||
```
|
||||
GET /health
|
||||
GET /agents → list[AgentDefinition]
|
||||
GET /agents/{name} → AgentDefinition (versión activa)
|
||||
GET /agents/{name}/versions → list[AgentVersionMeta]
|
||||
GET /agents/{name}/versions/{v} → AgentDefinition (versión concreta)
|
||||
GET /agents/{name}/versions/{v}/diff/{w} → DiffResult (unified diff)
|
||||
POST /agents/{name}/invoke → AgentExecution
|
||||
body: {"input": str, "version": str?}
|
||||
GET /executions → list[AgentExecutionSummary]
|
||||
GET /executions/{trace_id} → AgentExecution
|
||||
POST /executions/{trace_id}/approve → AgentExecution
|
||||
body: {"approved_action_ids": list[str],
|
||||
"comment": str?}
|
||||
POST /executions/{trace_id}/reject → AgentExecution
|
||||
body: {"reason": str}
|
||||
GET /violations → list[GuardrailViolation]
|
||||
query: trace_id?, severity?, since?
|
||||
GET /policies → list[PolicyDefinition]
|
||||
GET /policies/{name}/versions → list[PolicyVersionMeta]
|
||||
```
|
||||
|
||||
Códigos de error relevantes:
|
||||
|
||||
- `404` agente o ejecución no existe
|
||||
- `409` `/approve` o `/reject` sobre ejecución no en `awaiting_approval`
|
||||
- `422` body inválido (Pydantic)
|
||||
- `500` fallo interno (checkpoint corrupto, configuración inválida)
|
||||
|
||||
Todas las respuestas incluyen header `X-Trace-Id` (eco del request o generado).
|
||||
|
||||
## 8. Interfaces clave (Strategy)
|
||||
|
||||
```python
|
||||
class Message(BaseModel):
|
||||
role: Literal["system", "user", "assistant"]
|
||||
content: str
|
||||
|
||||
class CompletionResult(BaseModel):
|
||||
content: str
|
||||
model: str
|
||||
tokens_in: int
|
||||
tokens_out: int
|
||||
latency_ms: int
|
||||
|
||||
class LLMProvider(Protocol):
|
||||
name: str
|
||||
async def complete(
|
||||
self,
|
||||
messages: list[Message],
|
||||
schema: dict | None = None,
|
||||
temperature: float = 0.2,
|
||||
max_tokens: int = 2000,
|
||||
) -> CompletionResult: ...
|
||||
|
||||
class GuardrailEngine(Protocol):
|
||||
name: str
|
||||
async def validate_input(
|
||||
self, payload: str, policy: PolicyDefinition, trace_id: UUID
|
||||
) -> list[GuardrailViolation]: ...
|
||||
async def validate_output(
|
||||
self, payload: dict, policy: PolicyDefinition, trace_id: UUID
|
||||
) -> list[GuardrailViolation]: ...
|
||||
|
||||
class AgentRegistry(Protocol):
|
||||
def list_agents(self) -> list[AgentDefinition]: ...
|
||||
def get_agent(self, name: str, version: str | None = None) -> AgentDefinition: ...
|
||||
def list_versions(self, name: str) -> list[AgentVersionMeta]: ...
|
||||
def get_version(self, name: str, version: str) -> AgentDefinition: ...
|
||||
def diff_versions(self, name: str, v1: str, v2: str) -> DiffResult: ...
|
||||
def upsert_version(
|
||||
self, name: str, body: AgentDefinition, message: str, author: str
|
||||
) -> AgentVersionMeta: ...
|
||||
|
||||
class PolicyStore(Protocol):
|
||||
def list_policies(self) -> list[PolicyDefinition]: ...
|
||||
def get_policy(self, name: str, version: str | None = None) -> PolicyDefinition: ...
|
||||
def list_versions(self, name: str) -> list[PolicyVersionMeta]: ...
|
||||
```
|
||||
|
||||
## 9. Flujo de ejecución (LangGraph)
|
||||
|
||||
**Topología del grafo (común para todos los agentes; varía la `AgentDefinition` y `Policy`):**
|
||||
|
||||
```
|
||||
┌──────────────┐
|
||||
│ validate_in │── violations.severity=block ──► [STATUS: blocked_by_guardrail] ─► END
|
||||
└──────┬───────┘
|
||||
│ pass
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ llm_reason │── 3xretry → fallback → giveup ─► [STATUS: failed] ─► END
|
||||
└──────┬───────┘
|
||||
│ ok
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ validate_out │── violations.severity=block ──► [STATUS: blocked_by_guardrail] ─► END
|
||||
└──────┬───────┘
|
||||
│ pass
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ propose_actions │
|
||||
└──────┬───────────┘
|
||||
▼
|
||||
┌──────────────────┐ ─ any(risk≥threshold or requires_approval) ─┐
|
||||
│ approve_gate │ │
|
||||
│ (dyn. interrupt)│ ─ todas seguras ──┐ │
|
||||
└──────────────────┘ │ │
|
||||
▼ ▼
|
||||
┌──────────────┐ [interrupt(payload)]
|
||||
│ finalize │ status=awaiting_approval
|
||||
└──────┬───────┘ │
|
||||
▼ │ POST /approve
|
||||
[STATUS: completed] │ POST /reject
|
||||
▼
|
||||
[Command(resume={...}) → finalize → completed
|
||||
o status=failed, error="rejected_by_human"]
|
||||
```
|
||||
|
||||
**Estado del grafo (TypedDict de LangGraph):**
|
||||
|
||||
```python
|
||||
class AgentState(TypedDict):
|
||||
trace_id: str
|
||||
agent_name: str
|
||||
agent_version: str
|
||||
user_input: str
|
||||
messages: list[dict]
|
||||
raw_llm_output: str | None
|
||||
parsed_output: dict | None
|
||||
proposed_actions: list[dict]
|
||||
violations: list[dict]
|
||||
decision_path: Annotated[list[dict], operator.add] # acumulativo
|
||||
status: str
|
||||
error: str | None
|
||||
human_decision: dict | None # rellena en /approve|/reject
|
||||
```
|
||||
|
||||
`Annotated[..., operator.add]` permite que cada nodo añada pasos sin pisar lo previo (semántica nativa de LangGraph para reducers).
|
||||
|
||||
**Checkpointing:** `SqliteSaver("data/checkpoints.sqlite")` con `thread_id = trace_id`. El estado se persiste tras cada nodo. Si el dashboard cierra durante un `awaiting_approval`, `GET /executions/{trace_id}` reconstruye el snapshot exacto.
|
||||
|
||||
**Recorrido típico — `POST /agents/incident_analyzer/invoke`:**
|
||||
|
||||
1. API genera `trace_id` (UUID4); resuelve `AgentDefinition` vía `AgentRegistry`; resuelve `PolicyDefinition` referenciada en `agent_def.guardrails`; loga `execution.started` con `trace_id`.
|
||||
2. `runtime.graph.build_graph(agent_def, policy)` compila el grafo:
|
||||
- Inyecta `LLMProvider` vía factory (override por `LLM_PROVIDER` env).
|
||||
- Inyecta `GuardrailEngine` vía factory (`Composite[GuardrailsAI]`, +`NeMo` si flag).
|
||||
- Checkpointer = `SqliteSaver`. El nodo `approve_gate` invoca dinámicamente `interrupt(payload)` (API de LangGraph ≥0.2) solo cuando hay acciones que requieren aprobación; si todas son seguras, transiciona a `finalize` sin pausa. Esto evita el `interrupt_before` estático y mantiene el flujo declarativo.
|
||||
3. `graph.invoke({...}, config={"configurable":{"thread_id": trace_id}})` ejecuta hasta:
|
||||
- (a) END por guardrail block en `validate_input` o `validate_output`.
|
||||
- (b) END por LLM unrecoverable failure.
|
||||
- (c) INTERRUPT en `approve_gate` (HITL).
|
||||
- (d) END normal (`finalize`) si no hay acciones de alto riesgo.
|
||||
4. API serializa `AgentExecution` y devuelve 200. Append a `data/executions.jsonl` y violaciones a `data/violations.jsonl`.
|
||||
5. (caso HITL) Dashboard pinta cards con cada `ProposedAction` (action, target, badge `risk_score`, `rollback_plan`, botones `[Approve]`/`[Reject]`). Operador pulsa Approve.
|
||||
6. Dashboard `POST /executions/{trace_id}/approve` con body `{"approved_action_ids": ["..."], "comment": "..."}`.
|
||||
7. API: `graph.invoke(Command(resume={"human_decision": {...}}), config={"configurable":{"thread_id": trace_id}})`. Nodo `finalize` lee `state.human_decision`, compone `final_output`. `status="completed"`, `finished_at=now`. Append final a `executions.jsonl`.
|
||||
|
||||
## 10. Guardrails (capa runtime)
|
||||
|
||||
**Estructura `policies/default/versions/v1.yaml`:**
|
||||
|
||||
```yaml
|
||||
name: default
|
||||
version: v1
|
||||
description: Política base para agentes de operación de plataforma de voz
|
||||
input_validators:
|
||||
- type: detect_pii
|
||||
entities: [PERSON, EMAIL, PHONE_NUMBER, ES_NIF, IP_ADDRESS, IBAN_CODE]
|
||||
severity_on_match: block
|
||||
- type: prompt_injection
|
||||
severity_on_match: block
|
||||
- type: toxic_language
|
||||
threshold: 0.7
|
||||
severity_on_match: warning
|
||||
- type: forbidden_topics
|
||||
topics: ["instrucciones de explotación", "credenciales", "código malicioso"]
|
||||
severity_on_match: block
|
||||
|
||||
output_validators:
|
||||
- type: schema_match
|
||||
schema_ref: agent.output_schema # del agent_def en runtime
|
||||
severity_on_mismatch: block
|
||||
- type: pii_leakage
|
||||
severity_on_match: block
|
||||
- type: forbidden_action_keywords
|
||||
keywords: ["DROP TABLE", "rm -rf", "shutdown -h now", "delete production"]
|
||||
severity_on_match: block
|
||||
- type: telco_safety_rules # validador custom
|
||||
rules:
|
||||
- never_propose_action_targeting_production_without_rollback
|
||||
- never_propose_mass_action_without_canary
|
||||
|
||||
on_validator_error: fail_closed
|
||||
```
|
||||
|
||||
**Validadores (Guardrails AI engine):**
|
||||
|
||||
- `DetectPII` — usa `presidio_analyzer` + entidades configuradas; `ES_NIF` como recognizer custom (regex documentada).
|
||||
- `PromptInjection` — heurística de patrones (lista mantenible) + clasificador ligero. En modo `mock` LLM, lista de strings exactos para reproducibilidad de tests.
|
||||
- `ToxicLanguage` — validator del Guardrails Hub.
|
||||
- `ForbiddenTopics` — substring matcher en MVP. `docs/futuro.md` documenta upgrade a similarity con embeddings.
|
||||
- `SchemaMatch` — Pydantic parse contra `agent.output_schema`.
|
||||
- `PIILeakage` — Presidio sobre output stringificado.
|
||||
- `ForbiddenActionKeywords` — substring matcher sobre acciones propuestas.
|
||||
- `TelcoSafetyRules` — validador custom; reglas declarativas evaluadas sobre `proposed_actions[]`.
|
||||
|
||||
**Comportamiento del engine:**
|
||||
|
||||
- Toda violación se persiste a `violations.jsonl` (incluso `info`/`warning`).
|
||||
- Solo `severity=block` con `blocked=True` detiene el grafo.
|
||||
- `on_validator_error` decide qué pasa cuando un validador lanza excepción (`fail_closed` por defecto).
|
||||
- Composite engine ejecuta sub-engines en paralelo y agrega resultados.
|
||||
|
||||
**NeMo Guardrails (opt-in):** activado con `GUARDRAILS_NEMO_ENABLED=true`. Aporta topical rails declarados en Colang (`policies/default/nemo/rails.co`). MVP entrega configuración mínima (off-topic refusal); el flag por defecto está en `false` para mantener la imagen ligera.
|
||||
|
||||
## 11. Versionado de prompts y políticas (simulación tipo Git)
|
||||
|
||||
**Estructura `agents/<name>/index.yaml`:**
|
||||
|
||||
```yaml
|
||||
name: incident_analyzer
|
||||
versions:
|
||||
- id: v1
|
||||
hash: 7c9a4f...
|
||||
author: Juan
|
||||
message: "Versión inicial; cobertura básica de SIP/IMS"
|
||||
created_at: 2026-04-12T10:00:00Z
|
||||
- id: v2
|
||||
hash: a1b2c3...
|
||||
author: Juan
|
||||
message: "Añade detección de codec mismatch; sube risk_threshold a 4"
|
||||
created_at: 2026-05-01T12:00:00Z
|
||||
active_version: v2
|
||||
```
|
||||
|
||||
**Operaciones:**
|
||||
|
||||
- `Registry.upsert_version(name, body, message, author)` — calcula SHA-256 del YAML normalizado, valida formato, escribe `versions/vN.yaml`, actualiza `index.yaml`.
|
||||
- `Registry.diff_versions(name, v1, v2)` — `difflib.unified_diff` sobre los YAMLs.
|
||||
- `Registry.get_agent(name)` — devuelve la versión `active_version`.
|
||||
- Promoción de `draft` → `active` cambia `index.yaml.active_version`.
|
||||
|
||||
**Estructura idéntica para `policies/<name>/`.**
|
||||
|
||||
El dashboard ofrece:
|
||||
- Vista de "Versiones" del agente con autor, mensaje, hash, fecha.
|
||||
- Botón "Comparar con anterior" → render del diff con sintaxis coloreada.
|
||||
|
||||
## 12. Observabilidad
|
||||
|
||||
- **Logging**: `structlog` con renderer JSON. Cada log incluye `trace_id`, `agent_name`, `agent_version`, `step`, `duration_ms` cuando aplica.
|
||||
- **Trace-id propagation**: middleware FastAPI inyecta `X-Trace-Id` (genera UUID4 si no viene). `structlog` lo bind-ea en context (`structlog.contextvars`). Dashboard incluye `X-Trace-Id` en approve/reject para correlar logs.
|
||||
- **Eventos persistidos**:
|
||||
- `data/executions.jsonl` — un append por ejecución completada/fallida (no por step).
|
||||
- `data/violations.jsonl` — un append por violación (cualquier severidad).
|
||||
- **Métricas/Tracing OTel**: fuera de MVP. Documentado en `docs/futuro.md`.
|
||||
|
||||
## 13. Manejo de errores
|
||||
|
||||
| Tipo de fallo | Respuesta |
|
||||
|---|---|
|
||||
| Azure OpenAI 5xx / timeout | Retry exponencial (1s, 2s, 4s; 3 intentos). Si agota, fallback a `LLM_FALLBACK_PROVIDER` si está configurado. Si falla, `status=failed`, `error="llm_unavailable"`. La API devuelve `200` con la ejecución fallida (el dominio sí ha respondido). |
|
||||
| Guardrail validator excepción interna | Por defecto `fail_closed` → cuenta como `severity=block`, `blocked=True`. Configurable por policy. |
|
||||
| Pydantic validation falla en LLM output | 1 retry con prompt extendido `"Respond strictly with JSON matching this schema: ..."`. Si falla, `status=failed`, `error="output_schema_mismatch"`. |
|
||||
| Checkpoint SQLite corrupto | Log `error.checkpoint_corrupt`, devolver `500`, `error_class="checkpoint_unreadable"`. Sin auto-recovery (mejor falla explícita). |
|
||||
| Agente solicitado no existe | `404` con `{"detail":"agent 'X' not found"}`. |
|
||||
| `/approve` o `/reject` sobre estado no válido | `409` con `{"detail":"execution status 'X' does not allow approve"}`. |
|
||||
| Dashboard pierde conexión con core | `httpx` retry simple (2 intentos, 1s). Si sigue fallando, banner `st.error("Core API unreachable")`. |
|
||||
| Variable `.env` requerida ausente | `Pydantic Settings` falla en arranque con mensaje claro (no en runtime: el contenedor no debe estar "vivo y roto"). |
|
||||
|
||||
## 14. Testing
|
||||
|
||||
```
|
||||
tests/
|
||||
├── unit/
|
||||
│ ├── test_llm_mock.py
|
||||
│ ├── test_guardrails_ai.py
|
||||
│ ├── test_registry.py
|
||||
│ ├── test_runtime_state.py
|
||||
│ └── test_policy_loader.py
|
||||
├── integration/
|
||||
│ ├── test_invoke_happy_path.py
|
||||
│ ├── test_invoke_hitl.py
|
||||
│ ├── test_invoke_pii_block.py
|
||||
│ └── test_invoke_resume_after_restart.py
|
||||
└── fixtures/
|
||||
├── agents/incident_analyzer_test.yaml
|
||||
├── policies/test_policy.yaml
|
||||
└── inputs/
|
||||
```
|
||||
|
||||
- **Stack**: `pytest` + `pytest-asyncio` + `httpx.AsyncClient` (FastAPI TestClient) + `freezegun`.
|
||||
- **MockProvider** con lookup determinista (`hash(input) → respuesta`).
|
||||
- **Sin tests Streamlit** en MVP. Smoke manual en `docs/manual_qa.md`.
|
||||
- **CI** (`.github/workflows/ci.yml`): `ruff check` + `ruff format --check` + `mypy` + `pytest`.
|
||||
- **Cobertura objetivo** ~70% sobre `core/src/agentforge_core`.
|
||||
|
||||
## 15. Despliegue
|
||||
|
||||
- `docker-compose.yml` con dos servicios: `core` (uvicorn, `:8000`) y `dashboard` (Streamlit, `:8501`).
|
||||
- `Dockerfile.core` y `Dockerfile.dashboard` separados; cada uno con sus deps mínimas.
|
||||
- Healthchecks: `core` `GET /health`; `dashboard` `GET /` (Streamlit responde 200 cuando el server arranca).
|
||||
- Volume mount `./data:/app/data` y `./agents:/app/agents:ro`, `./policies:/app/policies:ro`.
|
||||
- `.env.example` con todas las variables documentadas y comentadas.
|
||||
- `LLM_PROVIDER=mock` por defecto → arranque sin claves.
|
||||
- Variables relevantes: `LLM_PROVIDER`, `LLM_FALLBACK_PROVIDER`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_DEPLOYMENT`, `OPENAI_API_KEY`, `GUARDRAILS_NEMO_ENABLED`, `LOG_LEVEL`, `DATA_DIR`.
|
||||
|
||||
## 16. README — estructura
|
||||
|
||||
1. **Qué es AgentForge** — Plataforma de gobernanza de agentes IA: catalogación, versionado de prompts y políticas, guardrails runtime, ejecución stateful con HITL, observabilidad.
|
||||
2. **Problema que resuelve** — Sin gobernanza, los agentes en producción son cajas negras (ver §2).
|
||||
3. **Diagrama de arquitectura** — Mermaid en línea + PNG renderizado en `docs/`.
|
||||
4. **Quickstart**: `cp .env.example .env && docker compose up`. Funciona out-of-the-box (`LLM_PROVIDER=mock`).
|
||||
5. **Demo guiada en 3 pasos**: 1) registro; 2) ejecuta `incident_analyzer` con `02_mos_degradation_pool_sbc.txt`; 3) aprueba el rollback en HITL.
|
||||
6. **Capacidades implementadas** — tabla `feature → ubicación en código`.
|
||||
7. **Roadmap** — link a `docs/futuro.md`.
|
||||
|
||||
## 17. Roadmap (`docs/futuro.md`)
|
||||
|
||||
- NeMo Guardrails con configuración rica (Colang KB).
|
||||
- Autenticación: OAuth2/OIDC con MSAL para entornos Azure AD.
|
||||
- OpenTelemetry: spans por nodo, métricas de violaciones por validador.
|
||||
- Multi-tenant: aislamiento por `tenant_id`, registry por tenant.
|
||||
- Evaluadores LLM-as-judge automatizados: regression suite que ejecuta cada agente sobre escenarios canónicos y mide deriva.
|
||||
- UI de aprobación con SLA, reasignación entre operadores, cola Kanban.
|
||||
- Persistencia migrable a Postgres + pgvector.
|
||||
|
||||
## 18. No-objetivos (out of scope MVP)
|
||||
|
||||
- Aprendizaje/fine-tuning de modelos.
|
||||
- Integración con sistemas reales de orquestación (RPA, Ansible, Kubernetes).
|
||||
- Inteligencia avanzada del agente (RAG, multi-step planning con tool use complejo).
|
||||
- Tests automatizados de UI Streamlit.
|
||||
- Soporte multi-idioma del dashboard.
|
||||
- High-availability del core (1 réplica suficiente para MVP).
|
||||
|
||||
## 19. Decisiones pospuestas
|
||||
|
||||
- **Postgres vs SQLite para checkpoints en multi-instance**: SQLite suficiente para MVP single-node; cambio a Postgres documentado en `docs/futuro.md`.
|
||||
- **NeMo full integration**: feature flag preparado, pero la configuración Colang completa es post-MVP.
|
||||
- **Métricas**: estructura de logging permite extracción posterior; no se entregan dashboards Grafana.
|
||||
|
||||
## 20. Referencias
|
||||
|
||||
- [LangGraph documentation](https://langchain-ai.github.io/langgraph/) — patrón checkpointer + interrupt.
|
||||
- [Guardrails AI](https://www.guardrailsai.com/) — validators hub.
|
||||
- [NVIDIA NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails) — Colang DSL.
|
||||
- [Microsoft Presidio](https://microsoft.github.io/presidio/) — PII detection.
|
||||
- [Pydantic v2](https://docs.pydantic.dev/latest/) — modelos y settings.
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) — API REST.
|
||||
- [Streamlit](https://streamlit.io/) — dashboard.
|
||||
- [structlog](https://www.structlog.org/) — logging estructurado.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,7 @@ EXAMPLES = (
|
||||
@pytest.mark.integration
|
||||
def test_happy_path_completa_sin_hitl(integration_client) -> None: # type: ignore[no-untyped-def]
|
||||
payload = (EXAMPLES / "02_mos_degradation_pool_sbc.txt").read_text(encoding="utf-8")
|
||||
r = integration_client.post("/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
r = integration_client.post("/api/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "completed"
|
||||
|
||||
@@ -12,7 +12,7 @@ EXAMPLES = (
|
||||
@pytest.mark.integration
|
||||
def test_hitl_approve_completa(integration_client) -> None: # type: ignore[no-untyped-def]
|
||||
payload = (EXAMPLES / "01_sip_registration_drop.txt").read_text(encoding="utf-8")
|
||||
r = integration_client.post("/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
r = integration_client.post("/api/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
body = r.json()
|
||||
assert body["status"] == "awaiting_approval"
|
||||
assert body["needs_human_for"]
|
||||
@@ -20,7 +20,7 @@ def test_hitl_approve_completa(integration_client) -> None: # type: ignore[no-u
|
||||
pending = [a["id"] for a in body["needs_human_for"]]
|
||||
|
||||
r2 = integration_client.post(
|
||||
f"/executions/{trace_id}/approve",
|
||||
f"/api/executions/{trace_id}/approve",
|
||||
json={"approved_action_ids": pending, "comment": "OK rollback"},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
@@ -31,10 +31,10 @@ def test_hitl_approve_completa(integration_client) -> None: # type: ignore[no-u
|
||||
@pytest.mark.integration
|
||||
def test_hitl_reject_marca_failed(integration_client) -> None: # type: ignore[no-untyped-def]
|
||||
payload = (EXAMPLES / "01_sip_registration_drop.txt").read_text(encoding="utf-8")
|
||||
r = integration_client.post("/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
r = integration_client.post("/api/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
trace_id = r.json()["trace_id"]
|
||||
r2 = integration_client.post(
|
||||
f"/executions/{trace_id}/reject", json={"reason": "rollback no procede"}
|
||||
f"/api/executions/{trace_id}/reject", json={"reason": "rollback no procede"}
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
assert r2.json()["status"] == "failed"
|
||||
|
||||
@@ -6,7 +6,7 @@ import pytest
|
||||
@pytest.mark.integration
|
||||
def test_pii_es_nif_bloquea(integration_client) -> None: # type: ignore[no-untyped-def]
|
||||
pii_input = "El cliente con NIF 12345678Z reporta caída de servicio. Email juan@example.com"
|
||||
r = integration_client.post("/agents/incident_analyzer/invoke", json={"input": pii_input})
|
||||
r = integration_client.post("/api/agents/incident_analyzer/invoke", json={"input": pii_input})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "blocked_by_guardrail"
|
||||
|
||||
@@ -44,7 +44,7 @@ def test_resume_tras_recreacion_del_app(tmp_path: Path, monkeypatch: pytest.Monk
|
||||
|
||||
# 1) Primera "ejecución" del core: invoca y queda en awaiting_approval.
|
||||
client_a = _fresh()
|
||||
r = client_a.post("/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
r = client_a.post("/api/agents/incident_analyzer/invoke", json={"input": payload})
|
||||
body = r.json()
|
||||
assert body["status"] == "awaiting_approval"
|
||||
trace_id = body["trace_id"]
|
||||
@@ -54,7 +54,7 @@ def test_resume_tras_recreacion_del_app(tmp_path: Path, monkeypatch: pytest.Monk
|
||||
# 2) Recreamos el app (simula restart del contenedor) — DATA_DIR persiste.
|
||||
client_b = _fresh()
|
||||
r2 = client_b.post(
|
||||
f"/executions/{trace_id}/approve",
|
||||
f"/api/executions/{trace_id}/approve",
|
||||
json={"approved_action_ids": pending},
|
||||
)
|
||||
assert r2.status_code == 200
|
||||
|
||||
@@ -28,41 +28,41 @@ def patch_settings(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
|
||||
def test_list_agents_incluye_incident_analyzer() -> None:
|
||||
r = TestClient(create_app()).get("/agents")
|
||||
r = TestClient(create_app()).get("/api/agents")
|
||||
assert r.status_code == 200
|
||||
assert any(a["name"] == "incident_analyzer" for a in r.json())
|
||||
|
||||
|
||||
def test_get_agent_activo_devuelve_v1() -> None:
|
||||
r = TestClient(create_app()).get("/agents/incident_analyzer")
|
||||
r = TestClient(create_app()).get("/api/agents/incident_analyzer")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["version"] == "v1"
|
||||
|
||||
|
||||
def test_get_agent_inexistente_404() -> None:
|
||||
r = TestClient(create_app()).get("/agents/inexistente")
|
||||
r = TestClient(create_app()).get("/api/agents/inexistente")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_list_versions_devuelve_meta() -> None:
|
||||
r = TestClient(create_app()).get("/agents/incident_analyzer/versions")
|
||||
r = TestClient(create_app()).get("/api/agents/incident_analyzer/versions")
|
||||
assert r.status_code == 200
|
||||
assert r.json()[0]["id"] == "v1"
|
||||
|
||||
|
||||
def test_get_version_concreta() -> None:
|
||||
r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v1")
|
||||
r = TestClient(create_app()).get("/api/agents/incident_analyzer/versions/v1")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["name"] == "incident_analyzer"
|
||||
|
||||
|
||||
def test_get_version_inexistente_404() -> None:
|
||||
r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v999")
|
||||
r = TestClient(create_app()).get("/api/agents/incident_analyzer/versions/v999")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_diff_misma_version_da_diff_vacio() -> None:
|
||||
r = TestClient(create_app()).get("/agents/incident_analyzer/versions/v1/diff/v1")
|
||||
r = TestClient(create_app()).get("/api/agents/incident_analyzer/versions/v1/diff/v1")
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["from_version"] == "v1"
|
||||
|
||||
@@ -34,7 +34,7 @@ def client() -> TestClient:
|
||||
|
||||
|
||||
def test_invoke_devuelve_execution_completada(client: TestClient) -> None:
|
||||
r = client.post("/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"})
|
||||
r = client.post("/api/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "completed"
|
||||
@@ -44,12 +44,12 @@ def test_invoke_devuelve_execution_completada(client: TestClient) -> None:
|
||||
|
||||
|
||||
def test_invoke_agente_inexistente_404(client: TestClient) -> None:
|
||||
r = client.post("/agents/inexistente/invoke", json={"input": "x"})
|
||||
r = client.post("/api/agents/inexistente/invoke", json={"input": "x"})
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_invoke_riesgo_alto_pausa_hitl(client: TestClient) -> None:
|
||||
r = client.post("/agents/incident_analyzer/invoke", json={"input": "caída registros sip"})
|
||||
r = client.post("/api/agents/incident_analyzer/invoke", json={"input": "caída registros sip"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "awaiting_approval"
|
||||
@@ -59,21 +59,21 @@ def test_invoke_riesgo_alto_pausa_hitl(client: TestClient) -> None:
|
||||
|
||||
def test_get_execution_existe_tras_invoke(client: TestClient) -> None:
|
||||
trace_id = client.post(
|
||||
"/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"}
|
||||
"/api/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"}
|
||||
).json()["trace_id"]
|
||||
r = client.get(f"/executions/{trace_id}")
|
||||
r = client.get(f"/api/executions/{trace_id}")
|
||||
assert r.status_code == 200
|
||||
assert r.json()["trace_id"] == trace_id
|
||||
|
||||
|
||||
def test_get_execution_inexistente_404(client: TestClient) -> None:
|
||||
r = client.get("/executions/00000000-0000-0000-0000-000000000000")
|
||||
r = client.get("/api/executions/00000000-0000-0000-0000-000000000000")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_list_executions_incluye_la_completada(client: TestClient) -> None:
|
||||
client.post("/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"})
|
||||
r = client.get("/executions")
|
||||
client.post("/api/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"})
|
||||
r = client.get("/api/executions")
|
||||
assert r.status_code == 200
|
||||
rows = r.json()
|
||||
assert len(rows) == 1
|
||||
@@ -85,9 +85,9 @@ def test_list_executions_incluye_la_awaiting(client: TestClient) -> None:
|
||||
"""Una ejecución pausada en HITL no se escribe en el JSONL; aún así debe listarse
|
||||
(la página de Aprobaciones del dashboard depende de ello)."""
|
||||
trace_id = client.post(
|
||||
"/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
"/api/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
).json()["trace_id"]
|
||||
rows = client.get("/executions").json()
|
||||
rows = client.get("/api/executions").json()
|
||||
awaiting = [r for r in rows if r["trace_id"] == trace_id]
|
||||
assert len(awaiting) == 1
|
||||
assert awaiting[0]["status"] == "awaiting_approval"
|
||||
@@ -95,12 +95,12 @@ def test_list_executions_incluye_la_awaiting(client: TestClient) -> None:
|
||||
|
||||
def test_approve_resume_completa_ejecucion(client: TestClient) -> None:
|
||||
invoked = client.post(
|
||||
"/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
"/api/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
).json()
|
||||
trace_id = invoked["trace_id"]
|
||||
action_ids = [a["id"] for a in invoked["needs_human_for"]]
|
||||
r = client.post(
|
||||
f"/executions/{trace_id}/approve",
|
||||
f"/api/executions/{trace_id}/approve",
|
||||
json={"approved_action_ids": action_ids, "comment": "OK adelante"},
|
||||
)
|
||||
assert r.status_code == 200
|
||||
@@ -111,9 +111,9 @@ def test_approve_resume_completa_ejecucion(client: TestClient) -> None:
|
||||
|
||||
def test_reject_marca_failed(client: TestClient) -> None:
|
||||
trace_id = client.post(
|
||||
"/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
"/api/agents/incident_analyzer/invoke", json={"input": "caída registros sip"}
|
||||
).json()["trace_id"]
|
||||
r = client.post(f"/executions/{trace_id}/reject", json={"reason": "no procede ahora"})
|
||||
r = client.post(f"/api/executions/{trace_id}/reject", json={"reason": "no procede ahora"})
|
||||
assert r.status_code == 200
|
||||
body = r.json()
|
||||
assert body["status"] == "failed"
|
||||
@@ -122,15 +122,15 @@ def test_reject_marca_failed(client: TestClient) -> None:
|
||||
|
||||
def test_approve_sobre_estado_no_pausado_da_409(client: TestClient) -> None:
|
||||
trace_id = client.post(
|
||||
"/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"}
|
||||
"/api/agents/incident_analyzer/invoke", json={"input": "degradación MOS pool SBC"}
|
||||
).json()["trace_id"]
|
||||
r = client.post(f"/executions/{trace_id}/approve", json={"approved_action_ids": []})
|
||||
r = client.post(f"/api/executions/{trace_id}/approve", json={"approved_action_ids": []})
|
||||
assert r.status_code == 409
|
||||
|
||||
|
||||
def test_approve_trace_id_desconocido_da_404(client: TestClient) -> None:
|
||||
r = client.post(
|
||||
"/executions/11111111-1111-1111-1111-111111111111/approve",
|
||||
"/api/executions/11111111-1111-1111-1111-111111111111/approve",
|
||||
json={"approved_action_ids": []},
|
||||
)
|
||||
assert r.status_code == 404
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Tests de los routers /policies y /violations."""
|
||||
"""Tests de los routers /api/policies y /api/violations."""
|
||||
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
@@ -37,24 +37,24 @@ def client() -> TestClient:
|
||||
|
||||
|
||||
def test_list_policies_incluye_default(client: TestClient) -> None:
|
||||
r = client.get("/policies")
|
||||
r = client.get("/api/policies")
|
||||
assert r.status_code == 200
|
||||
assert any(p["name"] == "default" for p in r.json())
|
||||
|
||||
|
||||
def test_list_policy_versions(client: TestClient) -> None:
|
||||
r = client.get("/policies/default/versions")
|
||||
r = client.get("/api/policies/default/versions")
|
||||
assert r.status_code == 200
|
||||
assert r.json()[0]["id"] == "v1"
|
||||
|
||||
|
||||
def test_list_policy_versions_inexistente_404(client: TestClient) -> None:
|
||||
r = client.get("/policies/inexistente/versions")
|
||||
r = client.get("/api/policies/inexistente/versions")
|
||||
assert r.status_code == 404
|
||||
|
||||
|
||||
def test_violations_vacio_si_no_hay(client: TestClient) -> None:
|
||||
r = client.get("/violations")
|
||||
r = client.get("/api/violations")
|
||||
assert r.status_code == 200
|
||||
assert r.json() == []
|
||||
|
||||
@@ -75,11 +75,11 @@ def test_violations_lista_y_filtra(client: TestClient, tmp_path: Path) -> None:
|
||||
tid = uuid4()
|
||||
append_violation(tmp_path, _violation(tid, "DetectPII", "block", blocked=True))
|
||||
append_violation(tmp_path, _violation(uuid4(), "Schema", "warning", blocked=False))
|
||||
assert len(client.get("/violations").json()) == 2
|
||||
assert len(client.get("/violations", params={"severity": "block"}).json()) == 1
|
||||
assert len(client.get("/violations", params={"trace_id": str(tid)}).json()) == 1
|
||||
assert len(client.get("/api/violations").json()) == 2
|
||||
assert len(client.get("/api/violations", params={"severity": "block"}).json()) == 1
|
||||
assert len(client.get("/api/violations", params={"trace_id": str(tid)}).json()) == 1
|
||||
|
||||
|
||||
def test_violations_severity_invalida_422(client: TestClient) -> None:
|
||||
r = client.get("/violations", params={"severity": "nope"})
|
||||
r = client.get("/api/violations", params={"severity": "nope"})
|
||||
assert r.status_code == 422
|
||||
|
||||
Reference in New Issue
Block a user