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>
This commit is contained in:
commit
8840ba74f7
13 changed files with 959 additions and 0 deletions
112
gpfs_agent/cli.py
Normal file
112
gpfs_agent/cli.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue