añadido guidelines de karpathy y repensado del proyecto
This commit is contained in:
@@ -30,6 +30,8 @@ USER agent
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
# Nota: Este servicio ahora sirve tanto la API REST como la UI HTMX (Opción A)
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=15s --retries=3 \
|
||||
CMD curl -fsS http://localhost:8000/health || exit 1
|
||||
|
||||
|
||||
@@ -17,3 +17,7 @@ openai>=1.50,<2.0
|
||||
PyYAML>=6.0,<7.0
|
||||
jsonschema>=4.0,<5.0
|
||||
nemoguardrails>=0.10,<0.12
|
||||
|
||||
# HTMX UI embebida (Opción A)
|
||||
jinja2>=3.1,<4.0
|
||||
python-multipart>=0.0.9,<0.1
|
||||
|
||||
@@ -45,5 +45,6 @@ class AgentDefinition(BaseModel):
|
||||
input_placeholder: str | None = None
|
||||
category: str | None = None
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
icon: str | None = None
|
||||
icon: str | None = "🤖"
|
||||
color: str | None = None
|
||||
template: str = "governed_llm"
|
||||
|
||||
@@ -24,13 +24,19 @@ 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
|
||||
|
||||
# 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.
|
||||
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"])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Paquete de UI HTMX embebida (FastAPI + Jinja2 + HTMX)."""
|
||||
@@ -0,0 +1,103 @@
|
||||
<div class="space-y-6">
|
||||
<!-- Header -->
|
||||
<div>
|
||||
<div class="flex items-center gap-3 flex-wrap">
|
||||
<span class="text-3xl font-semibold tracking-tight">{{ agent.name }}</span>
|
||||
<span class="text-sm px-2.5 py-0.5 rounded-lg bg-zinc-800 text-zinc-400">v{{ agent.version }}</span>
|
||||
<span class="text-sm px-2.5 py-0.5 rounded-lg {% if agent.state == 'active' %}bg-emerald-900 text-emerald-300{% else %}bg-zinc-800 text-zinc-300{% endif %}">
|
||||
{{ agent.state }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-zinc-400 mt-2 text-[15px] leading-snug">
|
||||
{{ agent.purpose }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadata principal -->
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 text-sm">
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1">Owner</div>
|
||||
<div class="font-medium">{{ agent.owner }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1">Risk threshold (HITL)</div>
|
||||
<div class="font-medium">{{ agent.risk_threshold_for_hitl }}</div>
|
||||
</div>
|
||||
{% if agent.category %}
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1">Categoría</div>
|
||||
<div>{{ agent.category }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- LLM + Guardrails -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1.5">LLM</div>
|
||||
<div class="font-mono text-xs bg-zinc-950 border border-zinc-800 p-3 rounded-xl">
|
||||
{{ agent.llm.provider }} / {{ agent.llm.model }}<br>
|
||||
<span class="text-zinc-500">temp={{ agent.llm.temperature }} · max={{ agent.llm.max_tokens }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1.5">Guardrails</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{% for g in agent.guardrails %}
|
||||
<span class="text-xs px-3 py-1 bg-zinc-900 border border-zinc-800 rounded-full">{{ g }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Comportamiento (Prompt + Output Schema) -->
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1.5">Comportamiento</div>
|
||||
|
||||
<!-- 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 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>
|
||||
<div class="mt-2 p-4 bg-black border border-zinc-800 rounded-xl text-sm whitespace-pre-wrap font-light leading-relaxed">
|
||||
{{ agent.system_prompt }}
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<!-- Output Schema -->
|
||||
<div>
|
||||
<div class="text-xs text-zinc-400 mb-1.5">Output schema</div>
|
||||
<pre class="text-[10px] bg-zinc-950 border border-zinc-800 p-3 rounded-xl overflow-auto max-h-48 text-zinc-300">{{ agent.output_schema | tojson(indent=2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Metadatos adicionales -->
|
||||
{% if agent.input_label or agent.input_placeholder or agent.updated_at %}
|
||||
<div class="pt-2 border-t border-zinc-800">
|
||||
<div class="text-zinc-400 text-xs mb-2">Metadatos</div>
|
||||
<div class="text-sm space-y-1 text-zinc-300">
|
||||
{% if agent.input_label %}
|
||||
<div><span class="text-zinc-500">Input label:</span> {{ agent.input_label }}</div>
|
||||
{% endif %}
|
||||
{% 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>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if agent.tags %}
|
||||
<div>
|
||||
<div class="text-zinc-400 text-xs mb-1.5">Tags</div>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
{% for tag in agent.tags %}
|
||||
<span class="text-xs px-3 py-0.5 bg-zinc-900 border border-zinc-800 rounded-full">{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -0,0 +1,8 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-3xl">
|
||||
<h1 class="text-2xl font-semibold mb-4">{{ agent.name }} <span class="text-sm text-zinc-400">v{{ agent.version }}</span></h1>
|
||||
{% include "agent_detail.html" %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">Agentes registrados</h1>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<!-- Lista de agentes -->
|
||||
<div class="lg:col-span-1">
|
||||
<div class="border border-zinc-800 bg-zinc-900 rounded-xl overflow-hidden">
|
||||
{% for a in agents %}
|
||||
<a href="/agents/{{ a.name }}"
|
||||
hx-get="/agents/{{ a.name }}"
|
||||
hx-target="#agent-detail"
|
||||
hx-swap="innerHTML"
|
||||
class="block px-4 py-3 border-b border-zinc-800 last:border-b-0 hover:bg-zinc-800 cursor-pointer">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="text-2xl flex-shrink-0"
|
||||
{% if a.color %}style="color: {{ a.color }}"{% endif %}>
|
||||
{{ a.icon or "🤖" }}
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium truncate">{{ a.name }}</div>
|
||||
<div class="text-xs text-zinc-400">v{{ a.version }} · {{ a.state }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% else %}
|
||||
<div class="px-4 py-6 text-zinc-400">No hay agentes registrados.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Área de detalle -->
|
||||
<div class="lg:col-span-2">
|
||||
<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.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">🤝 Aprobaciones Pendientes (HITL)</h1>
|
||||
|
||||
<div class="space-y-4 max-w-4xl" id="approvals-list">
|
||||
{% for ex in pending %}
|
||||
<div class="border border-amber-800 bg-zinc-900 rounded-xl p-4">
|
||||
<div class="flex justify-between items-start">
|
||||
<div>
|
||||
<div class="font-medium">{{ ex.agent_name }} v{{ ex.agent_version }}</div>
|
||||
<div class="text-xs text-zinc-400">{{ ex.trace_id }}</div>
|
||||
</div>
|
||||
<div class="text-amber-400 text-xs">AWAITING APPROVAL</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 flex gap-2">
|
||||
<button
|
||||
hx-post="/approvals/{{ ex.trace_id }}/approve"
|
||||
hx-target="#approvals-list"
|
||||
hx-swap="outerHTML"
|
||||
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-target="#approvals-list"
|
||||
hx-swap="outerHTML"
|
||||
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>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,42 @@
|
||||
<!doctype html>
|
||||
<html lang="es">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Forja • {{ title or "Gobernanza de Agentes IA" }}</title>
|
||||
<script src="https://cdn.tailwindcss.com"></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; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-zinc-950 text-zinc-200">
|
||||
<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">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,19 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">📜 Historial de Ejecuciones</h1>
|
||||
|
||||
<div class="space-y-3 max-w-4xl">
|
||||
{% for ex in executions %}
|
||||
<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="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>
|
||||
{% else %}
|
||||
<div class="text-zinc-400">No hay ejecuciones todavía.</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% 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>
|
||||
|
||||
<!-- 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>
|
||||
</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>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block content %}
|
||||
<h1 class="text-3xl font-semibold mb-6">📐 Políticas</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-xs mt-2 text-zinc-500">Validadores: {{ p.validators | length }}</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,33 @@
|
||||
{% 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm mb-1">Input / Escenario</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 class="htmx-indicator">...</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="result" class="mt-6"></div>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,183 @@
|
||||
"""HTMX UI router — montado dentro del propio forja-core (Opción A)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
from fastapi.templating import Jinja2Templates
|
||||
|
||||
from forja_core.api.deps import get_orchestrator, get_policy_store, get_registry, get_settings
|
||||
from forja_core.api.persistence import read_execution_summaries
|
||||
from forja_core.domain.execution import AgentExecution
|
||||
|
||||
router = APIRouter(prefix="", tags=["ui"])
|
||||
|
||||
# Ruta robusta a los templates (funciona tanto en dev como dentro del contenedor)
|
||||
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",
|
||||
]
|
||||
|
||||
def get_slogan() -> str:
|
||||
import random
|
||||
return random.choice(SLOGANS)
|
||||
|
||||
|
||||
@router.get("/", response_class=HTMLResponse)
|
||||
async def home(request: Request) -> HTMLResponse:
|
||||
return templates.TemplateResponse(
|
||||
"home.html",
|
||||
{"request": request, "title": "Inicio", "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents", response_class=HTMLResponse)
|
||||
async def agents_page(request: Request) -> HTMLResponse:
|
||||
registry = get_registry()
|
||||
agents = registry.list_agents()
|
||||
return templates.TemplateResponse(
|
||||
"agents.html",
|
||||
{"request": request, "title": "Agentes", "agents": agents, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/agents/{name}", response_class=HTMLResponse)
|
||||
async def agent_detail(request: Request, name: str) -> HTMLResponse:
|
||||
registry = get_registry()
|
||||
try:
|
||||
agent = registry.get_agent(name)
|
||||
except FileNotFoundError:
|
||||
return HTMLResponse('<div class="text-red-400">Agente no encontrado</div>', status_code=404)
|
||||
|
||||
# Si viene por HTMX, devolvemos solo el fragmento (sin layout)
|
||||
if request.headers.get("HX-Request"):
|
||||
return templates.TemplateResponse(
|
||||
"agent_detail.html",
|
||||
{"request": request, "agent": agent, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
# Acceso directo por navegador → página completa
|
||||
return templates.TemplateResponse(
|
||||
"agent_detail_full.html",
|
||||
{"request": request, "title": agent.name, "agent": agent, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/run", response_class=HTMLResponse)
|
||||
async def run_page(request: Request) -> HTMLResponse:
|
||||
registry = get_registry()
|
||||
agents = registry.list_agents()
|
||||
return templates.TemplateResponse(
|
||||
"run.html",
|
||||
{"request": request, "title": "Ejecutar", "agents": agents, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/run", response_class=HTMLResponse)
|
||||
async def run_invoke(
|
||||
request: Request,
|
||||
agent_name: str = Form(...),
|
||||
input: str = Form(...),
|
||||
) -> HTMLResponse:
|
||||
"""Ejecuta el agente usando el orchestrator directamente (mismo proceso)."""
|
||||
from forja_core.api.deps import get_policy_store
|
||||
|
||||
registry = get_registry()
|
||||
policy_store = get_policy_store()
|
||||
orchestrator = get_orchestrator()
|
||||
|
||||
try:
|
||||
agent_def = registry.get_agent(agent_name) # resuelve la versión activa
|
||||
policy_name = agent_def.guardrails[0] if agent_def.guardrails else "default"
|
||||
policy = policy_store.get_policy(policy_name)
|
||||
|
||||
execution: AgentExecution = asyncio.run(
|
||||
orchestrator.invoke(
|
||||
agent_def=agent_def,
|
||||
policy=policy,
|
||||
user_input=input,
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
return HTMLResponse(f'<div class="text-red-400">Error: {str(e)[:300]}</div>', status_code=400)
|
||||
|
||||
# Resultado simple pero informativo
|
||||
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>
|
||||
"""
|
||||
return HTMLResponse(html)
|
||||
|
||||
|
||||
@router.get("/policies", response_class=HTMLResponse)
|
||||
async def policies_page(request: Request) -> HTMLResponse:
|
||||
policy_store = get_policy_store()
|
||||
policies = policy_store.list_policies()
|
||||
return templates.TemplateResponse(
|
||||
"policies.html",
|
||||
{"request": request, "title": "Políticas", "policies": policies, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/history", response_class=HTMLResponse)
|
||||
async def history_page(request: Request) -> HTMLResponse:
|
||||
data_dir = get_settings().data_dir
|
||||
executions = read_execution_summaries(data_dir)[:50]
|
||||
return templates.TemplateResponse(
|
||||
"history.html",
|
||||
{"request": request, "title": "Historial", "executions": executions, "slogan": get_slogan()},
|
||||
)
|
||||
|
||||
|
||||
# ====================== APROBACIONES (HITL) ======================
|
||||
|
||||
@router.get("/approvals", response_class=HTMLResponse)
|
||||
async def approvals_page(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()},
|
||||
)
|
||||
|
||||
|
||||
@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()},
|
||||
)
|
||||
Reference in New Issue
Block a user