cambios profundos

This commit is contained in:
2026-05-27 16:29:02 +02:00
parent d95ed15766
commit 34b11dfc66
29 changed files with 594 additions and 11102 deletions
+8 -1
View File
@@ -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,
},
+8 -8
View File
@@ -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 %}
+66 -22
View File
@@ -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 %}
+78 -42
View File
@@ -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>
<p class="text-2xl text-zinc-400 mb-8">
Gobernanza profesional para agentes de IA
</p>
<div class="max-w-[1200px] mx-auto pb-12">
<!-- 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>
<!-- Branding -->
<div class="pt-6 pb-6">
<div>
<span class="text-6xl font-semibold tracking-tighter">Forja</span>
</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>
</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 &amp; 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 &amp; 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 &amp; 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 %}
+5 -5
View File
@@ -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>
+69 -45
View File
@@ -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()},
)