feat(frontend): Agent tab with decision-trace expander
Browse filesAdd 6th "🤖 Agent" tab to the Streamlit UI that calls POST /agent/run,
renders the response text + model/finish_reason caption, and shows an
expandable decision-trace panel listing each tool call's name, args,
and result (or error). Also makes _post() timeout configurable via a
keyword arg (default 120 s) for backwards compatibility.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- src/frontend/app.py +53 -3
src/frontend/app.py
CHANGED
|
@@ -935,9 +935,9 @@ def _check_api_health() -> tuple[bool, str]:
|
|
| 935 |
return False, type(e).__name__.lower()
|
| 936 |
|
| 937 |
|
| 938 |
-
def _post(endpoint: str, payload: dict) -> dict:
|
| 939 |
"""POST to the FastAPI surface; let httpx raise on non-2xx."""
|
| 940 |
-
resp = httpx.post(f"{_API_URL}{endpoint}", json=payload, timeout=
|
| 941 |
resp.raise_for_status()
|
| 942 |
return resp.json()
|
| 943 |
|
|
@@ -1752,12 +1752,13 @@ def main() -> None:
|
|
| 1752 |
"Run `uvicorn src.api.main:app --port 8000` or `docker compose up`."
|
| 1753 |
)
|
| 1754 |
|
| 1755 |
-
bbb_tab, eeg_tab, mri_tab, assistant_tab, experiments_tab = st.tabs([
|
| 1756 |
"Molecule",
|
| 1757 |
"Signal",
|
| 1758 |
"Image",
|
| 1759 |
"AI Assistant",
|
| 1760 |
"Experiments",
|
|
|
|
| 1761 |
])
|
| 1762 |
|
| 1763 |
with bbb_tab:
|
|
@@ -1771,6 +1772,55 @@ def main() -> None:
|
|
| 1771 |
with experiments_tab:
|
| 1772 |
_render_experiments_tab()
|
| 1773 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1774 |
|
| 1775 |
if __name__ == "__main__":
|
| 1776 |
main()
|
|
|
|
| 935 |
return False, type(e).__name__.lower()
|
| 936 |
|
| 937 |
|
| 938 |
+
def _post(endpoint: str, payload: dict, timeout: float = 120.0) -> dict:
|
| 939 |
"""POST to the FastAPI surface; let httpx raise on non-2xx."""
|
| 940 |
+
resp = httpx.post(f"{_API_URL}{endpoint}", json=payload, timeout=timeout)
|
| 941 |
resp.raise_for_status()
|
| 942 |
return resp.json()
|
| 943 |
|
|
|
|
| 1752 |
"Run `uvicorn src.api.main:app --port 8000` or `docker compose up`."
|
| 1753 |
)
|
| 1754 |
|
| 1755 |
+
bbb_tab, eeg_tab, mri_tab, assistant_tab, experiments_tab, agent_tab = st.tabs([
|
| 1756 |
"Molecule",
|
| 1757 |
"Signal",
|
| 1758 |
"Image",
|
| 1759 |
"AI Assistant",
|
| 1760 |
"Experiments",
|
| 1761 |
+
"🤖 Agent",
|
| 1762 |
])
|
| 1763 |
|
| 1764 |
with bbb_tab:
|
|
|
|
| 1772 |
with experiments_tab:
|
| 1773 |
_render_experiments_tab()
|
| 1774 |
|
| 1775 |
+
with agent_tab:
|
| 1776 |
+
st.markdown("### Orchestrator Agent")
|
| 1777 |
+
st.caption(
|
| 1778 |
+
"Pick the pipeline automatically, run it, then ground the response "
|
| 1779 |
+
"in curated reference docs (RAG)."
|
| 1780 |
+
)
|
| 1781 |
+
|
| 1782 |
+
with st.form("agent_form"):
|
| 1783 |
+
agent_input = st.text_input(
|
| 1784 |
+
"Input",
|
| 1785 |
+
value="CCO",
|
| 1786 |
+
help="SMILES (e.g., CCO), .fif/.edf path, or NIfTI directory path",
|
| 1787 |
+
)
|
| 1788 |
+
agent_question = st.text_input(
|
| 1789 |
+
"Question (optional)",
|
| 1790 |
+
value="",
|
| 1791 |
+
help="Ask in any language — the agent will mirror it in the response",
|
| 1792 |
+
)
|
| 1793 |
+
submitted = st.form_submit_button("Run agent")
|
| 1794 |
+
|
| 1795 |
+
if submitted and agent_input:
|
| 1796 |
+
with st.spinner("Agent is reasoning..."):
|
| 1797 |
+
try:
|
| 1798 |
+
payload: dict = {"user_input": agent_input}
|
| 1799 |
+
if agent_question:
|
| 1800 |
+
payload["user_question"] = agent_question
|
| 1801 |
+
response = _post("/agent/run", payload, timeout=120.0)
|
| 1802 |
+
except Exception as e:
|
| 1803 |
+
st.error(f"Agent run failed: {e}")
|
| 1804 |
+
else:
|
| 1805 |
+
st.markdown("#### Response")
|
| 1806 |
+
st.write(response.get("text", ""))
|
| 1807 |
+
st.caption(
|
| 1808 |
+
f"model: `{response.get('model', '?')}` · "
|
| 1809 |
+
f"finish: `{response.get('finish_reason', '?')}`"
|
| 1810 |
+
)
|
| 1811 |
+
trace = response.get("trace", [])
|
| 1812 |
+
expander_title = f"🧠 Decision trace ({len(trace)} step{'s' if len(trace) != 1 else ''})"
|
| 1813 |
+
with st.expander(expander_title, expanded=True):
|
| 1814 |
+
if not trace:
|
| 1815 |
+
st.write("_(no tool calls)_")
|
| 1816 |
+
for i, step in enumerate(trace, start=1):
|
| 1817 |
+
st.markdown(f"**{i}. `{step['name']}`**")
|
| 1818 |
+
if step.get("error"):
|
| 1819 |
+
st.error(step["error"])
|
| 1820 |
+
else:
|
| 1821 |
+
st.json(step.get("args", {}))
|
| 1822 |
+
st.json(step.get("result", {}))
|
| 1823 |
+
|
| 1824 |
|
| 1825 |
if __name__ == "__main__":
|
| 1826 |
main()
|