errands
small jobs the runtime does for the studio.
git clone https://owenrusk.dev/errands.git
commit 83f1aa0584af4df7e50080123def3131ee533a78 parent 80998294706659e50f33e515823dcdcda37924af author Owen Rusk <owen@papermothgames.com> date 2024-10-29 15:05:19 -0500
tools: a registry the runtime can call
| errands/__main__.py | +23 | -0 |
| errands/tools.py | +36 | -0 |
| tests/test_tools.py | +17 | -0 |
diff --git a/errands/__main__.py b/errands/__main__.py new file mode 100644 index 0000000..6953272 --- /dev/null +++ b/errands/__main__.py @@ -0,0 +1,23 @@ +# python -m errands serve tool calls on stdin, one json object per line +# python -m errands tools what it can call +import sys + +from . import tools + + +def main(argv: list[str]) -> int: + command = argv[1] if len(argv) > 1 else "" + if command == "serve": + for line in sys.stdin: + if line.strip(): + print(tools.call(line), flush=True) + return 0 + if command == "tools": + for name in sorted(tools.TOOLS): + print(name) + return 0 + print("usage: python -m errands serve|tools", file=sys.stderr) + return 2 + + +sys.exit(main(sys.argv)) diff --git a/errands/tools.py b/errands/tools.py new file mode 100644 index 0000000..81586d1 --- /dev/null +++ b/errands/tools.py @@ -0,0 +1,36 @@ +# what the runtime may call. each tool takes and returns plain json. +import json +from typing import Callable + +from . import mail + +TOOLS: dict[str, Callable] = {} + + +def tool(fn: Callable) -> Callable: + TOOLS[fn.__name__] = fn + return fn + + +@tool +def mail_unread() -> list[dict]: + return [vars(m) for m in mail.unread()] + + +@tool +def mail_reply(uid: str, body: str) -> str: + original = next(m for m in mail.unread() if m.uid == uid) + message_id = mail.reply(original, body) + mail.mark_read(uid) + return message_id + + +@tool +def mail_send(to: str, subject: str, body: str) -> str: + return mail.send(mail.compose(to, subject, body)) + + +def call(line: str) -> str: + request = json.loads(line) + result = TOOLS[request["tool"]](**request.get("args", {})) + return json.dumps({"ok": True, "result": result}) diff --git a/tests/test_tools.py b/tests/test_tools.py new file mode 100644 index 0000000..8dba143 --- /dev/null +++ b/tests/test_tools.py @@ -0,0 +1,17 @@ +import json +import unittest + +from errands import tools + + +class CallTest(unittest.TestCase): + def test_calls_a_registered_tool(self): + tools.TOOLS["echo"] = lambda text: text + try: + out = json.loads(tools.call(json.dumps({"tool": "echo", "args": {"text": "hi"}}))) + finally: + del tools.TOOLS["echo"] + self.assertEqual(out, {"ok": True, "result": "hi"}) + + def test_mail_tools_are_there(self): + self.assertLessEqual({"mail_unread", "mail_reply", "mail_send"}, set(tools.TOOLS))