#!/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 _entry_or_fail(presets: dict, name: str) -> dict: if name not in presets: fail(f"no preset named {name!r}") entry = presets[name] if not isinstance(entry, dict): fail(f"preset {name!r} is corrupt: expected an object") return entry 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() emit({"id": args.name, **_entry_or_fail(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() current = presets.get(args.name) entry = dict(current) if isinstance(current, dict) else {} 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() snap = _entry_or_fail(presets, args.name) 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()))