From 826d5b494e7107b7d00feca172acfdea39972580 Mon Sep 17 00:00:00 2001 From: Juan Date: Mon, 11 May 2026 12:44:07 +0200 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20p=C3=A1gina=20Aprobaciones?= =?UTF-8?q?=20con=20HITL=20approve/reject=20por=20acci=C3=B3n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lista ejecuciones en awaiting_approval; por cada acción de needs_human_for muestra risk_score coloreado, target y rollback_plan con un checkbox de aprobación. Aprueba el subconjunto seleccionado (con comentario) o rechaza con razón vía /approve y /reject; rerun tras la decisión. Co-Authored-By: Claude Opus 4.7 --- .../pages/3_🤝_Aprobaciones.py | 82 +++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 dashboard/src/agentforge_dashboard/pages/3_🤝_Aprobaciones.py diff --git a/dashboard/src/agentforge_dashboard/pages/3_🤝_Aprobaciones.py b/dashboard/src/agentforge_dashboard/pages/3_🤝_Aprobaciones.py new file mode 100644 index 0000000..4698dd5 --- /dev/null +++ b/dashboard/src/agentforge_dashboard/pages/3_🤝_Aprobaciones.py @@ -0,0 +1,82 @@ +"""Página de aprobaciones pendientes (HITL).""" + +from __future__ import annotations + +import streamlit as st + +from agentforge_dashboard.client import CoreClient + + +@st.cache_resource +def get_client() -> CoreClient: + return CoreClient() + + +client = get_client() +st.title("🤝 Aprobaciones pendientes") +st.caption("Ejecuciones pausadas en HITL. Revisa cada acción propuesta y aprueba o rechaza.") + +executions = client.list_executions() +pending = [e for e in executions if e["status"] == "awaiting_approval"] + +if not pending: + st.success("No hay aprobaciones pendientes.") + st.stop() + +selected = st.selectbox( + "Ejecución", + pending, + format_func=lambda e: f"{e['trace_id'][:8]} — {e['agent_name']} @ {e['agent_version']}", +) + +execution = client.get_execution(selected["trace_id"]) + +st.markdown(f"**trace_id:** `{execution['trace_id']}`") +st.markdown(f"**Agente:** `{execution['agent_name']}@{execution['agent_version']}`") +st.markdown(f"**Iniciada:** {execution['started_at']}") +if execution.get("final_output"): + with st.expander("Output bruto del LLM"): + st.json(execution["final_output"]) + +st.subheader("Acciones propuestas (requieren aprobación)") + +needs = execution.get("needs_human_for") or [] +if not needs: + st.info("No hay acciones que requieran aprobación.") + st.stop() + +approved_ids: list[str] = [] +for action in needs: + with st.container(border=True): + risk = action["risk_score"] + risk_color = {1: "🟩", 2: "🟩", 3: "🟨", 4: "🟧", 5: "🟥"}.get(risk, "⬜") + st.markdown(f"### {risk_color} `{action['action']}` → `{action['target']}`") + st.write(f"**Risk score:** {risk}/5 | Requires approval: `{action['requires_approval']}`") + st.write(f"**Rollback plan:** {action['rollback_plan']}") + if st.checkbox("Aprobar esta acción", key=f"chk_{action['id']}"): + approved_ids.append(action["id"]) + +comment = st.text_input("Comentario (opcional)") +col_a, col_r = st.columns(2) +with col_a: + if st.button("✅ Aprobar seleccionadas", type="primary", disabled=not approved_ids): + with st.spinner("Aplicando aprobación..."): + r = client.approve( + execution["trace_id"], + {"approved_action_ids": approved_ids, "comment": comment}, + ) + if "error" in r: + st.error(r["error"]) + else: + st.success(f"Status: {r['status']}") + st.rerun() +with col_r: + reason = st.text_input("Razón de rechazo") + if st.button("❌ Rechazar ejecución", disabled=not reason): + with st.spinner("Aplicando rechazo..."): + r = client.reject(execution["trace_id"], {"reason": reason}) + if "error" in r: + st.error(r["error"]) + else: + st.success(f"Status: {r['status']}") + st.rerun()