tools.py
tools.pyBrowse 18 files
1,963 tokens
8,107 bytes
Token encoding: o200k_base
Snapshot 24fd22b
← Back to SKILL.md
1"""Agent-facing tools for the google_meet plugin.2 3 meet_join — join a Meet URL (locally, or on a remote node via node=<name>)4 meet_status — bot liveness + transcript progress5 meet_transcript — read the transcript (optional last-N)6 meet_leave — signal the bot to leave cleanly7 meet_say — speak text through the realtime bridge (mode='realtime' only)8"""9 10from __future__ import annotations11 12import json13from typing import Any, Dict, Optional14 15from plugins.google_meet import process_manager as pm16 17 18def check_meet_requirements() -> bool:19 """True when the plugin can run LOCALLY: Linux/macOS + importable ``playwright``.20 Remote-node operation only needs ``websockets``; handlers relax this gate when a node is addressed."""21 import importlib.util22 import platform as _p23 return (_p.system().lower() in {"linux", "darwin"}24 and importlib.util.find_spec("playwright") is not None)25 26 27def resolve_node(node: str):28 """``(NodeClient, node_name)`` for *node* (``'auto'`` = the sole registered node), or ``(None, None)``."""29 from plugins.google_meet.node.registry import NodeRegistry30 from plugins.google_meet.node.client import NodeClient31 entry = NodeRegistry().resolve(node if node != "auto" else None)32 if entry is None:33 return None, None34 return NodeClient(url=entry["url"], token=entry["token"]), entry.get("name")35 36 37_NODE_PROP = {"type": "string"}38 39 40def _str(description: str) -> Dict[str, Any]:41 return {"type": "string", "description": description}42 43 44def _schema(name: str, description: str, properties: Dict[str, Any], required=None) -> Dict[str, Any]:45 params: Dict[str, Any] = {"type": "object", "properties": properties}46 if required:47 params["required"] = required48 params["additionalProperties"] = False49 return {"name": name, "description": description, "parameters": params}50 51 52MEET_JOIN_SCHEMA = _schema(53 "meet_join",54 "Join a Google Meet call and start scraping live captions into a transcript file. Only "55 "meet.google.com URLs are accepted; no calendar scanning, no auto-dial. Spawns a headless "56 "Chromium subprocess that runs in parallel with the agent loop — returns immediately. Poll "57 "with meet_status and read captions with meet_transcript. Reminder to the agent: you "58 "should announce yourself in the meeting (there is no automatic consent announcement).",59 {"url": _str("Full https://meet.google.com/... URL. Required."),60 "mode": {"type": "string", "enum": ["transcribe", "realtime"],61 "description": ("transcribe (default): listen-only, scrape captions. "62 "realtime: also enable agent speech via meet_say "63 "(requires OpenAI Realtime key + platform audio bridge).")},64 "guest_name": _str("Display name to use when joining as guest. Defaults to 'Hermes Agent'."),65 "duration": _str("Optional max duration before auto-leave (e.g. '30m', "66 "'2h', '90s'). Omit to stay until meet_leave is called."),67 "headed": {"type": "boolean",68 "description": "Run Chromium headed instead of headless (debug only). Default false."},69 "node": _str("Name of a registered remote node to run the bot on (useful when the gateway "70 "runs on a headless Linux box but the user's Chrome with a signed-in Google "71 "profile lives on their Mac). Pass 'auto' to use the single registered node. "72 "Default: run locally. Nodes are approved via `hermes meet node approve`.")},73 required=["url"])74 75MEET_STATUS_SCHEMA = _schema(76 "meet_status",77 "Report the current Meet session state — whether the bot is alive, has joined, is sitting "78 "in the lobby, number of transcript lines captured, and last-caption timestamp.",79 {"node": _NODE_PROP})80 81MEET_TRANSCRIPT_SCHEMA = _schema(82 "meet_transcript",83 "Read the scraped transcript for the active Meet session. Returns "84 "full transcript unless 'last' is set, in which case returns the last N lines only.",85 {"last": {"type": "integer",86 "description": ("Optional: return only the last N caption lines. Useful "87 "for polling during a meeting without re-reading the whole transcript."),88 "minimum": 1},89 "node": _NODE_PROP})90 91MEET_LEAVE_SCHEMA = _schema(92 "meet_leave",93 "Leave the active Meet call cleanly, stop caption scraping, and finalize the transcript "94 "file. Safe to call when no meeting is active — returns ok=false with a reason.",95 {"node": _NODE_PROP})96 97MEET_SAY_SCHEMA = _schema(98 "meet_say",99 "Speak text into the active Meet call. Requires the active meeting to have been joined "100 "with mode='realtime'. The text is queued to the bot's OpenAI Realtime session; the "101 "generated audio is streamed into Chrome's fake microphone via a virtual audio device "102 "(PulseAudio null-sink on Linux, BlackHole on macOS). Returns immediately — the actual "103 "speech lags by a couple of seconds.",104 {"text": _str("Text to speak."), "node": _NODE_PROP},105 required=["text"])106 107 108def _json(obj: Any) -> str:109 return json.dumps(obj, ensure_ascii=False)110 111 112def _err(msg: str, **extra) -> str:113 return _json({"success": False, "error": msg, **extra})114 115 116def _dispatch(node: Optional[str], op: str, remote, local) -> str:117 """Run *remote(client)* on the addressed node, else *local()*; wrap as a tool result."""118 if not node:119 res = local()120 return _json({"success": bool(res.get("ok")), **res})121 client, node_name = resolve_node(node)122 if client is None:123 return _err(f"no registered meet node matches {node!r} — "124 "run `hermes meet node approve <name> <url> <token>` first")125 try:126 res = remote(client)127 except Exception as e:128 return _err(f"remote node {op} failed: {e}", node=node_name)129 return _json({"success": bool(res.get("ok")), "node": node_name, **res})130 131 132def handle_meet_join(args: Dict[str, Any], **_kw) -> str:133 url = (args.get("url") or "").strip()134 if not url:135 return _err("url is required")136 mode = (args.get("mode") or "transcribe").strip().lower()137 if mode not in {"transcribe", "realtime"}:138 return _err(f"mode must be 'transcribe' or 'realtime' (got {mode!r})")139 common: Dict[str, Any] = dict(140 url=url, guest_name=str(args.get("guest_name") or "Hermes Agent"),141 duration=str(args.get("duration")) if args.get("duration") else None,142 headed=bool(args.get("headed", False)), mode=mode)143 144 def _local():145 if not check_meet_requirements():146 return {"ok": False, "error": (147 "google_meet plugin prerequisites missing — install with "148 "`pip install playwright && python -m playwright install "149 "chromium`. Plugin is supported on Linux and macOS only.")}150 return pm.start(**common)151 152 return _dispatch(args.get("node"), "start_bot", lambda c: c.start_bot(**common), _local)153 154 155def handle_meet_status(args: Dict[str, Any], **_kw) -> str:156 return _dispatch(args.get("node"), "status", lambda c: c.status(), pm.status)157 158 159def handle_meet_transcript(args: Dict[str, Any], **_kw) -> str:160 try:161 last = int(args["last"]) if args.get("last") is not None else None162 except (TypeError, ValueError):163 last = None164 if last is not None and last < 1:165 last = None166 return _dispatch(args.get("node"), "transcript", lambda c: c.transcript(last=last),167 lambda: pm.transcript(last=last))168 169 170def handle_meet_leave(args: Dict[str, Any], **_kw) -> str:171 return _dispatch(args.get("node"), "stop", lambda c: c.stop(),172 lambda: pm.stop(reason="agent called meet_leave"))173 174 175def handle_meet_say(args: Dict[str, Any], **_kw) -> str:176 text = (args.get("text") or "").strip()177 if not text:178 return _err("text is required")179 return _dispatch(args.get("node"), "say", lambda c: c.say(text), lambda: pm.enqueue_say(text))180