migracion de streamlit a niceguy
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
"""forja_common - shared utilities (client, types, etc.)."""
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
__all__ = ["CoreClient"]
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Shared HTTP client for Forja Core API (used by web and cli)."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
|
||||||
|
class CoreClient:
|
||||||
|
"""Synchronous HTTP client to the Forja Core API."""
|
||||||
|
|
||||||
|
def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None:
|
||||||
|
self.base_url = base_url or os.getenv("FORJA_CORE_URL", "http://localhost:8000")
|
||||||
|
transport = httpx.HTTPTransport(retries=2)
|
||||||
|
self._client = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
self._client.close()
|
||||||
|
|
||||||
|
# --- Public API methods ---
|
||||||
|
|
||||||
|
def health(self) -> dict:
|
||||||
|
return self._get("/health")
|
||||||
|
|
||||||
|
def list_agents(self) -> list[dict]:
|
||||||
|
return self._get("/agents")
|
||||||
|
|
||||||
|
def get_agent(self, name: str) -> dict:
|
||||||
|
return self._get(f"/agents/{name}")
|
||||||
|
|
||||||
|
def list_versions(self, name: str) -> list[dict]:
|
||||||
|
return self._get(f"/agents/{name}/versions")
|
||||||
|
|
||||||
|
def diff_versions(self, name: str, v_from: str, v_to: str) -> dict:
|
||||||
|
return self._get(f"/agents/{name}/versions/{v_from}/diff/{v_to}")
|
||||||
|
|
||||||
|
def invoke_agent(self, name: str, body: dict) -> dict:
|
||||||
|
return self._post(f"/agents/{name}/invoke", body)
|
||||||
|
|
||||||
|
def get_execution(self, trace_id: str) -> dict:
|
||||||
|
return self._get(f"/executions/{trace_id}")
|
||||||
|
|
||||||
|
def list_executions(self) -> list[dict]:
|
||||||
|
return self._get("/executions")
|
||||||
|
|
||||||
|
def approve(self, trace_id: str, body: dict) -> dict:
|
||||||
|
return self._post(f"/executions/{trace_id}/approve", body)
|
||||||
|
|
||||||
|
def reject(self, trace_id: str, body: dict) -> dict:
|
||||||
|
return self._post(f"/executions/{trace_id}/reject", body)
|
||||||
|
|
||||||
|
def list_violations(self, **filters: Any) -> list[dict]:
|
||||||
|
params = {k: v for k, v in filters.items() if v is not None}
|
||||||
|
return self._get("/violations", params=params)
|
||||||
|
|
||||||
|
def list_policies(self) -> list[dict]:
|
||||||
|
return self._get("/policies")
|
||||||
|
|
||||||
|
def list_policy_versions(self, name: str) -> list[dict]:
|
||||||
|
return self._get(f"/policies/{name}/versions")
|
||||||
|
|
||||||
|
def create_agent_version(self, name: str, agent_def: dict, message: str, author: str = "cli") -> dict:
|
||||||
|
body = {"agent": agent_def, "message": message, "author": author}
|
||||||
|
return self._post(f"/agents/{name}/versions", body)
|
||||||
|
|
||||||
|
def create_policy_version(self, name: str, policy_def: dict, message: str, author: str = "cli") -> dict:
|
||||||
|
body = {"policy": policy_def, "message": message, "author": author}
|
||||||
|
return self._post(f"/policies/{name}/versions", body)
|
||||||
|
|
||||||
|
def list_validators(self) -> dict:
|
||||||
|
return self._get("/policies/validators")
|
||||||
|
|
||||||
|
# --- Internal helpers ---
|
||||||
|
|
||||||
|
def _get(self, path: str, params: dict | None = None) -> Any:
|
||||||
|
r = self._client.get(path, params=params)
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
|
def _post(self, path: str, body: dict) -> Any:
|
||||||
|
r = self._client.post(path, json=body)
|
||||||
|
if r.status_code in {404, 409, 422}:
|
||||||
|
return {"api_error": r.json()}
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
@@ -9,11 +9,8 @@ langgraph-checkpoint-sqlite>=2.0,<3.0
|
|||||||
# AsyncSqliteSaver (langgraph-checkpoint-sqlite 2.x) usa Connection.is_alive(),
|
# AsyncSqliteSaver (langgraph-checkpoint-sqlite 2.x) usa Connection.is_alive(),
|
||||||
# eliminado en aiosqlite 0.21. Fijado a la última serie compatible.
|
# eliminado en aiosqlite 0.21. Fijado a la última serie compatible.
|
||||||
aiosqlite>=0.20,<0.21
|
aiosqlite>=0.20,<0.21
|
||||||
guardrails-ai>=0.5,<0.6
|
|
||||||
# typer 0.12.x (pin transitivo de guardrails-ai <0.6) rompe con click >= 8.2
|
|
||||||
# ("Secondary flag is not valid for non-boolean flag" al ejecutar `spacy download`
|
# ("Secondary flag is not valid for non-boolean flag" al ejecutar `spacy download`
|
||||||
# y la CLI de typer/weasel). Fijado a la última serie 8.1.
|
# y la CLI de typer/weasel). Fijado a la última serie 8.1.
|
||||||
click>=8.1,<8.2
|
|
||||||
presidio-analyzer>=2.2,<3.0
|
presidio-analyzer>=2.2,<3.0
|
||||||
presidio-anonymizer>=2.2,<3.0
|
presidio-anonymizer>=2.2,<3.0
|
||||||
openai>=1.50,<2.0
|
openai>=1.50,<2.0
|
||||||
|
|||||||
@@ -139,12 +139,12 @@
|
|||||||
<div class="text-xs uppercase tracking-[1px] text-zinc-500 mb-3 font-medium">Progreso general</div>
|
<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="flex items-baseline gap-x-2">
|
||||||
<div class="text-6xl font-semibold tabular-nums tracking-tighter">78</div>
|
<div class="text-6xl font-semibold tabular-nums tracking-tighter">82</div>
|
||||||
<div class="text-2xl font-medium text-zinc-400">/100</div>
|
<div class="text-2xl font-medium text-zinc-400">/100</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="h-2.5 bg-zinc-800 rounded-full mt-3 overflow-hidden">
|
<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: 78%"></div>
|
<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>
|
||||||
|
|
||||||
<div class="grid grid-cols-3 gap-4 mt-6 text-center">
|
<div class="grid grid-cols-3 gap-4 mt-6 text-center">
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-zinc-500">UI Gráfica</div>
|
<div class="text-xs text-zinc-500">UI Gráfica</div>
|
||||||
<div class="font-semibold text-xl">95%</div>
|
<div class="font-semibold text-xl">68%</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="text-xs text-zinc-500">Docs</div>
|
<div class="text-xs text-zinc-500">Docs</div>
|
||||||
@@ -194,6 +194,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</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="bg-zinc-900 border border-zinc-800 rounded-3xl p-4">
|
||||||
<div class="flex items-center gap-x-3">
|
<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 class="feature-icon bg-sky-500/10 text-sky-400"><i class="fa-solid fa-database"></i></div>
|
||||||
@@ -378,13 +389,13 @@
|
|||||||
<!-- Notas técnicas -->
|
<!-- Notas técnicas -->
|
||||||
<div class="mt-16 text-xs text-zinc-500 border-t border-zinc-800 pt-8">
|
<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 class="flex flex-wrap gap-x-8 gap-y-2">
|
||||||
<div><strong>Paquetes:</strong> <span class="mono">forja_core</span> / <span class="mono">forja_dashboard</span></div>
|
<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>Python:</strong> 3.11+</div>
|
||||||
<div><strong>UI:</strong> Streamlit 1.38 + Tailwind (editores)</div>
|
<div><strong>UI:</strong> NiceGUI (reemplazo moderno de Streamlit) + Tailwind</div>
|
||||||
<div><strong>Runtime:</strong> LangGraph + FastAPI</div>
|
<div><strong>Runtime:</strong> LangGraph + FastAPI</div>
|
||||||
<div><strong>Persistencia:</strong> YAML versionado + SQLite checkpoints</div>
|
<div><strong>Persistencia:</strong> YAML versionado + SQLite checkpoints</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-[10px] text-zinc-600">Este documento fue generado automáticamente tras la sesión de refactorización del 23 de mayo de 2026.</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
+12
-2
@@ -9,6 +9,16 @@ description = "Plataforma profesional de gobernanza de agentes IA"
|
|||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
authors = [{name = "Juan", email = "jm0x@proton.me"}]
|
authors = [{name = "Juan", email = "jm0x@proton.me"}]
|
||||||
|
|
||||||
|
[project.dependencies]
|
||||||
|
httpx = ">=0.27,<0.28"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
web = ["nicegui>=2.0"]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["core/src", "common/src", "web/src"]
|
||||||
|
namespaces = false
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py311"
|
target-version = "py311"
|
||||||
@@ -23,7 +33,7 @@ ignore = ["E501"]
|
|||||||
"dashboard/src/forja_dashboard/pages/*" = ["N999"]
|
"dashboard/src/forja_dashboard/pages/*" = ["N999"]
|
||||||
|
|
||||||
[tool.ruff.lint.isort]
|
[tool.ruff.lint.isort]
|
||||||
known-first-party = ["forja_core", "forja_dashboard"]
|
known-first-party = ["forja_core", "forja_dashboard", "forja_common", "forja_web"]
|
||||||
|
|
||||||
[tool.mypy]
|
[tool.mypy]
|
||||||
python_version = "3.11"
|
python_version = "3.11"
|
||||||
@@ -41,7 +51,7 @@ ignore_missing_imports = true
|
|||||||
asyncio_mode = "auto"
|
asyncio_mode = "auto"
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
addopts = "-ra -q --strict-markers"
|
addopts = "-ra -q --strict-markers"
|
||||||
pythonpath = ["core/src", "dashboard/src"]
|
pythonpath = ["core/src", "dashboard/src", "common/src", "web/src"]
|
||||||
markers = [
|
markers = [
|
||||||
"integration: integration tests (slower, may touch FS)",
|
"integration: integration tests (slower, may touch FS)",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
nicegui>=2.0
|
||||||
|
httpx>=0.27,<0.28
|
||||||
|
rich>=13.0 # optional, for nice tables if we want
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
"""forja_web - Modern NiceGUI dashboard for Forja."""
|
||||||
|
|
||||||
|
from forja_web.main import main
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
"""Forja Web - NiceGUI modern dashboard."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_web.pages import agents_page, approvals_page, executions_page, home_page, policies_page, run_page
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
ui.page_title("Forja • Gobernanza de Agentes IA")
|
||||||
|
|
||||||
|
# Top bar
|
||||||
|
with ui.header().classes("bg-zinc-900 text-white items-center shadow-md"):
|
||||||
|
with ui.row().classes("w-full items-center px-6"):
|
||||||
|
ui.label("🔨 Forja").classes("text-2xl font-bold tracking-tight")
|
||||||
|
ui.space()
|
||||||
|
ui.label("Plataforma de gobernanza para agentes IA").classes("text-zinc-400 text-sm")
|
||||||
|
|
||||||
|
# Left drawer navigation
|
||||||
|
with ui.left_drawer(value=True, bordered=True).classes("bg-zinc-950 text-white"):
|
||||||
|
ui.label("Navegación").classes("text-xs uppercase tracking-widest text-zinc-500 px-2 pt-4 pb-1")
|
||||||
|
with ui.column().classes("gap-1"):
|
||||||
|
ui.link("Inicio", "/").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Agentes", "/agents").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Ejecutar", "/run").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Aprobaciones", "/approvals").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Historial", "/history").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Políticas", "/policies").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.separator().classes("my-2")
|
||||||
|
ui.link("Forjar Agente", "/forge/agent").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
ui.link("Forjar Política", "/forge/policy").classes("text-base hover:bg-zinc-800 px-3 py-1.5 rounded")
|
||||||
|
|
||||||
|
# Main content area with pages
|
||||||
|
@ui.page("/")
|
||||||
|
def home() -> None:
|
||||||
|
home_page.render()
|
||||||
|
|
||||||
|
@ui.page("/agents")
|
||||||
|
def agents() -> None:
|
||||||
|
agents_page.render()
|
||||||
|
|
||||||
|
@ui.page("/run")
|
||||||
|
def run() -> None:
|
||||||
|
run_page.render()
|
||||||
|
|
||||||
|
@ui.page("/approvals")
|
||||||
|
def approvals() -> None:
|
||||||
|
approvals_page.render()
|
||||||
|
|
||||||
|
@ui.page("/history")
|
||||||
|
def history() -> None:
|
||||||
|
executions_page.render()
|
||||||
|
|
||||||
|
@ui.page("/policies")
|
||||||
|
def policies() -> None:
|
||||||
|
policies_page.render()
|
||||||
|
|
||||||
|
@ui.page("/forge/agent")
|
||||||
|
def forge_agent() -> None:
|
||||||
|
ui.label("🔨 Forjar Agente").classes("text-3xl font-semibold")
|
||||||
|
ui.label("Editor gráfico moderno (en desarrollo)").classes("text-zinc-400 mt-2")
|
||||||
|
|
||||||
|
@ui.page("/forge/policy")
|
||||||
|
def forge_policy() -> None:
|
||||||
|
ui.label("🛡️ Forjar Política").classes("text-3xl font-semibold")
|
||||||
|
ui.label("Editor de políticas (en desarrollo)").classes("text-zinc-400 mt-2")
|
||||||
|
|
||||||
|
ui.run(
|
||||||
|
title="Forja",
|
||||||
|
host="0.0.0.0",
|
||||||
|
port=8501,
|
||||||
|
dark=True,
|
||||||
|
reload=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
"""Agents listing page."""
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("📚 Registro de Agentes").classes("text-3xl font-semibold mb-6")
|
||||||
|
|
||||||
|
client = CoreClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
agents = client.list_agents()
|
||||||
|
except Exception as e:
|
||||||
|
ui.label(f"Error cargando agentes: {e}").classes("text-red-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not agents:
|
||||||
|
ui.label("No hay agentes registrados todavía.").classes("text-zinc-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
for agent in agents:
|
||||||
|
with ui.card().classes("w-full mb-4"):
|
||||||
|
with ui.row().classes("items-center justify-between"):
|
||||||
|
ui.label(agent.get("name", "sin-nombre")).classes("text-xl font-bold")
|
||||||
|
ui.badge(agent.get("latest_version", "v?"), color="blue")
|
||||||
|
|
||||||
|
if "description" in agent:
|
||||||
|
ui.label(agent["description"]).classes("text-sm text-zinc-400")
|
||||||
|
|
||||||
|
with ui.row().classes("gap-2 mt-2"):
|
||||||
|
ui.button("Ver versiones", on_click=lambda n=agent["name"]: ui.navigate.to(f"/agents/{n}"))
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
"""Approvals page (HITL)."""
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("🤝 Aprobaciones pendientes").classes("text-3xl font-semibold mb-6")
|
||||||
|
|
||||||
|
client = CoreClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
executions = client.list_executions()
|
||||||
|
pending = [e for e in executions if e.get("status") in ("awaiting_approval", "hitl")]
|
||||||
|
except Exception as e:
|
||||||
|
ui.label(f"Error: {e}").classes("text-red-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not pending:
|
||||||
|
ui.label("No hay ejecuciones esperando aprobación.").classes("text-emerald-400 text-lg")
|
||||||
|
return
|
||||||
|
|
||||||
|
for ex in pending:
|
||||||
|
with ui.card().classes("w-full mb-3"):
|
||||||
|
ui.label(f"Trace: {ex.get('trace_id')}").classes("font-mono text-sm")
|
||||||
|
ui.label(f"Agente: {ex.get('agent_name')}")
|
||||||
|
with ui.row():
|
||||||
|
ui.button("Aprobar", color="green", on_click=lambda t=ex["trace_id"]: approve(t))
|
||||||
|
ui.button("Rechazar", color="red", on_click=lambda t=ex["trace_id"]: reject(t))
|
||||||
|
|
||||||
|
|
||||||
|
def approve(trace_id: str) -> None:
|
||||||
|
client = CoreClient()
|
||||||
|
client.approve(trace_id, {"notes": "Approved from NiceGUI"})
|
||||||
|
ui.notify("Aprobado", type="positive")
|
||||||
|
ui.navigate.reload()
|
||||||
|
|
||||||
|
|
||||||
|
def reject(trace_id: str) -> None:
|
||||||
|
client = CoreClient()
|
||||||
|
client.reject(trace_id, {"reason": "Rejected from NiceGUI"})
|
||||||
|
ui.notify("Rechazado", type="negative")
|
||||||
|
ui.navigate.reload()
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""History / executions page."""
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("📜 Historial de Ejecuciones").classes("text-3xl font-semibold mb-6")
|
||||||
|
|
||||||
|
client = CoreClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
executions = client.list_executions()
|
||||||
|
except Exception as e:
|
||||||
|
ui.label(f"Error: {e}").classes("text-red-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not executions:
|
||||||
|
ui.label("Aún no hay ejecuciones.").classes("text-zinc-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
for ex in executions[:30]:
|
||||||
|
with ui.card().classes("w-full mb-2"):
|
||||||
|
with ui.row().classes("justify-between items-center"):
|
||||||
|
ui.label(ex.get("trace_id", "")[:20] + "...").classes("font-mono text-sm")
|
||||||
|
status = ex.get("status", "unknown")
|
||||||
|
color = "green" if status == "completed" else "orange"
|
||||||
|
ui.badge(status, color=color)
|
||||||
|
ui.label(f"{ex.get('agent_name')} • {ex.get('created_at', '')}").classes("text-xs text-zinc-400")
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"""Home page for the NiceGUI dashboard."""
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("Bienvenido a Forja").classes("text-4xl font-bold tracking-tight mt-8")
|
||||||
|
ui.label(
|
||||||
|
"Plataforma profesional de gobernanza para agentes de IA. "
|
||||||
|
"Versión moderna con NiceGUI."
|
||||||
|
).classes("text-lg text-zinc-400 mt-2")
|
||||||
|
|
||||||
|
ui.separator().classes("my-8")
|
||||||
|
|
||||||
|
with ui.card().classes("w-full max-w-2xl"):
|
||||||
|
ui.label("Estado del sistema").classes("text-xl font-semibold mb-4")
|
||||||
|
|
||||||
|
try:
|
||||||
|
client = CoreClient()
|
||||||
|
health = client.health()
|
||||||
|
ui.badge("Core API: Saludable", color="green").classes("text-lg")
|
||||||
|
ui.json_editor({"status": health})
|
||||||
|
except Exception as exc:
|
||||||
|
ui.badge(f"Core API unreachable: {exc}", color="red")
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
"""Policies page."""
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("🛡️ Políticas de Guardrails").classes("text-3xl font-semibold mb-6")
|
||||||
|
|
||||||
|
client = CoreClient()
|
||||||
|
|
||||||
|
try:
|
||||||
|
policies = client.list_policies()
|
||||||
|
except Exception as e:
|
||||||
|
ui.label(f"Error: {e}").classes("text-red-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not policies:
|
||||||
|
ui.label("No hay políticas definidas.").classes("text-zinc-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
for p in policies:
|
||||||
|
with ui.card().classes("w-full mb-3"):
|
||||||
|
ui.label(p.get("name")).classes("font-bold text-lg")
|
||||||
|
versions = client.list_policy_versions(p["name"])
|
||||||
|
ui.label(f"{len(versions)} versiones").classes("text-sm text-zinc-400")
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
"""Ejecutar agente page - modern invoke form."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from nicegui import ui
|
||||||
|
|
||||||
|
from forja_common.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def render() -> None:
|
||||||
|
ui.label("▶️ Ejecutar Agente").classes("text-3xl font-semibold mb-2")
|
||||||
|
ui.label("Invoca un agente con todo el gobierno (guardrails + HITL + observabilidad)").classes("text-zinc-400 mb-6")
|
||||||
|
|
||||||
|
client = CoreClient()
|
||||||
|
|
||||||
|
# Load data
|
||||||
|
try:
|
||||||
|
agents = client.list_agents()
|
||||||
|
agent_names = [a["name"] for a in agents] if agents else []
|
||||||
|
policies = client.list_policies()
|
||||||
|
policy_names = [p["name"] for p in policies] if policies else []
|
||||||
|
except Exception as e:
|
||||||
|
ui.label(f"No se pudo conectar al Core: {e}").classes("text-red-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
if not agent_names:
|
||||||
|
ui.label("No hay agentes registrados. Crea uno primero en 'Forjar Agente'.").classes("text-amber-400")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Form state
|
||||||
|
selected_agent = ui.select(agent_names, label="Agente", value=agent_names[0]).classes("w-80")
|
||||||
|
scenario = ui.input("Escenario", value="01_sip_registration_drop").classes("w-80")
|
||||||
|
|
||||||
|
with ui.card().classes("w-full max-w-2xl mt-4"):
|
||||||
|
ui.label("Payload (JSON)").classes("font-medium mb-2")
|
||||||
|
payload_text = ui.textarea(
|
||||||
|
value=json.dumps({"scenario": "01_sip_registration_drop"}, indent=2),
|
||||||
|
placeholder='{"scenario": "..."}',
|
||||||
|
).classes("w-full font-mono text-sm").props("rows=6")
|
||||||
|
|
||||||
|
result_box = ui.column().classes("w-full max-w-2xl mt-4")
|
||||||
|
|
||||||
|
async def do_invoke() -> None:
|
||||||
|
result_box.clear()
|
||||||
|
try:
|
||||||
|
body = json.loads(payload_text.value or "{}")
|
||||||
|
if "scenario" not in body:
|
||||||
|
body["scenario"] = scenario.value
|
||||||
|
|
||||||
|
with result_box:
|
||||||
|
ui.spinner("dots", size="lg")
|
||||||
|
ui.label("Invocando agente con gobernanza...").classes("ml-2")
|
||||||
|
|
||||||
|
res = client.invoke_agent(selected_agent.value, body)
|
||||||
|
|
||||||
|
result_box.clear()
|
||||||
|
with result_box:
|
||||||
|
if "api_error" in res:
|
||||||
|
ui.label("Error de la API").classes("text-red-400 font-bold")
|
||||||
|
ui.json_editor(res["api_error"])
|
||||||
|
else:
|
||||||
|
ui.label("✅ Ejecución iniciada").classes("text-emerald-400 text-lg font-semibold")
|
||||||
|
ui.json_editor(res, options={"readOnly": True})
|
||||||
|
if "trace_id" in res:
|
||||||
|
trace = res["trace_id"]
|
||||||
|
ui.button(
|
||||||
|
"Ver traza completa",
|
||||||
|
on_click=lambda: ui.navigate.to(f"/executions/{trace}"),
|
||||||
|
).classes("mt-2")
|
||||||
|
except Exception as exc:
|
||||||
|
result_box.clear()
|
||||||
|
with result_box:
|
||||||
|
ui.label(f"Error: {exc}").classes("text-red-400")
|
||||||
|
|
||||||
|
ui.button("🚀 Invocar con Gobernanza", on_click=do_invoke, color="primary").classes("mt-4 px-8 py-2 text-lg")
|
||||||
Reference in New Issue
Block a user