diff --git a/CHANGELOG.md b/CHANGELOG.md index 849470e..960bf8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to Hermes Relay will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [3.3.0] - 2026-08-16 + +### Changed +- **HTTP-only response by default:** `/ask` now returns the answer only to its HTTP caller. Relay responses are no longer delivered to Telegram automatically. +- **Explicit opt-in for Telegram:** set `HERMES_RELAY_TELEGRAM_DELIVERY_ENABLED=true` only when asynchronous Telegram delivery is wanted. `telegram_queued` and `/health` now expose the active delivery state. +- **Hermes import compatibility:** auto-detect the Hermes Git-checkout layout as well as the former virtualenv site-packages layout. + ## [3.2.0] - 2026-07-20 ### Fixed diff --git a/README.md b/README.md index e62722e..6902a57 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ -# Hermes Relay v3.2 +# Hermes Relay v3.3 Snelle Quart-bridge tussen Node-RED en een warme Hermes Agent. ```text Node-RED ──POST /ask──▶ Quart relay ──serialized call──▶ warme Hermes-agent │ - ├── JSON direct terug naar Node-RED - └── Telegram-bezorging op de achtergrond via `hermes send` + └── JSON direct terug naar Node-RED + +Optioneel: Telegram-bezorging via `hermes send` (standaard uit) ``` ## Gedrag en grenzen @@ -14,8 +15,9 @@ Node-RED ──POST /ask──▶ Quart relay ──serialized call──▶ war - **Warme agent:** Hermes wordt tijdens startup geladen/geïnitialiseerd. - **Veilige concurrency:** de gedeelde, mutable agent verwerkt precies één LLM-call tegelijk. Overige HTTP-verzoeken wachten in de queue; ze kunnen niet elkaars `messages` resetten. - **Timeout:** `HERMES_RELAY_TIMEOUT` (standaard 120 s) omvat wachttijd plus LLM-call. Een timeout retourneert HTTP `504`. -- **Snelle response:** Telegram ligt niet meer in het HTTP-pad. Een succesvolle `/ask` reageert zodra het model antwoordt. -- **Background delivery:** Telegram wordt betrouwbaar gelogd. Bij gecontroleerde shutdown wacht de service op lopende bezorgingen. +- **Snelle response:** Een succesvolle `/ask` reageert zodra het model antwoordt. +- **Geen chatbezorging standaard:** relay-antwoorden blijven in de HTTP-response en verschijnen dus niet in Telegram. +- **Optionele Telegram-bezorging:** alleen met `HERMES_RELAY_TELEGRAM_DELIVERY_ENABLED=true` wordt het antwoord asynchroon via `hermes send` bezorgd. Bij een gecontroleerde shutdown wacht de service op lopende bezorgingen. ## Endpoints @@ -36,11 +38,11 @@ Succesresponse: "elapsed_seconds": 6.213, "llm_elapsed_seconds": 6.213, "request_elapsed_seconds": 6.214, - "telegram_queued": true + "telegram_queued": false } ``` -`elapsed_seconds` blijft aanwezig voor compatibiliteit en is de LLM-duur. `request_elapsed_seconds` is de feitelijke HTTP-duur. `telegram_queued` betekent dat bezorging geaccepteerd is voor de achtergrondtaak; raadpleeg journald voor het uiteindelijke bezorgresultaat. +`elapsed_seconds` blijft aanwezig voor compatibiliteit en is de LLM-duur. `request_elapsed_seconds` is de feitelijke HTTP-duur. `telegram_queued` is standaard `false`; bij expliciet ingeschakelde Telegram-bezorging betekent `true` dat de achtergrondtaak is ingepland. Raadpleeg dan journald voor het uiteindelijke bezorgresultaat. Fouten: `400` lege payload, `413` payload groter dan limiet, `504` timeout, `502` onverwachte agentfout. @@ -55,7 +57,8 @@ Geeft service-status, versie en het aantal lopende Telegram-bezorgingen terug. | Variabele | Standaard | Betekenis | |---|---:|---| | `HERMES_RELAY_TIMEOUT` | `120` | Max. wachttijd + modelcall per HTTP-request | -| `HERMES_RELAY_DELIVERY_WORKERS` | `2` | Begrensde workers voor Telegram-bezorging | +| `HERMES_RELAY_TELEGRAM_DELIVERY_ENABLED` | `false` | Zet alleen op `true` voor asynchrone Telegram-bezorging | +| `HERMES_RELAY_DELIVERY_WORKERS` | `2` | Begrensde workers voor Telegram-bezorging als die is ingeschakeld | | `HERMES_RELAY_MAX_PAYLOAD_CHARS` | `12000` | Maximale lengte van `payload` | | `HERMES_RELAY_MAX_TURNS` | `10` | Agent turn-budget | | `HERMES_RELAY_MODEL` | profieldefault | Optionele modelovertuiging | diff --git a/app.py b/app.py index 9550132..9632f76 100644 --- a/app.py +++ b/app.py @@ -19,11 +19,16 @@ if _HERMES_SITE not in sys.path: from quart import Quart, jsonify, request -VERSION = "3.2.0" +VERSION = "3.3.0" TIMEOUT = int(os.environ.get("HERMES_RELAY_TIMEOUT", "120")) TG_TARGET = os.environ.get("HERMES_RELAY_TELEGRAM_TARGET", "telegram") DELIVERY_WORKERS = int(os.environ.get("HERMES_RELAY_DELIVERY_WORKERS", "2")) MAX_PAYLOAD_CHARS = int(os.environ.get("HERMES_RELAY_MAX_PAYLOAD_CHARS", "12000")) +# HTTP replies are the default. Set this explicitly to true only when relay +# answers should additionally be delivered through Telegram. +TELEGRAM_DELIVERY_ENABLED = os.environ.get( + "HERMES_RELAY_TELEGRAM_DELIVERY_ENABLED", "false" +).strip().lower() in {"1", "true", "yes", "on"} logging.basicConfig( level=logging.INFO, @@ -114,7 +119,7 @@ def _queue_telegram(text: str) -> None: @app.route("/ask", methods=["POST"]) async def ask(): - """Answer a request; Telegram delivery is asynchronous and non-blocking.""" + """Answer a request; optional Telegram delivery stays outside the HTTP path.""" request_started = time.perf_counter() if request.is_json: @@ -161,7 +166,8 @@ async def ask(): request_elapsed = round(time.perf_counter() - request_started, 3) logger.info("Answer (llm=%.3fs, request=%.3fs): %s...", llm_elapsed, request_elapsed, answer[:200]) - _queue_telegram(answer) + if TELEGRAM_DELIVERY_ENABLED: + _queue_telegram(answer) return jsonify({ "status": "ok", "answer": answer, @@ -169,7 +175,7 @@ async def ask(): "elapsed_seconds": llm_elapsed, "llm_elapsed_seconds": llm_elapsed, "request_elapsed_seconds": request_elapsed, - "telegram_queued": True, + "telegram_queued": TELEGRAM_DELIVERY_ENABLED, }) @@ -181,6 +187,7 @@ async def health(): "version": VERSION, "mode": "async-quart", "profile": "voice-assistant", + "telegram_delivery_enabled": TELEGRAM_DELIVERY_ENABLED, "pending_telegram_deliveries": len(_delivery_tasks), } if request.args.get("detail", "false").lower() == "true": @@ -199,8 +206,8 @@ async def startup() -> None: global _hermes_lock _hermes_lock = asyncio.Lock() logger.info( - "Hermes Relay v%s startup complete (one serialized agent, delivery_workers=%d, timeout=%ds)", - VERSION, DELIVERY_WORKERS, TIMEOUT, + "Hermes Relay v%s startup complete (one serialized agent, telegram_delivery=%s, delivery_workers=%d, timeout=%ds)", + VERSION, TELEGRAM_DELIVERY_ENABLED, DELIVERY_WORKERS, TIMEOUT, ) diff --git a/hermes_client.py b/hermes_client.py index 6faec25..72c3db4 100644 --- a/hermes_client.py +++ b/hermes_client.py @@ -34,9 +34,25 @@ HERMES_MAX_TURNS = int(os.environ.get("HERMES_RELAY_MAX_TURNS", "10")) HERMES_TIMEOUT = int(os.environ.get("HERMES_RELAY_TIMEOUT", "120")) LOG_LEVEL = os.environ.get("HERMES_RELAY_LOG_LEVEL", "INFO").upper() -# Default paths for auto-detection (used when env vars not set) +# Default paths for auto-detection (used when env vars not set). Hermes v0.20 +# installs from a Git checkout, where modules live at the project root rather +# than inside the former venv site-packages directory. DEFAULT_HERMES_HOME = "/root/.hermes/profiles/voice-assistant" -DEFAULT_SITE_PACKAGES = "/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages" +DEFAULT_SITE_PACKAGES = "/usr/local/lib/hermes-agent" + + +def _resolve_hermes_import_path() -> str: + """Return the directory that contains the installed hermes_cli package.""" + configured = HERMES_SITE_PACKAGES or DEFAULT_SITE_PACKAGES + candidates = [ + configured, + "/usr/local/lib/hermes-agent", + "/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages", + ] + for candidate in candidates: + if os.path.isdir(os.path.join(candidate, "hermes_cli")): + return candidate + return configured # ── Module-level state (true warm agent) ──────────────────────────────── _HERMES_LOADED = False @@ -76,8 +92,8 @@ def _ensure_hermes_loaded() -> None: # Force voice-assistant profile via HERMES_HOME os.environ["HERMES_HOME"] = HERMES_HOME or DEFAULT_HERMES_HOME - # Ensure Hermes is in Python path - site_packages = HERMES_SITE_PACKAGES or DEFAULT_SITE_PACKAGES + # Ensure Hermes is in Python path. v0.20 uses a Git-checkout layout. + site_packages = _resolve_hermes_import_path() if site_packages not in sys.path: sys.path.insert(0, site_packages)