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