import inspect
import json
import sys
import threading
import traceback
_out = sys.stdout _lock = threading.Lock()
VERSION = 1
init = {} settings = {}
_tools = {}
_events = {}
_messages = []
_starts = []
_views = [] _view_fns = {} _event_fns = {} _keys = {} _hooks = {} _cards = [0]
_context = {} _artifacts = {} MAX_ARTIFACT_KEYS = 16 _web_fns = {}
def _send(msg):
line = json.dumps(msg, separators=(",", ":"), default=str)
with _lock:
_out.write(line + "\n")
_out.flush()
def tool(fn=None, *, name=None):
def register(f):
_tools[name or f.__name__] = f
return f
return register(fn) if fn is not None else register
def on(event):
def register(f):
_events.setdefault(event, []).append(f)
return f
return register
def hook(event):
def register(f):
_hooks[event] = f
return f
return register
def modify(**fields):
return dict(fields, action="modify")
def stop(reason=""):
return {"action": "stop", "reason": str(reason)}
def allow():
return {"action": "allow"}
def deny(message):
return {"action": "deny", "message": str(message)}
def ask(message=""):
return {"action": "ask", "message": str(message)}
def on_message(fn):
_messages.append(fn)
return fn
def on_start(fn):
_starts.append(fn)
return fn
def on_view(fn):
_views.append(fn)
return fn
def view(view_id):
def register(f):
_view_fns[view_id] = f
return f
return register
def on_event(view_id=None):
if callable(view_id): _event_fns.setdefault(None, []).append(view_id)
return view_id
def register(f):
_event_fns.setdefault(view_id, []).append(f)
return f
return register
def _site(view_id):
for v in init.get("views") or []:
if isinstance(v, dict) and v.get("id") == view_id:
return v.get("site")
return None
class ToolError(Exception):
def context():
return dict(_context)
def artifact(key, view_id=None):
if view_id is None:
for (vid, k), a in _artifacts.items():
if k == key:
return a
return None
return _artifacts.get((view_id, key))
def save_artifact(file, chat, id=None, title=None, filename=None, note="", draft=False):
msg = {"type": "artifact", "chat": chat, "file": str(file), "note": str(note), "draft": bool(draft)}
for k, v in (("id", id), ("title", title), ("filename", filename)):
if v is not None:
msg[k] = v
_send(msg)
def _remember_key(view_id, key, params):
keys = _keys.setdefault(view_id, set())
keys.add(key)
a = params.get("artifact") if isinstance(params, dict) else None
if isinstance(a, dict):
_artifacts[(view_id, key)] = a
mine = [k for (v, k) in _artifacts if v == view_id]
while len(mine) > MAX_ARTIFACT_KEYS:
old = mine.pop(0)
_artifacts.pop((view_id, old), None)
keys.discard(old)
def _draw(view_id, key):
fn = _view_fns[view_id]
try:
takes_key = len(inspect.signature(fn).parameters) > 0
except (TypeError, ValueError):
takes_key = True
return fn(key) if takes_key else fn()
def refresh(view_id=None, values=None, client=None):
ids = [view_id] if view_id is not None else list(_view_fns)
for vid in ids:
if vid not in _view_fns:
continue
keys = set(_keys.get(vid) or ())
if _site(vid) not in ("card", "artifact"):
keys.add("")
for key in sorted(keys):
try:
tree = _draw(vid, key)
except Exception:
traceback.print_exc()
fault(f"view {vid}: drawing it failed:\n" + _short_traceback())
continue
msg = {"type": "view", "id": vid, "key": key, "view": tree}
if values:
msg["values"] = values
msg["client"] = client
_send(msg)
def card(text, view_id=None, key=None, tree=None):
if key is None:
_cards[0] += 1
key = f"card{_cards[0]}-{threading.get_ident() & 0xffff:x}"
key = str(key)
if view_id is not None:
_keys.setdefault(view_id, set()).add(key)
if tree is None and view_id in _view_fns:
tree = _draw(view_id, key)
out = {"text": str(text), "card": tree, "card_key": key}
if view_id is not None:
out["card_view"] = view_id
return out
def send(msg, client=None):
_send({"type": "notify", "msg": msg, "client": client})
def send_view(tree, id=None, key="", values=None, client=None, site=None):
msg = {"type": "view", "id": id, "key": key, "view": tree}
if site is not None:
msg["site"] = site
if values:
msg["values"] = values
msg["client"] = client
_send(msg)
def notice(text, level="info"):
_send({"type": "notice", "text": str(text), "level": level})
def log(*parts):
_send({"type": "log", "text": " ".join(str(p) for p in parts)})
def fault(text):
_send({"type": "fault", "text": str(text)})
def subscribe(*events):
_send({"type": "subscribe", "events": list(events)})
def on_web(view_id=None):
if callable(view_id):
_web_fns[None] = view_id
return view_id
def register(f):
_web_fns[view_id] = f
return f
return register
def web_send(view_id, msg, key="", client=None):
out = {"type": "web", "view": view_id, "key": key, "msg": msg}
if client is not None:
out["client"] = client
_send(out)
def _web_handler(msg):
fn = _web_fns.get(msg.get("view")) or _web_fns.get(None)
if fn is None:
raise LookupError(f"web view {msg.get('view')!r}: nothing handles its messages; add @byob.on_web({msg.get('view')!r})")
params = msg.get("msg") or {}
return fn(params.get("msg", params) if isinstance(params, dict) else params, msg)
def _short_traceback():
return "".join(traceback.format_exc().splitlines(True)[-12:]).strip()
def _call(handlers, *args):
for h in handlers:
try:
h(*args)
except Exception:
traceback.print_exc()
fault(_short_traceback())
def _request(msg):
rid = msg.get("id")
method = msg.get("method")
params = msg.get("params") or {}
try:
if method == "tool":
fn = _tools.get(params.get("name"))
if fn is None:
raise LookupError(f"this backend has no tool {params.get('name')!r} (known: {', '.join(_tools) or 'none'})")
args = params.get("args") or {}
_context.clear()
_context.update({k: params.get(k) for k in ("name", "workspace", "chat")})
try:
result = fn(**args) if isinstance(args, dict) else fn(args)
finally:
_context.clear()
elif method == "hook":
fn = _hooks.get(params.get("event"))
result = fn(params) if fn is not None else None
if result is None:
result = {"action": "continue"}
elif method == "web":
result = _web_handler(params)
elif method == "view":
vid, key = params.get("view"), params.get("key") or ""
if vid in _view_fns:
_remember_key(vid, key, params)
result = _draw(vid, key)
elif _views:
result = _views[0](params.get("site"), key, params.get("client"))
else:
raise LookupError(f"this backend draws no view {vid!r}: add @byob.view({vid!r}) to a function that returns it")
else:
raise LookupError(f"unknown request {method!r}")
_send({"type": "reply", "id": rid, "result": result})
except ToolError as e:
_send({"type": "reply", "id": rid, "error": str(e)})
except Exception:
traceback.print_exc()
_send({"type": "reply", "id": rid, "error": _short_traceback()})
if method == "tool":
refresh()
def _view_event(msg):
vid = msg.get("view")
_remember_key(vid, msg.get("key") or "", msg)
handlers = _event_fns.get(vid, []) + _event_fns.get(None, [])
if not handlers:
fault(f"view {vid}: nothing handles its events; add @byob.on_event({vid!r})")
values = None
for h in handlers:
try:
r = h(msg)
if isinstance(r, dict) and isinstance(r.get("values"), dict):
values = r["values"]
except Exception:
traceback.print_exc()
fault(f"view {vid}: the event handler failed on {msg.get('action')!r}:\n" + _short_traceback())
refresh(values=values, client=msg.get("client"))
def run(handlers=None):
for stream, errors in ((sys.stdin, "replace"), (_out, "strict"), (sys.stderr, "backslashreplace")):
try:
stream.reconfigure(encoding="utf-8", errors=errors)
except (AttributeError, ValueError, OSError):
pass
sys.stdout = sys.stderr for key, fn in (handlers or {}).items():
if key.startswith("tool:"):
_tools[key[5:]] = fn
elif key == "message":
_messages.append(fn)
elif key == "start":
_starts.append(fn)
elif key == "view":
_views.append(fn)
else:
_events.setdefault(key, []).append(fn)
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
msg = json.loads(line)
except ValueError:
continue
if not isinstance(msg, dict):
continue
kind = msg.get("type")
if kind == "init":
init.clear()
init.update(msg)
settings.clear()
settings.update(msg.get("settings") or {})
if _events:
subscribe(*_events)
_call(_starts, msg)
elif kind == "event":
if msg.get("event") == "client":
_call(_messages, msg.get("msg"), msg.get("client"))
refresh()
elif msg.get("event") == "view":
_view_event(msg)
elif msg.get("event") == "web":
try:
_web_handler(msg)
except Exception:
traceback.print_exc()
fault(_short_traceback())
refresh()
else:
_call(_events.get(msg.get("event"), []), msg)
elif kind == "request":
_request(msg)
elif kind == "shutdown":
break