133 lines
3.9 KiB
Python
Executable File
133 lines
3.9 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""odysseus-preset — shell wrapper for AI system-prompt presets.
|
|
|
|
Presets are name → {name, temperature, system_prompt} in
|
|
`data/presets.json`. Used by the chat UI's preset picker.
|
|
|
|
odysseus-preset list
|
|
odysseus-preset get NAME
|
|
odysseus-preset set NAME --temperature 0.7 --prompt "You are..."
|
|
odysseus-preset set NAME --prompt-file ./prompt.md
|
|
odysseus-preset delete NAME
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
import sys
|
|
import os, sys
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "_lib"))
|
|
from cli import quiet_logs, emit, fail, common_parser, run, REPO_ROOT as _REPO_ROOT
|
|
quiet_logs()
|
|
|
|
import argparse, json, logging, os, sys
|
|
from pathlib import Path
|
|
|
|
_PATH = _REPO_ROOT / "data" / "presets.json"
|
|
|
|
|
|
def _load() -> dict:
|
|
if not _PATH.exists():
|
|
return {}
|
|
try:
|
|
data = json.loads(_PATH.read_text())
|
|
except json.JSONDecodeError as e:
|
|
fail(f"presets.json corrupt: {e}")
|
|
if not isinstance(data, dict):
|
|
fail("presets.json corrupt: expected an object")
|
|
return data
|
|
|
|
|
|
def _save(data: dict) -> None:
|
|
_PATH.parent.mkdir(parents=True, exist_ok=True)
|
|
# Backup; same pattern as cookbook state-set.
|
|
if _PATH.exists():
|
|
try:
|
|
_PATH.with_suffix(_PATH.suffix + ".bak").write_bytes(_PATH.read_bytes())
|
|
except Exception:
|
|
pass
|
|
tmp = _PATH.with_suffix(_PATH.suffix + ".tmp")
|
|
tmp.write_text(json.dumps(data, indent=2, ensure_ascii=False))
|
|
tmp.replace(_PATH)
|
|
|
|
|
|
def cmd_list(args):
|
|
presets = _load()
|
|
rows = []
|
|
for key, val in sorted(presets.items()):
|
|
if not isinstance(val, dict):
|
|
continue
|
|
rows.append({
|
|
"id": key,
|
|
"name": val.get("name") or key,
|
|
"temperature": val.get("temperature"),
|
|
"prompt_length": len(val.get("system_prompt") or ""),
|
|
})
|
|
emit(rows, args)
|
|
|
|
|
|
def cmd_get(args):
|
|
presets = _load()
|
|
if args.name not in presets:
|
|
fail(f"no preset named {args.name!r}")
|
|
emit({"id": args.name, **presets[args.name]}, args)
|
|
|
|
|
|
def cmd_set(args):
|
|
prompt = args.prompt
|
|
if args.prompt_file:
|
|
prompt = Path(args.prompt_file).read_text()
|
|
if prompt is None and args.temperature is None:
|
|
fail("nothing to set — pass --prompt, --prompt-file, or --temperature")
|
|
presets = _load()
|
|
entry = dict(presets.get(args.name) or {})
|
|
entry.setdefault("name", args.name)
|
|
if prompt is not None:
|
|
entry["system_prompt"] = prompt
|
|
if args.temperature is not None:
|
|
entry["temperature"] = args.temperature
|
|
if args.display_name:
|
|
entry["name"] = args.display_name
|
|
presets[args.name] = entry
|
|
_save(presets)
|
|
emit({"ok": True, "id": args.name, "entry": entry}, args)
|
|
|
|
|
|
def cmd_delete(args):
|
|
presets = _load()
|
|
if args.name not in presets:
|
|
fail(f"no preset named {args.name!r}")
|
|
snap = presets.pop(args.name)
|
|
_save(presets)
|
|
emit({"ok": True, "deleted": {"id": args.name, **snap}}, args)
|
|
|
|
|
|
def _build_parser():
|
|
common = argparse.ArgumentParser(add_help=False)
|
|
common.add_argument("--pretty", action="store_true")
|
|
p = argparse.ArgumentParser(prog="odysseus-preset", parents=[common])
|
|
sub = p.add_subparsers(dest="cmd", required=True)
|
|
|
|
pl = sub.add_parser("list", parents=[common])
|
|
pl.set_defaults(func=cmd_list)
|
|
|
|
pg = sub.add_parser("get", parents=[common])
|
|
pg.add_argument("name")
|
|
pg.set_defaults(func=cmd_get)
|
|
|
|
ps = sub.add_parser("set", parents=[common])
|
|
ps.add_argument("name")
|
|
ps.add_argument("--prompt", help="literal system prompt text")
|
|
ps.add_argument("--prompt-file", help="path to read prompt from")
|
|
ps.add_argument("--temperature", type=float)
|
|
ps.add_argument("--display-name")
|
|
ps.set_defaults(func=cmd_set)
|
|
|
|
pd = sub.add_parser("delete", parents=[common])
|
|
pd.add_argument("name")
|
|
pd.set_defaults(func=cmd_delete)
|
|
|
|
return p
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(run(_build_parser()))
|