algunas soluciones
This commit is contained in:
@@ -66,6 +66,9 @@ class CoreClient:
|
|||||||
def _post(self, path: str, body: dict) -> Any:
|
def _post(self, path: str, body: dict) -> Any:
|
||||||
r = self._client.post(path, json=body)
|
r = self._client.post(path, json=body)
|
||||||
if r.status_code in {404, 409, 422}:
|
if r.status_code in {404, 409, 422}:
|
||||||
return {"error": r.json()}
|
# Clave deliberadamente distinta de "error": el cuerpo de un AgentExecution
|
||||||
|
# correcto ya trae error=None, así que las páginas no podrían distinguir
|
||||||
|
# "ejecución sin error" de "la API rechazó la petición" si reusáramos "error".
|
||||||
|
return {"api_error": r.json()}
|
||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|||||||
@@ -53,7 +53,9 @@ if st.button("🚀 Invocar agente", type="primary", disabled=not user_input):
|
|||||||
st.session_state["last_execution"] = result
|
st.session_state["last_execution"] = result
|
||||||
|
|
||||||
execution = st.session_state.get("last_execution")
|
execution = st.session_state.get("last_execution")
|
||||||
if execution and "error" not in execution:
|
if execution and "api_error" in execution:
|
||||||
|
st.error(f"Error de la API: {execution['api_error']}")
|
||||||
|
elif execution:
|
||||||
st.divider()
|
st.divider()
|
||||||
st.markdown(f"**trace_id:** `{execution['trace_id']}`")
|
st.markdown(f"**trace_id:** `{execution['trace_id']}`")
|
||||||
st.markdown(f"**Status:** `{execution['status']}`")
|
st.markdown(f"**Status:** `{execution['status']}`")
|
||||||
@@ -63,10 +65,10 @@ if execution and "error" not in execution:
|
|||||||
"Esta ejecución requiere aprobación humana. "
|
"Esta ejecución requiere aprobación humana. "
|
||||||
"Ve a la página **Aprobaciones** para revisar y decidir."
|
"Ve a la página **Aprobaciones** para revisar y decidir."
|
||||||
)
|
)
|
||||||
|
if execution.get("error"):
|
||||||
|
st.error(f"La ejecución terminó con error: `{execution['error']}`")
|
||||||
if execution.get("final_output"):
|
if execution.get("final_output"):
|
||||||
st.subheader("📦 Output final")
|
st.subheader("📦 Output final")
|
||||||
st.json(execution["final_output"])
|
st.json(execution["final_output"])
|
||||||
render_violations(execution.get("violations", []))
|
render_violations(execution.get("violations", []))
|
||||||
render_trace(execution.get("decision_path", []))
|
render_trace(execution.get("decision_path", []))
|
||||||
elif execution and "error" in execution:
|
|
||||||
st.error(f"Error de la API: {execution['error']}")
|
|
||||||
|
|||||||
@@ -65,8 +65,8 @@ with col_a:
|
|||||||
execution["trace_id"],
|
execution["trace_id"],
|
||||||
{"approved_action_ids": approved_ids, "comment": comment},
|
{"approved_action_ids": approved_ids, "comment": comment},
|
||||||
)
|
)
|
||||||
if "error" in r:
|
if "api_error" in r:
|
||||||
st.error(r["error"])
|
st.error(r["api_error"])
|
||||||
else:
|
else:
|
||||||
st.success(f"Status: {r['status']}")
|
st.success(f"Status: {r['status']}")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
@@ -75,8 +75,8 @@ with col_r:
|
|||||||
if st.button("❌ Rechazar ejecución", disabled=not reason):
|
if st.button("❌ Rechazar ejecución", disabled=not reason):
|
||||||
with st.spinner("Aplicando rechazo..."):
|
with st.spinner("Aplicando rechazo..."):
|
||||||
r = client.reject(execution["trace_id"], {"reason": reason})
|
r = client.reject(execution["trace_id"], {"reason": reason})
|
||||||
if "error" in r:
|
if "api_error" in r:
|
||||||
st.error(r["error"])
|
st.error(r["api_error"])
|
||||||
else:
|
else:
|
||||||
st.success(f"Status: {r['status']}")
|
st.success(f"Status: {r['status']}")
|
||||||
st.rerun()
|
st.rerun()
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"""Tests del CoreClient: cómo mapea respuestas HTTP del core a dicts.
|
||||||
|
|
||||||
|
Importante para el dashboard: una ejecución correcta (``AgentExecution`` serializado)
|
||||||
|
incluye SIEMPRE el campo ``error`` (``str | None``). El wrapper que el cliente añade
|
||||||
|
ante un 4xx debe usar una clave distinta (``api_error``) para no colisionar con ese
|
||||||
|
campo; si usara ``error``, las páginas no podrían distinguir "el agente terminó sin
|
||||||
|
error" de "la API devolvió 404/409/422".
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from agentforge_dashboard.client import CoreClient
|
||||||
|
|
||||||
|
|
||||||
|
def _client_with(handler: Callable[[httpx.Request], httpx.Response]) -> CoreClient:
|
||||||
|
client = CoreClient(base_url="http://test")
|
||||||
|
client._client.close()
|
||||||
|
client._client = httpx.Client(base_url="http://test", transport=httpx.MockTransport(handler))
|
||||||
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_2xx_devuelve_el_cuerpo_verbatim_incluido_error_none() -> None:
|
||||||
|
"""Un AgentExecution con ``error=None`` se devuelve tal cual, sin envolver."""
|
||||||
|
execution = {
|
||||||
|
"trace_id": "11111111-1111-1111-1111-111111111111",
|
||||||
|
"status": "completed",
|
||||||
|
"error": None,
|
||||||
|
"violations": [],
|
||||||
|
"decision_path": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
def handler(_: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(200, json=execution)
|
||||||
|
|
||||||
|
result = _client_with(handler).invoke_agent("incident_analyzer", {"input": "x"})
|
||||||
|
assert result == execution
|
||||||
|
assert "api_error" not in result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("status", [404, 409, 422])
|
||||||
|
def test_post_4xx_envuelve_el_detalle_en_api_error(status: int) -> None:
|
||||||
|
def handler(_: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(status, json={"detail": "boom"})
|
||||||
|
|
||||||
|
result = _client_with(handler).invoke_agent("incident_analyzer", {"input": "y"})
|
||||||
|
assert result == {"api_error": {"detail": "boom"}}
|
||||||
|
|
||||||
|
|
||||||
|
def test_post_5xx_propaga_la_excepcion() -> None:
|
||||||
|
def handler(_: httpx.Request) -> httpx.Response:
|
||||||
|
return httpx.Response(500, json={"detail": "kaput"})
|
||||||
|
|
||||||
|
with pytest.raises(httpx.HTTPStatusError):
|
||||||
|
_client_with(handler).invoke_agent("incident_analyzer", {"input": "z"})
|
||||||
Reference in New Issue
Block a user