5 Commits

Author SHA1 Message Date
wouser 58ca990673 v2.1.1: Alleen antwoord naar Telegram sturen (geen Vraag:/Antwoord: prefix) 2026-06-16 14:04:59 +02:00
wouser e05fa9c5c1 v2.1: Switch to voice-assistant profile
- Use voice-assistant profile (minimal chatbot, max_turns=10, nous provider)
- Remove /fast prefix and model override - handled by profile config
- Use messaging toolset for send_message tool (Telegram delivery)
- Add hardcoded TELEGRAM_BOT_TOKEN and TELEGRAM_HOME_CHANNEL to relay env
- Telegram delivery now works via gateway's send_message_tool
- Version bump to 2.1
- Health endpoint shows profile: voice-assistant
2026-06-16 09:37:48 +02:00
wouser 1780eed406 feat: relay met qwen3.7-plus voor snellere responses
- Model override: qwen3.7-plus (4.5x meer requests/uur dan max)
- Toolsets disabled bij model override (DeepSeek compat fix)
- NoneType crash fix voor error paths
- Originele qwen3.7-max blijft actief voor andere chats
2026-06-08 19:48:31 +02:00
wouser 75311845ee chore: relay v2 cleanup — remove f-strings, add version/mode to health endpoint 2026-06-08 19:00:10 +02:00
wouser 1bac8db3c5 v1.1: Warme agent — directe Python import i.p.v. subprocess
- Vervang subprocess calls naar hermes chat -q door directe Python import
- Nieuwe hermes_client.py met warme AIAgent (eenmalig init, hergebruik per query)
- Initializing agent overhead: 3-5s per query -> 0.48s eenmalig
- Queries na eerste zijn ~5s (alleen LLM API tijd)
- Auxiliary vision provider leeggezet (geen DNS lookup naar Nous)
2026-06-03 04:53:42 +02:00
2 changed files with 226 additions and 80 deletions
+82 -80
View File
@@ -1,95 +1,98 @@
""" """
Hermes Relay — Flask bridge between Node-RED and Hermes Agent. Hermes Relay v2 — Flask bridge tussen Node-RED en Hermes Agent.
Node-RED sends a POST with msg.payload → this script calls `hermes chat -q` Gebruikt de voice-assistant profiel (minimale chatbot, max_turns=10).
via subprocess → returns the answer as JSON response AND sends it to the Geen prefix/model override nodig — profiel handelt dit af.
Telegram chat via `hermes send`.
Endpoints: Node-RED stuurt POST met msg.payload → Hermes (direct) → Alleen antwoord terug.
POST /ask — main relay endpoint
GET /health — health check for Node-RED monitoring
Architecture:
Node-RED (192.168.1.125) ──POST──▶ Hermes Relay (192.168.1.74:8650)
│
├──▶ hermes chat -q (subprocess)
│ │
│ ▼
│ JSON response ──▶ Node-RED
│
└──▶ hermes send --to telegram
│
▼
Telegram chat
""" """
import subprocess import os
import sys import sys
import time import time
import logging
# ── Hermes path toevoegen (zodat we Hermes direct kunnen importeren) ───
_HERMES_SITE = "/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages"
if _HERMES_SITE not in sys.path:
sys.path.insert(0, _HERMES_SITE)
from flask import Flask, request, jsonify from flask import Flask, request, jsonify
# ── Config ──────────────────────────────────────────────────────────────
TIMEOUT = 120 # max seconden voor Hermes query
TG_TARGET = "telegram" # hermes send target
# ── Logging ─────────────────────────────────────────────────────────────
logging.basicConfig(
level=logging.INFO,
format="[hermes-relay] %(asctime)s %(levelname)s %(message)s",
stream=sys.stderr,
)
logger = logging.getLogger(__name__)
# ── Flask app ───────────────────────────────────────────────────────────
app = Flask(__name__) app = Flask(__name__)
# ── Config ────────────────────────────────────────────────────────────── # ── Hermes warm houden ──────────────────────────────────────────────────
HERMES_BIN = "/usr/local/bin/hermes" from hermes_client import call_hermes as _hermes_query
TIMEOUT = 120 # seconds — hermes can take a while on complex queries
TG_TARGET = "telegram" # hermes send target (home channel)
# Prompt & Model # Laad gateway config eenmalig voor Telegram verzenden
PROMPT_PREFIX = "/fast" # prepended to every query (empty string to disable) _gw_config_loaded = False
MODEL = "qwen3.7-max" # model override (empty string to use default) _gw_config = None
def _ensure_gw_config():
"""Laad gateway config eenmalig."""
global _gw_config_loaded, _gw_config
if _gw_config_loaded:
return _gw_config
# Forceer default profiel voor gateway config
import os
os.environ["HERMES_HOME"] = "/root/.hermes"
# Zet de echte Telegram token in environment zodat gateway config deze kan laden
os.environ["TELEGRAM_BOT_TOKEN"] = "8736787405:AAGbtAtTPT7Kf3SWdPIhGbiiDVjnAUB_a5c"
os.environ["TELEGRAM_HOME_CHANNEL"] = "5453608010"
from gateway.config import load_gateway_config
_gw_config = load_gateway_config()
_gw_config_loaded = True
return _gw_config
def send_telegram(text: str) -> bool: def send_telegram(text):
"""Send a message via hermes send. Returns True on success.""" """Stuur bericht via gateway's send_message_tool (gebruikt gateway config met echte token)."""
try: try:
result = subprocess.run( logger.info("Telegram send: starting...")
[HERMES_BIN, "send", "--to", TG_TARGET, "--quiet", text], _ensure_gw_config()
capture_output=True, logger.info("Telegram send: gw config loaded")
text=True, from tools.send_message_tool import send_message_tool
timeout=30, import json
)
return result.returncode == 0 # Stuur via home channel (telegram = default target)
result_json = send_message_tool({
"action": "send",
"target": "telegram",
"message": text,
})
logger.info("Telegram send: send_message_tool returned")
result = json.loads(result_json)
if result.get("error"):
logger.error("Telegram send failed: %s", result["error"])
return False
logger.info("Telegram send: success=%s", result.get("success", False))
return result.get("success", False)
except Exception as e: except Exception as e:
print(f"[hermes-relay] Telegram send failed: {e}", file=sys.stderr) logger.error("Telegram send failed with exception: %s", e, exc_info=True)
return False return False
def call_hermes(prompt: str) -> str:
"""Call hermes chat -q and return stdout."""
# Build full prompt with prefix
full_prompt = f"{PROMPT_PREFIX} {prompt}".strip() if PROMPT_PREFIX else prompt
cmd = [HERMES_BIN, "chat", "-q", full_prompt, "-Q", "--yolo"]
# Add model override if configured
if MODEL:
cmd.extend(["-m", MODEL])
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=TIMEOUT,
)
if result.returncode != 0:
return f"⚠️ Hermes exited with code {result.returncode}\nstderr: {result.stderr.strip()}"
return result.stdout.strip()
except subprocess.TimeoutExpired:
return f"⚠️ Timeout — Hermes took longer than {TIMEOUT}s"
except Exception as e:
return f"⚠️ Error: {e}"
# ── Routes ────────────────────────────────────────────────────────────── # ── Routes ──────────────────────────────────────────────────────────────
@app.route("/ask", methods=["POST"]) @app.route("/ask", methods=["POST"])
def ask(): def ask():
"""Main relay endpoint. Expects JSON body with 'payload' field.""" """Main relay endpoint — retourneert alleen het antwoord."""
start = time.time() start = time.time()
# Accept both JSON body and form data # Parse input
if request.is_json: if request.is_json:
data = request.get_json(silent=True) or {} data = request.get_json(silent=True) or {}
prompt = data.get("payload", "") prompt = data.get("payload", "")
@@ -99,17 +102,16 @@ def ask():
if not prompt: if not prompt:
return jsonify({"status": "error", "message": "No payload received"}), 400 return jsonify({"status": "error", "message": "No payload received"}), 400
print(f"[hermes-relay] Received: {prompt[:100]}...") logger.info("Received: %s...", prompt[:100])
# Call Hermes # Roep Hermes aan (direct, geen subprocess)
answer = call_hermes(prompt) answer = _hermes_query(prompt)
elapsed = round(time.time() - start, 1) elapsed = round(time.time() - start, 1)
print(f"[hermes-relay] Answer ({elapsed}s): {answer[:200]}...") logger.info("Answer (%.1fs): %s...", elapsed, answer[:200])
# Also send to Telegram # Stuur ook naar Telegram (alleen het antwoord)
tg_msg = f"🤖 Hermes Relay\n\nVraag: {prompt}\n\nAntwoord: {answer}" tg_sent = send_telegram(answer)
tg_sent = send_telegram(tg_msg)
return jsonify({ return jsonify({
"status": "ok", "status": "ok",
@@ -121,16 +123,16 @@ def ask():
@app.route("/health", methods=["GET"]) @app.route("/health", methods=["GET"])
def health(): def health():
"""Health check — use this in Node-RED to monitor relay status.""" """Health check voor Node-RED monitoring."""
return jsonify({ return jsonify({
"status": "ok", "status": "ok",
"service": "hermes-relay", "service": "hermes-relay",
"hermes_bin": HERMES_BIN, "version": "2.1",
"timeout": TIMEOUT, "mode": "python-import",
"prompt_prefix": PROMPT_PREFIX or "(none)", "profile": "voice-assistant",
"model": MODEL or "(default)",
}) })
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", port=8650, debug=False) logger.info("Starting Hermes Relay v2.1 (voice-assistant profiel)...")
app.run(host="0.0.0.0", port=8650, debug=False)
+144
View File
@@ -0,0 +1,144 @@
"""
Hermes Client — directe Python import (geen subprocess overhead).
Warme agent: eenmalig initialiseren, daarna hergebruiken voor elke query.
Dit bespaart de subprocess + Python startup overhead van 2-5 seconden per query.
Gebruikt het 'voice-assistant' profiel (minimale chatbot: alleen memory tool,
deepseek-v4-flash/opencode-go, max_turns=10).
"""
import io
import os
import sys
import time
import logging
from contextlib import redirect_stdout, redirect_stderr
logger = logging.getLogger(__name__)
# ── Eenmalige Hermes imports (bij module laden) ──────────────────────
_HERMES_LOADED = False
_cli = None
# Voice-assistant profiel home directory
_HERMES_HOME = "/root/.hermes/profiles/voice-assistant"
def _ensure_hermes_loaded():
"""Laad Hermes eenmalig bij eerste gebruik — dit is het zware werk."""
global _HERMES_LOADED, _cli
if _HERMES_LOADED:
return
start = time.time()
# Forceer voice-assistant profiel via HERMES_HOME
os.environ["HERMES_HOME"] = _HERMES_HOME
# Zorg dat Hermes in de Python path zit
hermes_path = "/usr/local/lib/hermes-agent/venv/lib/python3.11/site-packages"
if hermes_path not in sys.path:
sys.path.insert(0, hermes_path)
# Force UTF-8
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# Laad .env via Hermes' eigen loader
from hermes_cli.env_loader import load_hermes_dotenv
from hermes_constants import get_hermes_home
_hermes_home = get_hermes_home()
_project_env = os.path.join(os.path.dirname(hermes_path), ".env")
load_hermes_dotenv(hermes_home=_hermes_home, project_env=_project_env)
# Load config (nu vanuit voice-assistant profiel)
from hermes_cli.config import load_config
config = load_config()
# Gebruik messaging toolset zodat agent send_message tool kan gebruiken
toolsets = ["messaging"]
# YOLO mode + interactive mode
os.environ["HERMES_YOLO_MODE"] = "1"
os.environ["HERMES_INTERACTIVE"] = "1"
os.environ["HERMES_SESSION_SOURCE"] = "relay"
# Import HermesCLI
from cli import HermesCLI
# Maak CLI instantie (nog géén agent init — dat gebeurt pas bij eerste query)
_cli = HermesCLI(
model=config.get("model", {}).get("default", ""),
toolsets=toolsets,
)
# Onderdruk banner/status output
_cli.tool_progress_mode = "off"
elapsed = time.time() - start
logger.info(
"Hermes loaded in %.2fs (toolsets: %s, profile: voice-assistant)",
elapsed, toolsets
)
_HERMES_LOADED = True
def call_hermes(prompt):
"""
Roep Hermes aan — hergebruikt een warme agent.
De agent wordt één keer geïnitialiseerd (bij eerste query) en daarna
hergebruikt. Na elke query wordt de conversation history gereset,
zodat elke query een frisse start krijgt.
"""
_ensure_hermes_loaded()
# Geen prefix meer — voice-assistant profiel handelt dit af via config
full_prompt = prompt.strip()
start = time.time()
# ── Agent initialiseren ──
turn_route = _cli._resolve_turn_agent_config(full_prompt)
if turn_route["signature"] != _cli._active_agent_route_signature:
_cli.agent = None
init_ok = _cli._init_agent(
model_override=turn_route["model"],
runtime_override=turn_route["runtime"],
request_overrides=turn_route.get("request_overrides"),
)
if not init_ok:
return "⚠️ Kon Hermes agent niet initialiseren."
# Stil modus
_cli.agent.quiet_mode = True
_cli.agent.suppress_status_output = True
_cli.agent.stream_delta_callback = None
_cli.agent.tool_gen_callback = None
# ── Query uitvoeren ──
# Capture alles wat naar stdout/stderr gaat (session_id, banner, etc.)
stdout_capture = io.StringIO()
stderr_capture = io.StringIO()
try:
with redirect_stdout(stdout_capture), redirect_stderr(stderr_capture):
# conversation_history wissen voor frisse start
_cli.agent.messages = []
result = _cli.agent.chat(full_prompt)
except SystemExit:
# Hermes roept sys.exit() aan in sommige error paths
logger.warning("Hermes riep sys.exit() aan tijdens query")
result = stdout_capture.getvalue().strip()
except Exception as e:
logger.error("Hermes error: %s", e, exc_info=True)
return "⚠️ Hermes error: " + str(e)
elapsed = time.time() - start
# Fallback als Hermes None teruggeeft (error path)
if result is None:
logger.warning("Hermes returned None — possible error during query")
return "⚠️ Geen antwoord van Hermes. Probeer opnieuw."
logger.info("Hermes responded in %.1fs (%d chars)", elapsed, len(result))
return result