From a27e9723608772ec0fa9de81c00c11a6d4adb912 Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 11 May 2026 12:39:16 +0200 Subject: [PATCH] feat(dashboard): scaffolding Streamlit con cliente HTTP y health check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CoreClient (httpx sync, 2 retries) cubre todos los endpoints del core y mapea 404/409/422 a {"error": ...}. app.py monta la página raíz con sidebar de branding y un health-check del core. Co-Authored-By: Claude Opus 4.7 --- dashboard/src/agentforge_dashboard/app.py | 47 +++++++++++++ dashboard/src/agentforge_dashboard/client.py | 71 ++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 dashboard/src/agentforge_dashboard/app.py create mode 100644 dashboard/src/agentforge_dashboard/client.py diff --git a/dashboard/src/agentforge_dashboard/app.py b/dashboard/src/agentforge_dashboard/app.py new file mode 100644 index 0000000..a890b45 --- /dev/null +++ b/dashboard/src/agentforge_dashboard/app.py @@ -0,0 +1,47 @@ +"""Entry point del dashboard Streamlit con sidebar de branding y health.""" + +from __future__ import annotations + +import streamlit as st + +from agentforge_dashboard.client import CoreClient + + +@st.cache_resource +def get_client() -> CoreClient: + return CoreClient() + + +def main() -> None: + st.set_page_config( + page_title="AgentForge", + page_icon="🛡️", + layout="wide", + initial_sidebar_state="expanded", + ) + st.sidebar.markdown("## 🛡️ AgentForge") + st.sidebar.caption("Plataforma de gobernanza de agentes IA") + client = get_client() + try: + client.health() + st.sidebar.success("Core API: OK") + except Exception as exc: + st.sidebar.error(f"Core API unreachable: {exc}") + + st.title("🛡️ AgentForge") + st.markdown( + """ + Bienvenido al panel de gobernanza de agentes IA. + + Usa la barra lateral para navegar: + - **Registro**: catálogo de agentes y versiones. + - **Ejecutar**: lanzar un agente con guardrails completos. + - **Aprobaciones**: ejecuciones pausadas a la espera de revisión humana. + - **Historial**: trazas, violaciones y resultados. + - **Politicas**: políticas de guardrails y diff entre versiones. + """ + ) + + +if __name__ == "__main__": + main() diff --git a/dashboard/src/agentforge_dashboard/client.py b/dashboard/src/agentforge_dashboard/client.py new file mode 100644 index 0000000..93f4980 --- /dev/null +++ b/dashboard/src/agentforge_dashboard/client.py @@ -0,0 +1,71 @@ +"""Cliente HTTP tipado al core. Encapsula httpx y mapea respuestas a dicts.""" + +from __future__ import annotations + +import os +from typing import Any + +import httpx + + +class CoreClient: + """Cliente sincrono (Streamlit es sync). 2 retries con httpx Transport.""" + + def __init__(self, base_url: str | None = None, timeout: float = 30.0) -> None: + self.base_url = base_url or os.getenv("AGENTFORGE_CORE_URL", "http://localhost:8000") + transport = httpx.HTTPTransport(retries=2) + self._client = httpx.Client(base_url=self.base_url, timeout=timeout, transport=transport) + + 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") + + # ----- helpers privados ----- + + 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 {"error": r.json()} + r.raise_for_status() + return r.json()