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