from __future__ import annotations import json from typing import Any import streamlit as st from .agent import Agent from .config import load_config from .gpfs_client import GPFSClient from .tools import ToolDispatcher st.set_page_config(page_title="GPFS Agent", page_icon="🗄️", layout="wide") def _init_session() -> None: if "cfg" in st.session_state: return try: cfg = load_config() except RuntimeError as exc: st.error(f"Config error: {exc}") st.stop() st.session_state.cfg = cfg st.session_state.client = GPFSClient(cfg) st.session_state.tool_log: list[dict[str, Any]] = [] st.session_state.transcript: list[dict[str, str]] = [] st.session_state.allow_mutations_session = False def confirm(message: str, call: dict[str, Any]) -> bool: return bool(st.session_state.get("allow_mutations_session", False)) dispatcher = ToolDispatcher(st.session_state.cfg, st.session_state.client, confirm=confirm) def on_tool(name: str, tool_input: dict[str, Any], result: dict[str, Any]) -> None: st.session_state.tool_log.append( {"name": name, "input": tool_input, "result": result} ) st.session_state.agent = Agent(cfg, dispatcher, on_tool_event=on_tool) def _mode_label(cfg) -> str: if cfg.allow_writes and cfg.allow_destructive: return "writes + DELETE" if cfg.allow_writes: return "writes (no DELETE)" return "read-only" def _render_sidebar() -> None: cfg = st.session_state.cfg with st.sidebar: st.subheader("Session") st.markdown(f"**Model** \n`{cfg.model}`") st.markdown(f"**API base** \n`{cfg.gpfs_base}`") st.markdown(f"**Mode** \n{_mode_label(cfg)}") if cfg.allow_writes and cfg.confirm_mutations: st.checkbox( "Auto-approve mutations this session", key="allow_mutations_session", help=( "When off, the agent's mutating calls are denied. " "Turn this on to let it run POST/PUT" + (" / DELETE" if cfg.allow_destructive else "") + "." ), ) elif not cfg.allow_writes: st.caption("Writes disabled in .env (GPFS_ALLOW_WRITES=false).") st.divider() if st.button("Reset conversation", use_container_width=True): st.session_state.agent.reset() st.session_state.transcript = [] st.session_state.tool_log = [] st.rerun() with st.expander("System prompt"): st.code(st.session_state.agent.system_prompt, language="markdown") with st.expander(f"Tool log ({len(st.session_state.tool_log)})"): for entry in reversed(st.session_state.tool_log[-25:]): ti = entry["input"] res = entry["result"] status = res.get("status", "ERR") ok = res.get("ok", False) icon = "✅" if ok else "⚠️" st.markdown( f"{icon} `{ti.get('method','?')} {ti.get('path','?')}` → **{status}**" ) if ti.get("reason"): st.caption(ti["reason"]) with st.expander("payload"): st.code(json.dumps(res, indent=2, default=str), language="json") def _render_transcript() -> None: for turn in st.session_state.transcript: with st.chat_message(turn["role"]): st.markdown(turn["content"]) def _handle_user_input(prompt: str) -> None: st.session_state.transcript.append({"role": "user", "content": prompt}) with st.chat_message("user"): st.markdown(prompt) with st.chat_message("assistant"): with st.spinner("Thinking…"): try: reply = st.session_state.agent.chat(prompt) except Exception as exc: reply = f"_Agent error: `{exc!r}`_" st.markdown(reply) st.session_state.transcript.append({"role": "assistant", "content": reply}) def main() -> None: _init_session() _render_sidebar() st.title("GPFS Agent") st.caption("Chat with your IBM Spectrum Scale cluster. Guardrails come from `.env`.") _render_transcript() prompt = st.chat_input("Ask about the cluster…") if prompt: _handle_user_input(prompt) main()