diff options
Diffstat (limited to 'assistant.py')
| -rw-r--r-- | assistant.py | 48 |
1 files changed, 48 insertions, 0 deletions
diff --git a/assistant.py b/assistant.py index cf92c16..37ad5f2 100644 --- a/assistant.py +++ b/assistant.py @@ -58,11 +58,34 @@ DEFAULTS = { "tool_rounds": 3, "temperature": None, # None: the llama-server preset's "thinking": True, # False skips the model's reasoning + "cloud": [], # [[cloud]] providers, see check_cloud "env": {}, } OPEN = {"env"} # free-form tables: replaced whole, not merged +def check_cloud(v): + # [[cloud]] tables. A provider's name prefixes its models' ids + # ("name:model"), so it is unique and holds no colon. models may be + # empty: the settings page saves a provider before fetching its list. + if not isinstance(v, list) or not all(isinstance(p, dict) for p in v): + raise SystemExit("config: cloud must be [[cloud]] tables") + seen = set() + for p in v: + name = p.get("name") + if set(p) != {"name", "url", "key_cmd", "models"}: + raise SystemExit(f"config: cloud {name!r} needs exactly name, url, key_cmd, models") + if (not all(isinstance(p[k], str) and p[k] for k in ("name", "url", "key_cmd")) + or not isinstance(p["models"], list) + or not all(isinstance(m, str) for m in p["models"])): + raise SystemExit(f"config: cloud {name!r}: name, url and key_cmd are strings, " + "models a list of strings") + if ":" in name or name in seen: + raise SystemExit(f"config: cloud name {name!r} is repeated or has a colon") + seen.add(name) + return v + + def merge(base, over, where=""): # A typo must fail loudly, not fall back to the default in silence. out = dict(base) @@ -73,6 +96,8 @@ def merge(base, over, where=""): if not isinstance(v, dict): raise SystemExit(f"config: {where}{k} must be a table") v = merge(base[k], v, f"{where}{k}.") + if k == "cloud": + v = check_cloud(v) out[k] = v return out @@ -92,6 +117,8 @@ def load_tools(d): t = tomllib.load(f) if not isinstance(t.get("description"), str) or not t.get("command"): raise SystemExit(f"{path}: needs description and command") + if not isinstance(t.get("cloud", False), bool): + raise SystemExit(f"{path}: cloud must be true or false") tools[os.path.basename(path)[:-5]] = t return tools @@ -1104,6 +1131,27 @@ def test(): assert False, bad except SystemExit as e: assert "config:" in str(e) + good = {"name": "or", "url": "https://llm.example.org/v1", "key_cmd": "echo k", + "models": ["a/b"]} + assert merge(DEFAULTS, {"cloud": [good]})["cloud"] == [good] + assert merge(DEFAULTS, {"cloud": [{**good, "models": []}]}) # before the first fetch + assert DEFAULTS["cloud"] == [] + for bad in ({"cloud": good}, {"cloud": [{**good, "extra": 1}]}, + {"cloud": [{**good, "name": "a:b"}]}, {"cloud": [good, good]}, + {"cloud": [{**good, "models": "a/b"}]}, {"cloud": [{**good, "url": ""}]}): + try: + merge(DEFAULTS, bad) + assert False, bad + except SystemExit as e: + assert "config: cloud" in str(e), (bad, e) + with tempfile.TemporaryDirectory() as d: + with open(os.path.join(d, "x.toml"), "w") as f: + f.write('description = "d"\ncommand = ["date"]\ncloud = "yes"\n') + try: + load_tools(d) + assert False + except SystemExit as e: + assert "cloud must be true or false" in str(e) with tempfile.TemporaryDirectory() as d: with open(os.path.join(d, "echo.sh"), "w") as f: f.write('#!/bin/sh\nprintf "%s|" "$@"\n') |
