okami.mom/albedo

git clone https://git.nkp.pet/okami.mom/albedo

Aly Raffaufrefactor: remove unused internal code8e0795c

main
6.4 KiB184 linesraw
1"""This session's slash commands as typed bindings, minted from the catalog.
2
3Each model-callable command in the catalog at kernel boot becomes a typed
4async method whose docstring and signature come from its declared arguments,
5so help(commands.<method>) is the command's help text. catalog() and invoke()
6read the live catalog, so a session /reload reaches them without a kernel
7restart; a command whose method name is reserved, invalid, taken, or added
8after boot stays callable through commands.invoke().
9"""
10
11from __future__ import annotations
12
13from collections.abc import Callable, Coroutine
14from typing import Any
15import inspect
16import keyword
17from typing import TypedDict, cast
18
19from albedo_api import Host, PythonApi
20
21RESERVED = frozenset({"catalog", "invoke", "help", "__init__"})
22
23
24class CommandArgument(TypedDict):
25    name: str
26    description: str
27    required: bool
28
29
30class CommandSummary(TypedDict):
31    name: str
32    description: str
33    method: str
34    usage: str
35    arguments: list[CommandArgument]
36    modelCallable: bool
37    userTurn: bool
38
39
40def _wire_value(value: object) -> str:
41    return value if isinstance(value, str) else str(value)
42
43
44def _bind(
45    spec: list[CommandArgument], args: tuple[object, ...], kwargs: dict[str, object]
46) -> dict[str, str]:
47    declared = [argument["name"] for argument in spec]
48    if len(args) > len(declared):
49        raise TypeError(
50            f"expected at most {len(declared)} positional arguments, got {len(args)}"
51        )
52    unknown = sorted(set(kwargs) - set(declared))
53    if unknown:
54        raise TypeError(f"unexpected arguments: {', '.join(unknown)}")
55    supplied: dict[str, str] = {}
56    for name, value in zip(declared, args):
57        if value is not None:
58            supplied[name] = _wire_value(value)
59    for name, value in kwargs.items():
60        if value is not None:
61            supplied[name] = _wire_value(value)
62    return supplied
63
64
65def _docstring(summary: CommandSummary) -> str:
66    lines = [summary["description"], "", "Usage: " + summary["usage"]]
67    if summary["arguments"]:
68        lines.append("")
69        lines.append("Args:")
70        for argument in summary["arguments"]:
71            kind = "required" if argument["required"] else "optional"
72            lines.append(f"    {argument['name']}: {argument['description']} ({kind})")
73    lines.append("")
74    lines.append(
75        "Returns the command's JSON result as a Python value; never submits a turn."
76    )
77    return "\n".join(lines)
78
79
80def _signature(spec: list[CommandArgument]) -> inspect.Signature | None:
81    """A real signature when declaration order is expressible in Python."""
82    parameters: list[inspect.Parameter] = []
83    defaulted = False
84    for argument in spec:
85        name = argument["name"]
86        if not name.isidentifier() or keyword.iskeyword(name):
87            return None
88        if argument["required"]:
89            if defaulted:
90                return None
91            default: object = inspect.Parameter.empty
92        else:
93            defaulted = True
94            default = None
95        parameters.append(
96            inspect.Parameter(
97                name, inspect.Parameter.POSITIONAL_OR_KEYWORD, default=default
98            )
99        )
100    return inspect.Signature(parameters)
101
102
103def _method(
104    host: Host, summary: CommandSummary
105) -> Callable[..., Coroutine[Any, Any, object]]:
106    spec = summary["arguments"]
107
108    async def run(*args: object, **kwargs: object) -> object:
109        supplied = _bind(spec, args, kwargs)
110        return await host("commands.run", {"name": summary["name"], "args": supplied})
111
112    run.__name__ = summary["method"]
113    run.__qualname__ = "Commands." + summary["method"]
114    run.__doc__ = _docstring(summary)
115    signature = _signature(spec)
116    if signature is not None:
117        setattr(run, "__signature__", signature)
118    return run
119
120
121class Commands:
122    """This session's slash commands. Every model-callable command at kernel
123    boot is a typed async method; see the catalog for method names and
124    help(commands.<method>) for one command's help."""
125
126    def __init__(self, host: Host) -> None:
127        self._host = host
128
129    async def catalog(self) -> list[CommandSummary]:
130        """This session's current command catalog, including reloaded ones."""
131        return cast(list[CommandSummary], await self._host("commands.list", {}))
132
133    async def invoke(
134        self, name: str, arguments: str | dict[str, object] = ""
135    ) -> object:
136        """Run any model-callable command by slash name or method name.
137
138        The arguments are either the raw invocation text or a dict of declared
139        argument names to values. Returns the command's JSON result and never
140        submits a turn.
141        """
142        target = name if name.startswith("/") else "/" + name
143        for summary in await self.catalog():
144            if summary["name"] == target or summary["method"] == name:
145                break
146        else:
147            raise LookupError(
148                f"unknown command {name!r}; commands.catalog() lists them"
149            )
150        wire: dict[str, object]
151        if isinstance(arguments, str):
152            wire = {"name": summary["name"], "arguments": arguments}
153        else:
154            wire = {
155                "name": summary["name"],
156                "args": {
157                    key: _wire_value(value)
158                    for key, value in arguments.items()
159                    if value is not None
160                },
161            }
162        return await self._host("commands.run", wire)
163
164
165async def setup(api: PythonApi) -> dict[str, object]:
166    """Mint typed bindings from the boot catalog. A fast pure RPC: the route
167    answers from the session's captured command list before any turn runs.
168    The catalog's method names are unique and mintable by construction; the
169    checks below keep a damaged catalog from breaking the kernel."""
170    catalog = cast(list[CommandSummary], await api.host("commands.list", {}))
171    bindings: dict[str, object] = {}
172    for summary in catalog:
173        method = summary["method"]
174        if (
175            not summary["modelCallable"]
176            or method in RESERVED
177            or method in bindings
178            or not method.isidentifier()
179            or keyword.iskeyword(method)
180        ):
181            continue
182        bindings[method] = staticmethod(_method(api.host, summary))
183    session_commands = type("SessionCommands", (Commands,), bindings)
184    return {"commands": session_commands(api.host), "CommandsError": api.HostError}