Merge branch 'simp/r3-26-H' into simp/integration3
This commit is contained in:
@@ -40,9 +40,7 @@ def _load_engine_from_dir(engine_dir: Path) -> Optional["ContextEngine"]: # noq
|
||||
mod = _loader.load_plugin_module(
|
||||
f"plugins.context_engine.{name}", engine_dir,
|
||||
parents=("plugins", "plugins.context_engine"), logger=logger)
|
||||
if mod is None:
|
||||
return None
|
||||
return _loader.instance_from_module(
|
||||
return mod and _loader.instance_from_module(
|
||||
mod, collector=_EngineCollector(engine_name=name), collected_attr="engine",
|
||||
base_cls=ContextEngine, name=name, logger=logger)
|
||||
|
||||
|
||||
@@ -53,9 +53,8 @@ def find_provider_dir(name: str) -> Optional[Path]:
|
||||
if bundled.is_dir() and (bundled / "__init__.py").exists():
|
||||
return bundled
|
||||
user_dir = _loader.user_plugins_dir()
|
||||
if user_dir and (user_dir / name).is_dir() and _is_cron_provider_dir(user_dir / name):
|
||||
return user_dir / name
|
||||
return None
|
||||
user = user_dir / name if user_dir else None
|
||||
return user if user and user.is_dir() and _is_cron_provider_dir(user) else None
|
||||
|
||||
|
||||
def discover_cron_schedulers() -> List[Tuple[str, str, bool]]:
|
||||
@@ -85,9 +84,7 @@ def _load_provider_from_dir(provider_dir: Path) -> Optional["CronScheduler"]: #
|
||||
mod = _loader.load_plugin_module(
|
||||
module_name, provider_dir, parents=("plugins", "plugins.cron_providers"), logger=logger,
|
||||
synthetic_namespace=None if is_bundled else _USER_NAMESPACE)
|
||||
if mod is None:
|
||||
return None
|
||||
return _loader.instance_from_module(
|
||||
return mod and _loader.instance_from_module(
|
||||
mod, collector=_ProviderCollector(), collected_attr="provider",
|
||||
base_cls=CronScheduler, name=name, logger=logger)
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
import shlex
|
||||
@@ -38,10 +39,8 @@ def _extract_paths_from_terminal(args: Dict[str, Any], result: str) -> Set[str]:
|
||||
paths: Set[str] = set()
|
||||
cmd = args.get("command") or ""
|
||||
if isinstance(cmd, str) and cmd:
|
||||
try: # tokenise — catches `touch /tmp/hermes-x/test_foo.py`
|
||||
with contextlib.suppress(ValueError): # tokenise — catches `touch /tmp/hermes-x/test_foo.py`
|
||||
paths.update(tok for tok in shlex.split(cmd, posix=True) if tok.startswith(("/", "~")))
|
||||
except ValueError:
|
||||
pass
|
||||
# Only scan the result text if it's a reasonable size (avoid 50KB dumps).
|
||||
if isinstance(result, str) and len(result) < 4096:
|
||||
paths.update(_TERMINAL_PATH_REGEX.findall(result))
|
||||
|
||||
@@ -8,6 +8,7 @@ Scope: strictly HERMES_HOME and /tmp/hermes-*; never ~/.hermes/logs/ or system d
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import logging
|
||||
import shutil
|
||||
@@ -37,11 +38,9 @@ def _state_file(name: str) -> Path:
|
||||
|
||||
def is_safe_path(path: Path) -> bool:
|
||||
"""Accept only paths under HERMES_HOME or ``/tmp/hermes-*`` (rejects /mnt/c etc.)."""
|
||||
try:
|
||||
with contextlib.suppress(ValueError, OSError):
|
||||
path.resolve().relative_to(get_hermes_home())
|
||||
return True
|
||||
except (ValueError, OSError):
|
||||
pass
|
||||
parts = path.parts
|
||||
return len(parts) >= 3 and parts[1] == "tmp" and parts[2].startswith("hermes-")
|
||||
|
||||
@@ -284,13 +283,11 @@ def _sweep_empty_dirs(hermes_home: Path) -> int:
|
||||
while stack:
|
||||
dirpath, visited = stack.pop()
|
||||
if visited:
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
if not any(dirpath.iterdir()):
|
||||
dirpath.rmdir()
|
||||
removed += 1
|
||||
_log(f"DELETED: {dirpath} (empty dir)")
|
||||
except OSError:
|
||||
pass
|
||||
continue
|
||||
stack.append((dirpath, True))
|
||||
stack.extend((child, False) for child in _subdirs(dirpath, _EMPTY_DIR_SWEEP_PRUNE_DIRS))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
@@ -24,8 +25,6 @@ def write_json_atomic(path: Path, data: Any, mode: Optional[int] = None) -> None
|
||||
tmp = path.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
||||
if mode is not None:
|
||||
try:
|
||||
with contextlib.suppress(OSError, NotImplementedError): # best-effort on non-POSIX filesystems
|
||||
tmp.chmod(mode)
|
||||
except (OSError, NotImplementedError): # best-effort on non-POSIX filesystems
|
||||
pass
|
||||
tmp.replace(path)
|
||||
|
||||
@@ -7,6 +7,7 @@ Linux: pactl creates a null-sink plus a virtual source on the sink's monitor; ca
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import platform
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
@@ -29,7 +30,6 @@ class AudioBridge:
|
||||
self._device_name: Optional[str] = None
|
||||
self._write_target: Optional[str] = None
|
||||
self._module_ids: list[int] = []
|
||||
self._torn_down = False
|
||||
|
||||
def _ready(self, value: Optional[str]) -> str:
|
||||
if not value:
|
||||
@@ -46,29 +46,21 @@ class AudioBridge:
|
||||
return self._setup_linux()
|
||||
if system == "Darwin":
|
||||
return self._setup_darwin()
|
||||
if system == "Windows":
|
||||
raise RuntimeError("windows not supported in v2")
|
||||
raise RuntimeError(f"unsupported platform: {system}")
|
||||
raise RuntimeError("windows not supported in v2" if system == "Windows"
|
||||
else f"unsupported platform: {system}")
|
||||
|
||||
def teardown(self) -> None:
|
||||
"""Release the virtual audio device. Idempotent; never raises."""
|
||||
if self._torn_down:
|
||||
return
|
||||
if self._platform == "linux":
|
||||
for mod_id in reversed(self._module_ids): # virtual-source before null-sink
|
||||
try:
|
||||
_pactl("unload-module", str(mod_id), check=False)
|
||||
except Exception:
|
||||
pass
|
||||
self._module_ids = []
|
||||
self._torn_down = True
|
||||
for mod_id in reversed(self._module_ids): # linux only; virtual-source before null-sink
|
||||
with contextlib.suppress(Exception):
|
||||
_pactl("unload-module", str(mod_id), check=False)
|
||||
self._module_ids = []
|
||||
|
||||
def _finish(self, tag: str, device: str, write_target: str, module_ids: list[int]) -> dict:
|
||||
self._platform = tag
|
||||
self._device_name = device
|
||||
self._write_target = write_target
|
||||
self._module_ids = module_ids
|
||||
self._torn_down = False
|
||||
return {"platform": tag, "device_name": device, "sample_rate": 48000, "channels": 2,
|
||||
"module_ids": list(module_ids), "write_target": write_target}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import json
|
||||
import platform
|
||||
@@ -213,17 +214,16 @@ def _cmd_auth() -> int:
|
||||
return 1
|
||||
path = _auth_state_path()
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
print(f"opening Chromium — sign in to Google, then return here and press Enter.\nsaving storage state to: {path}")
|
||||
print("opening Chromium — sign in to Google, then return here and press Enter.\n"
|
||||
f"saving storage state to: {path}")
|
||||
try:
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(headless=False)
|
||||
context = browser.new_context()
|
||||
page = context.new_page()
|
||||
page.goto("https://accounts.google.com/", wait_until="domcontentloaded")
|
||||
try:
|
||||
with contextlib.suppress(EOFError):
|
||||
input("press Enter after you've signed in ... ")
|
||||
except EOFError:
|
||||
pass
|
||||
context.storage_state(path=str(path))
|
||||
browser.close()
|
||||
except Exception as e:
|
||||
|
||||
@@ -72,18 +72,12 @@ def validate_request(msg: Dict[str, Any], expected_token: str) -> Tuple[bool, st
|
||||
"""Return ``(True, "")`` or ``(False, <reason>)``; reasons are safe to send back to the client."""
|
||||
if not isinstance(msg, dict):
|
||||
return False, "envelope must be a dict"
|
||||
t = msg.get("type")
|
||||
if not _nonempty_str(t):
|
||||
return False, "missing or non-string 'type'"
|
||||
if t not in VALID_REQUEST_TYPES:
|
||||
return False, f"unknown request type: {t!r}"
|
||||
if not _nonempty_str(msg.get("id")):
|
||||
return False, "missing or non-string 'id'"
|
||||
token = msg.get("token")
|
||||
if not _nonempty_str(token):
|
||||
return False, "missing token"
|
||||
if token != expected_token:
|
||||
return False, "token mismatch"
|
||||
if not isinstance(msg.get("payload"), dict):
|
||||
return False, "payload must be a dict"
|
||||
return True, ""
|
||||
t, token = msg.get("type"), msg.get("token")
|
||||
checks = ( # ordered, lazily evaluated: first failing check wins
|
||||
(lambda: _nonempty_str(t), "missing or non-string 'type'"),
|
||||
(lambda: t in VALID_REQUEST_TYPES, f"unknown request type: {t!r}"),
|
||||
(lambda: _nonempty_str(msg.get("id")), "missing or non-string 'id'"),
|
||||
(lambda: _nonempty_str(token), "missing token"),
|
||||
(lambda: token == expected_token, "token mismatch"),
|
||||
(lambda: isinstance(msg.get("payload"), dict), "payload must be a dict"))
|
||||
return next(((False, reason) for ok, reason in checks if not ok()), (True, ""))
|
||||
|
||||
@@ -8,6 +8,7 @@ across turns can find the bot. The bot is a detached subprocess reached via file
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import signal
|
||||
@@ -47,10 +48,8 @@ def _active_pid() -> int:
|
||||
|
||||
|
||||
def _kill(pid: int, sig) -> None:
|
||||
try:
|
||||
with contextlib.suppress(ProcessLookupError):
|
||||
os.kill(pid, sig)
|
||||
except ProcessLookupError:
|
||||
pass
|
||||
|
||||
|
||||
_NO_ACTIVE = {"ok": False, "reason": "no active meeting"}
|
||||
@@ -72,10 +71,8 @@ def start(url: str, *, out_dir: Optional[Path] = None, headed: bool = False,
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
# Wipe stale files from a previous run of this meeting id.
|
||||
for name in ("transcript.txt", "status.json"):
|
||||
try:
|
||||
with contextlib.suppress(OSError):
|
||||
(out / name).unlink()
|
||||
except OSError:
|
||||
pass
|
||||
env = {**os.environ, "HERMES_MEET_URL": url, "HERMES_MEET_OUT_DIR": str(out),
|
||||
"HERMES_MEET_GUEST_NAME": guest_name}
|
||||
for value, var in (
|
||||
@@ -149,7 +146,8 @@ def enqueue_say(text: str) -> Dict[str, Any]:
|
||||
entry = {"id": uuid.uuid4().hex[:12], "text": text}
|
||||
with queue_path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(entry) + "\n")
|
||||
return {"ok": True, "meetingId": active.get("meeting_id"), "enqueued_id": entry["id"], "queue_path": str(queue_path)}
|
||||
return {"ok": True, "meetingId": active.get("meeting_id"), "enqueued_id": entry["id"],
|
||||
"queue_path": str(queue_path)}
|
||||
|
||||
|
||||
def stop(*, reason: str = "requested") -> Dict[str, Any]:
|
||||
|
||||
@@ -7,6 +7,7 @@ Chrome's fake mic. One sync WebSocket per session; ``websockets`` is imported la
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import contextlib
|
||||
import json
|
||||
import threading
|
||||
import time
|
||||
@@ -69,10 +70,8 @@ class RealtimeSession:
|
||||
|
||||
def close(self) -> None:
|
||||
if self._ws is not None:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
self._ws.close()
|
||||
except Exception:
|
||||
pass
|
||||
self._ws = None
|
||||
|
||||
def speak(self, text: str, timeout: float = 30.0) -> dict:
|
||||
@@ -87,18 +86,16 @@ class RealtimeSession:
|
||||
})
|
||||
self._send_json({"type": "response.create", "response": {"modalities": ["audio"]}})
|
||||
bytes_written = 0
|
||||
sink_fp = None
|
||||
if self.audio_sink_path is not None:
|
||||
self.audio_sink_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sink_fp = open(self.audio_sink_path, "ab")
|
||||
try:
|
||||
with contextlib.ExitStack() as stack:
|
||||
sink_fp = None
|
||||
if self.audio_sink_path is not None:
|
||||
self.audio_sink_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
sink_fp = stack.enter_context(open(self.audio_sink_path, "ab"))
|
||||
while True:
|
||||
frame = self._recv_frame(start + timeout, timeout)
|
||||
if frame is None: # connection closed by peer
|
||||
if frame is None or frame.get("type") in _TERMINAL_FRAMES: # peer closed / response done
|
||||
break
|
||||
ftype = frame.get("type")
|
||||
if ftype in _TERMINAL_FRAMES:
|
||||
break
|
||||
if ftype == "error":
|
||||
raise RuntimeError(f"realtime error: {frame.get('error') or frame}")
|
||||
if ftype == "response.audio.delta" and sink_fp is not None:
|
||||
@@ -109,9 +106,6 @@ class RealtimeSession:
|
||||
bytes_written += len(chunk)
|
||||
self.audio_bytes_out += len(chunk)
|
||||
self.last_audio_out_at = time.time()
|
||||
finally:
|
||||
if sink_fp is not None:
|
||||
sink_fp.close()
|
||||
return {"ok": True, "bytes_written": bytes_written, "duration_ms": (time.monotonic() - start) * 1000.0}
|
||||
|
||||
def cancel_response(self) -> bool:
|
||||
|
||||
@@ -5,6 +5,7 @@ or an ABC-subclass fallback."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
import importlib.machinery
|
||||
import importlib.util
|
||||
import logging
|
||||
@@ -150,10 +151,8 @@ def instance_from_module(mod: Any, *, collector: Any, collected_attr: str, base_
|
||||
for attr_name in dir(mod):
|
||||
attr = getattr(mod, attr_name, None)
|
||||
if isinstance(attr, type) and issubclass(attr, base_cls) and attr is not base_cls:
|
||||
try:
|
||||
with contextlib.suppress(Exception):
|
||||
return attr()
|
||||
except Exception:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user