feat: make Telegram relay delivery opt-in
This commit is contained in:
@@ -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/),
|
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).
|
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
|
## [3.2.0] - 2026-07-20
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
# Hermes Relay v3.2
|
# Hermes Relay v3.3
|
||||||
|
|
||||||
Snelle Quart-bridge tussen Node-RED en een warme Hermes Agent.
|
Snelle Quart-bridge tussen Node-RED en een warme Hermes Agent.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
Node-RED ──POST /ask──▶ Quart relay ──serialized call──▶ warme Hermes-agent
|
Node-RED ──POST /ask──▶ Quart relay ──serialized call──▶ warme Hermes-agent
|
||||||
│
|
│
|
||||||
├── JSON direct terug naar Node-RED
|
└── JSON direct terug naar Node-RED
|
||||||
└── Telegram-bezorging op de achtergrond via `hermes send`
|
|
||||||
|
Optioneel: Telegram-bezorging via `hermes send` (standaard uit)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Gedrag en grenzen
|
## 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.
|
- **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.
|
- **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`.
|
- **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.
|
- **Snelle response:** Een succesvolle `/ask` reageert zodra het model antwoordt.
|
||||||
- **Background delivery:** Telegram wordt betrouwbaar gelogd. Bij gecontroleerde shutdown wacht de service op lopende bezorgingen.
|
- **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
|
## Endpoints
|
||||||
|
|
||||||
@@ -36,11 +38,11 @@ Succesresponse:
|
|||||||
"elapsed_seconds": 6.213,
|
"elapsed_seconds": 6.213,
|
||||||
"llm_elapsed_seconds": 6.213,
|
"llm_elapsed_seconds": 6.213,
|
||||||
"request_elapsed_seconds": 6.214,
|
"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.
|
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 |
|
| Variabele | Standaard | Betekenis |
|
||||||
|---|---:|---|
|
|---|---:|---|
|
||||||
| `HERMES_RELAY_TIMEOUT` | `120` | Max. wachttijd + modelcall per HTTP-request |
|
| `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_PAYLOAD_CHARS` | `12000` | Maximale lengte van `payload` |
|
||||||
| `HERMES_RELAY_MAX_TURNS` | `10` | Agent turn-budget |
|
| `HERMES_RELAY_MAX_TURNS` | `10` | Agent turn-budget |
|
||||||
| `HERMES_RELAY_MODEL` | profieldefault | Optionele modelovertuiging |
|
| `HERMES_RELAY_MODEL` | profieldefault | Optionele modelovertuiging |
|
||||||
|
|||||||
@@ -19,11 +19,16 @@ if _HERMES_SITE not in sys.path:
|
|||||||
|
|
||||||
from quart import Quart, jsonify, request
|
from quart import Quart, jsonify, request
|
||||||
|
|
||||||
VERSION = "3.2.0"
|
VERSION = "3.3.0"
|
||||||
TIMEOUT = int(os.environ.get("HERMES_RELAY_TIMEOUT", "120"))
|
TIMEOUT = int(os.environ.get("HERMES_RELAY_TIMEOUT", "120"))
|
||||||
TG_TARGET = os.environ.get("HERMES_RELAY_TELEGRAM_TARGET", "telegram")
|
TG_TARGET = os.environ.get("HERMES_RELAY_TELEGRAM_TARGET", "telegram")
|
||||||
DELIVERY_WORKERS = int(os.environ.get("HERMES_RELAY_DELIVERY_WORKERS", "2"))
|
DELIVERY_WORKERS = int(os.environ.get("HERMES_RELAY_DELIVERY_WORKERS", "2"))
|
||||||
MAX_PAYLOAD_CHARS = int(os.environ.get("HERMES_RELAY_MAX_PAYLOAD_CHARS", "12000"))
|
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(
|
logging.basicConfig(
|
||||||
level=logging.INFO,
|
level=logging.INFO,
|
||||||
@@ -114,7 +119,7 @@ def _queue_telegram(text: str) -> None:
|
|||||||
|
|
||||||
@app.route("/ask", methods=["POST"])
|
@app.route("/ask", methods=["POST"])
|
||||||
async def ask():
|
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()
|
request_started = time.perf_counter()
|
||||||
|
|
||||||
if request.is_json:
|
if request.is_json:
|
||||||
@@ -161,6 +166,7 @@ async def ask():
|
|||||||
request_elapsed = round(time.perf_counter() - request_started, 3)
|
request_elapsed = round(time.perf_counter() - request_started, 3)
|
||||||
logger.info("Answer (llm=%.3fs, request=%.3fs): %s...", llm_elapsed, request_elapsed, answer[:200])
|
logger.info("Answer (llm=%.3fs, request=%.3fs): %s...", llm_elapsed, request_elapsed, answer[:200])
|
||||||
|
|
||||||
|
if TELEGRAM_DELIVERY_ENABLED:
|
||||||
_queue_telegram(answer)
|
_queue_telegram(answer)
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
@@ -169,7 +175,7 @@ async def ask():
|
|||||||
"elapsed_seconds": llm_elapsed,
|
"elapsed_seconds": llm_elapsed,
|
||||||
"llm_elapsed_seconds": llm_elapsed,
|
"llm_elapsed_seconds": llm_elapsed,
|
||||||
"request_elapsed_seconds": request_elapsed,
|
"request_elapsed_seconds": request_elapsed,
|
||||||
"telegram_queued": True,
|
"telegram_queued": TELEGRAM_DELIVERY_ENABLED,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
@@ -181,6 +187,7 @@ async def health():
|
|||||||
"version": VERSION,
|
"version": VERSION,
|
||||||
"mode": "async-quart",
|
"mode": "async-quart",
|
||||||
"profile": "voice-assistant",
|
"profile": "voice-assistant",
|
||||||
|
"telegram_delivery_enabled": TELEGRAM_DELIVERY_ENABLED,
|
||||||
"pending_telegram_deliveries": len(_delivery_tasks),
|
"pending_telegram_deliveries": len(_delivery_tasks),
|
||||||
}
|
}
|
||||||
if request.args.get("detail", "false").lower() == "true":
|
if request.args.get("detail", "false").lower() == "true":
|
||||||
@@ -199,8 +206,8 @@ async def startup() -> None:
|
|||||||
global _hermes_lock
|
global _hermes_lock
|
||||||
_hermes_lock = asyncio.Lock()
|
_hermes_lock = asyncio.Lock()
|
||||||
logger.info(
|
logger.info(
|
||||||
"Hermes Relay v%s startup complete (one serialized agent, delivery_workers=%d, timeout=%ds)",
|
"Hermes Relay v%s startup complete (one serialized agent, telegram_delivery=%s, delivery_workers=%d, timeout=%ds)",
|
||||||
VERSION, DELIVERY_WORKERS, TIMEOUT,
|
VERSION, TELEGRAM_DELIVERY_ENABLED, DELIVERY_WORKERS, TIMEOUT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+20
-4
@@ -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"))
|
HERMES_TIMEOUT = int(os.environ.get("HERMES_RELAY_TIMEOUT", "120"))
|
||||||
LOG_LEVEL = os.environ.get("HERMES_RELAY_LOG_LEVEL", "INFO").upper()
|
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_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) ────────────────────────────────
|
# ── Module-level state (true warm agent) ────────────────────────────────
|
||||||
_HERMES_LOADED = False
|
_HERMES_LOADED = False
|
||||||
@@ -76,8 +92,8 @@ def _ensure_hermes_loaded() -> None:
|
|||||||
# Force voice-assistant profile via HERMES_HOME
|
# Force voice-assistant profile via HERMES_HOME
|
||||||
os.environ["HERMES_HOME"] = HERMES_HOME or DEFAULT_HERMES_HOME
|
os.environ["HERMES_HOME"] = HERMES_HOME or DEFAULT_HERMES_HOME
|
||||||
|
|
||||||
# Ensure Hermes is in Python path
|
# Ensure Hermes is in Python path. v0.20 uses a Git-checkout layout.
|
||||||
site_packages = HERMES_SITE_PACKAGES or DEFAULT_SITE_PACKAGES
|
site_packages = _resolve_hermes_import_path()
|
||||||
if site_packages not in sys.path:
|
if site_packages not in sys.path:
|
||||||
sys.path.insert(0, site_packages)
|
sys.path.insert(0, site_packages)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user