gpfsagent/gpfs_agent/cli.py
Laurence Horrocks-Barlow 8840ba74f7 Initial commit: agentic chat app for IBM Storage Scale REST API v3
Adds a Python package that wraps the Storage Scale /scalemgmt/v3 management
API behind a Claude-driven tool-use loop. Ships a Rich-styled terminal REPL
and a Streamlit web UI sharing the same Agent/Dispatcher/GPFSClient stack.

Guardrails are env-driven (.env): read-only by default; writes and DELETE
gated by GPFS_ALLOW_WRITES / GPFS_ALLOW_DESTRUCTIVE; optional path
allow/deny regex; mutating calls require operator confirmation. Free-text
GPFS_INSTRUCTIONS (or GPFS_INSTRUCTIONS_FILE) appended to the system prompt.

The system prompt also includes a curated v3 endpoint reference compiled
from IBM Storage Scale 5.2.3 / 6.0.0 documentation so the agent doesn't
have to guess endpoint shapes for common operations.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 01:36:59 +01:00

112 lines
3.4 KiB
Python

from __future__ import annotations
import argparse
import sys
from pathlib import Path
from typing import Any
from rich.console import Console
from rich.markdown import Markdown
from rich.panel import Panel
from .agent import Agent
from .config import Config, load_config
from .gpfs_client import GPFSClient
from .tools import ToolDispatcher
console = Console()
def _banner(cfg: Config) -> None:
write_state = (
"writes + DELETE"
if cfg.allow_writes and cfg.allow_destructive
else "writes (no DELETE)"
if cfg.allow_writes
else "read-only"
)
body = (
f"[bold]GPFS Agent[/bold] — {cfg.model}\n"
f"API base: {cfg.gpfs_base}\n"
f"Mode: {write_state}"
f"{' · confirm-on-mutate' if cfg.confirm_mutations else ''}\n"
"Type your question. Commands: /reset, /system, /quit"
)
console.print(Panel.fit(body, border_style="cyan"))
def _on_tool_event(name: str, tool_input: dict[str, Any], result: dict[str, Any]) -> None:
method = tool_input.get("method", "?")
path = tool_input.get("path", "?")
status = result.get("status", "ERR")
ok = result.get("ok", False)
colour = "green" if ok else "red"
console.print(
f" [dim]→ {name}[/dim] [{colour}]{method} {path}{status}[/{colour}]"
)
def make_confirm(console: Console):
def _confirm(message: str, call: dict[str, Any]) -> bool:
console.print(Panel.fit(
f"[yellow]{message}[/yellow]\n"
f"[bold]{call['method']}[/bold] {call['path']}\n"
+ (f"body: {call.get('body')}\n" if call.get("body") else "")
+ (f"reason: {call.get('reason')}" if call.get("reason") else ""),
border_style="yellow",
title="confirm mutation",
))
answer = console.input("[yellow]proceed?[/yellow] [y/N] ").strip().lower()
return answer in {"y", "yes"}
return _confirm
def run() -> int:
parser = argparse.ArgumentParser(prog="gpfs-agent")
parser.add_argument("--env", type=Path, default=None, help="Path to a .env file")
args = parser.parse_args()
try:
cfg = load_config(args.env)
except RuntimeError as exc:
console.print(f"[red]config error:[/red] {exc}")
return 2
_banner(cfg)
with GPFSClient(cfg) as client:
dispatcher = ToolDispatcher(cfg, client, confirm=make_confirm(console))
agent = Agent(cfg, dispatcher, on_tool_event=_on_tool_event)
while True:
try:
user = console.input("\n[bold cyan]you[/bold cyan] » ").strip()
except (EOFError, KeyboardInterrupt):
console.print("\nbye.")
return 0
if not user:
continue
if user in {"/quit", "/exit"}:
return 0
if user == "/reset":
agent.reset()
console.print("[dim]conversation reset[/dim]")
continue
if user == "/system":
console.print(Panel(agent.system_prompt, title="system prompt", border_style="magenta"))
continue
try:
reply = agent.chat(user)
except Exception as exc:
console.print(f"[red]agent error:[/red] {exc!r}")
continue
console.print()
console.print(Markdown(reply or "_(no text returned)_"))
if __name__ == "__main__":
sys.exit(run())