feat(dashboard): página Aprobaciones con HITL approve/reject por acción
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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()
|
||||||
Reference in New Issue
Block a user