owenrusk.dev

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))