From ab21db2476d3e5246bae2f6d6938d9caf91d0a63 Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 11 May 2026 12:42:12 +0200 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20p=C3=A1gina=20Ejecutar=20con?= =?UTF-8?q?=20trace=20timeline=20y=20violations=20rendering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - components/trace_view.render_trace: decision_path como timeline con duración. - components/violation_view.render_violations: violaciones con badge de severidad. - pages/2_▶️_Ejecutar.py: selector de agente, botones de escenarios pregrabados, textarea, invoke y render de status/output/violations/traza. Aviso si queda en awaiting_approval. (Se omite la variable muerta chosen_example del plan.) Co-Authored-By: Claude Opus 4.7 --- .../components/trace_view.py | 24 +++++++ .../components/violation_view.py | 26 +++++++ .../pages/2_▶️_Ejecutar.py | 72 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100644 dashboard/src/agentforge_dashboard/components/trace_view.py create mode 100644 dashboard/src/agentforge_dashboard/components/violation_view.py create mode 100644 dashboard/src/agentforge_dashboard/pages/2_▶️_Ejecutar.py diff --git a/dashboard/src/agentforge_dashboard/components/trace_view.py b/dashboard/src/agentforge_dashboard/components/trace_view.py new file mode 100644 index 0000000..94aff8e --- /dev/null +++ b/dashboard/src/agentforge_dashboard/components/trace_view.py @@ -0,0 +1,24 @@ +"""Renderiza el decision_path como timeline.""" + +from __future__ import annotations + +import streamlit as st + + +def render_trace(decision_path: list[dict]) -> None: + if not decision_path: + st.info("Aún no hay traza disponible.") + return + st.subheader("📜 Decision path") + for step in decision_path: + with st.container(border=True): + cols = st.columns([2, 1, 1]) + with cols[0]: + st.markdown(f"**{step['step']}**") + with cols[1]: + st.caption(step.get("timestamp", "—")) + with cols[2]: + st.metric("ms", step.get("duration_ms", 0), label_visibility="collapsed") + if step.get("detail"): + with st.expander("detalle"): + st.json(step["detail"]) diff --git a/dashboard/src/agentforge_dashboard/components/violation_view.py b/dashboard/src/agentforge_dashboard/components/violation_view.py new file mode 100644 index 0000000..584a22e --- /dev/null +++ b/dashboard/src/agentforge_dashboard/components/violation_view.py @@ -0,0 +1,26 @@ +"""Renderiza violaciones de guardrails con badges de severidad.""" + +from __future__ import annotations + +import streamlit as st + +_SEV_BADGE = { + "info": "🟦", + "warning": "🟧", + "block": "🟥", +} + + +def render_violations(violations: list[dict]) -> None: + if not violations: + st.success("Sin violaciones.") + return + st.subheader("🛡️ Violaciones de guardrails") + for v in violations: + with st.container(border=True): + badge = _SEV_BADGE.get(v["severity"], "⬜") + st.markdown( + f"{badge} **{v['validator']}** — `{v['stage']}` — " + f"severity=`{v['severity']}` — blocked=`{v['blocked']}`" + ) + st.caption(v["message"]) diff --git a/dashboard/src/agentforge_dashboard/pages/2_▶️_Ejecutar.py b/dashboard/src/agentforge_dashboard/pages/2_▶️_Ejecutar.py new file mode 100644 index 0000000..b78ecc9 --- /dev/null +++ b/dashboard/src/agentforge_dashboard/pages/2_▶️_Ejecutar.py @@ -0,0 +1,72 @@ +"""Página para invocar un agente y visualizar la traza completa.""" + +from __future__ import annotations + +from pathlib import Path + +import streamlit as st + +from agentforge_dashboard.client import CoreClient +from agentforge_dashboard.components.trace_view import render_trace +from agentforge_dashboard.components.violation_view import render_violations + + +@st.cache_resource +def get_client() -> CoreClient: + return CoreClient() + + +client = get_client() +st.title("▶️ Ejecutar Agente") +st.caption("Lanza una ejecución con la cadena completa de gobierno.") + +agents = client.list_agents() +if not agents: + st.warning("No hay agentes registrados.") + st.stop() + +names = [a["name"] for a in agents] +agent_name = st.selectbox("Agente", names) + +# Cargar escenarios pregrabados si existen en el disco montado. +examples_dir = Path("agents") / agent_name / "examples" +example_files = sorted(examples_dir.glob("*.txt")) if examples_dir.exists() else [] + +col_l, col_r = st.columns([3, 1]) +with col_r: + st.markdown("**Escenarios**") + for ef in example_files: + if st.button(ef.stem, use_container_width=True): + st.session_state["input_text"] = ef.read_text(encoding="utf-8") + +with col_l: + user_input = st.text_area( + "Descripción del incidente", + height=240, + key="input_text", + placeholder="Pega aquí la descripción del incidente o usa un escenario...", + ) + +if st.button("🚀 Invocar agente", type="primary", disabled=not user_input): + with st.spinner("Ejecutando..."): + result = client.invoke_agent(agent_name, {"input": user_input}) + st.session_state["last_execution"] = result + +execution = st.session_state.get("last_execution") +if execution and "error" not in execution: + st.divider() + st.markdown(f"**trace_id:** `{execution['trace_id']}`") + st.markdown(f"**Status:** `{execution['status']}`") + + if execution["status"] == "awaiting_approval": + st.warning( + "Esta ejecución requiere aprobación humana. " + "Ve a la página **Aprobaciones** para revisar y decidir." + ) + if execution.get("final_output"): + st.subheader("📦 Output final") + st.json(execution["final_output"]) + render_violations(execution.get("violations", [])) + render_trace(execution.get("decision_path", [])) +elif execution and "error" in execution: + st.error(f"Error de la API: {execution['error']}")