cambios profundos
This commit is contained in:
@@ -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>
|
||||
|
||||
<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 & 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()},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user