aboutsummaryrefslogtreecommitdiffstats
path: root/assistant.py
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-23 16:40:46 +0200
committerDanilo M. <danix@danix.xyz>2026-10-23 16:40:46 +0200
commitf2bade3f67b3bd4b57f1cec30498d0664d9aac2a (patch)
tree2457ee3379cafd732b441800bea246a4dfe9bc1e /assistant.py
parent1a8ec7844d757e2f01b71ff96405f49731711169 (diff)
downloaddesktop-assistant-f2bade3f67b3bd4b57f1cec30498d0664d9aac2a.tar.gz
desktop-assistant-f2bade3f67b3bd4b57f1cec30498d0664d9aac2a.zip
config: [[cloud]] providers and the tool cloud flag
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Diffstat (limited to 'assistant.py')
-rw-r--r--assistant.py48
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')