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>
112 lines
3.4 KiB
Python
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())
|