From 1bac8db3c57a808b238425fa5a10b6881333c735 Mon Sep 17 00:00:00 2001 From: wouser Date: Wed, 3 Jun 2026 04:53:42 +0200 Subject: [PATCH] =?UTF-8?q?v1.1:=20Warme=20agent=20=E2=80=94=20directe=20P?= =?UTF-8?q?ython=20import=20i.p.v.=20subprocess?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- app.py | 114 +++++++++++++++++------------------------ hermes_client.py | 131 +++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 68 deletions(-) create mode 100644 hermes_client.py diff --git a/app.py b/app.py index 7b06b1c..1f6cc95 100644 --- a/app.py +++ b/app.py @@ -1,47 +1,53 @@ """ -Hermes Relay — Flask bridge between Node-RED and Hermes Agent. +Hermes Relay — Flask bridge tussen Node-RED en Hermes Agent (v2.0). -Node-RED sends a POST with msg.payload → this script calls `hermes chat -q` -via subprocess → returns the answer as JSON response AND sends it to the -Telegram chat via `hermes send`. +Gebruikt een warme Hermes agent (directe Python import) in plaats van +subprocess per query. Dit bespaart ~3-5 seconden startup tijd per query. + +Node-RED stuurt POST met msg.payload → Hermes (direct) → Antwoord + Telegram. Endpoints: 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 + GET /health — health check """ - -import subprocess +import os import sys 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 +# ── Config ────────────────────────────────────────────────────────────── +TIMEOUT = 120 # max seconden voor Hermes query +TG_TARGET = "telegram" # hermes send target +PROMPT_PREFIX = "/fast" # wordt aan elke prompt toegevoegd +MODEL = "qwen3.7-max" # model override (leeg = default uit config) + +# ── 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__) -# ── Config ────────────────────────────────────────────────────────────── -HERMES_BIN = "/usr/local/bin/hermes" -TIMEOUT = 120 # seconds — hermes can take a while on complex queries -TG_TARGET = "telegram" # hermes send target (home channel) - -# Prompt & Model -PROMPT_PREFIX = "/fast" # prepended to every query (empty string to disable) -MODEL = "qwen3.7-max" # model override (empty string to use default) +# ── Hermes warm houden ────────────────────────────────────────────────── +# Import de Hermes client (triggert eenmalige module laden) +HERMES_BIN = "/usr/local/bin/hermes" # nog steeds nodig voor hermes send +from hermes_client import call_hermes as _hermes_query def send_telegram(text: str) -> bool: - """Send a message via hermes send. Returns True on success.""" + """Stuur bericht via hermes send subprocess.""" + import subprocess try: result = subprocess.run( [HERMES_BIN, "send", "--to", TG_TARGET, "--quiet", text], @@ -51,45 +57,18 @@ def send_telegram(text: str) -> bool: ) return result.returncode == 0 except Exception as e: - print(f"[hermes-relay] Telegram send failed: {e}", file=sys.stderr) + logger.error(f"Telegram send failed: {e}") 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 ────────────────────────────────────────────────────────────── @app.route("/ask", methods=["POST"]) def ask(): - """Main relay endpoint. Expects JSON body with 'payload' field.""" + """Main relay endpoint.""" start = time.time() - # Accept both JSON body and form data + # Parse input if request.is_json: data = request.get_json(silent=True) or {} prompt = data.get("payload", "") @@ -99,15 +78,15 @@ def ask(): if not prompt: return jsonify({"status": "error", "message": "No payload received"}), 400 - print(f"[hermes-relay] Received: {prompt[:100]}...") + logger.info(f"Received: {prompt[:100]}...") - # Call Hermes - answer = call_hermes(prompt) + # Roep Hermes aan (direct, geen subprocess) + answer = _hermes_query(prompt) elapsed = round(time.time() - start, 1) - print(f"[hermes-relay] Answer ({elapsed}s): {answer[:200]}...") + logger.info(f"Answer ({elapsed}s): {answer[:200]}...") - # Also send to Telegram + # Stuur ook naar Telegram tg_msg = f"🤖 Hermes Relay\n\nVraag: {prompt}\n\nAntwoord: {answer}" tg_sent = send_telegram(tg_msg) @@ -121,16 +100,15 @@ def ask(): @app.route("/health", methods=["GET"]) def health(): - """Health check — use this in Node-RED to monitor relay status.""" + """Health check voor Node-RED monitoring.""" return jsonify({ "status": "ok", "service": "hermes-relay", - "hermes_bin": HERMES_BIN, - "timeout": TIMEOUT, - "prompt_prefix": PROMPT_PREFIX or "(none)", "model": MODEL or "(default)", + "prompt_prefix": PROMPT_PREFIX or "(none)", }) if __name__ == "__main__": + logger.info("Starting Hermes Relay v2.0 (warme agent)...") app.run(host="0.0.0.0", port=8650, debug=False) diff --git a/hermes_client.py b/hermes_client.py new file mode 100644 index 0000000..e28ce21 --- /dev/null +++ b/hermes_client.py @@ -0,0 +1,131 @@ +""" +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. +""" +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 + + +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() + + # 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 + from hermes_cli.config import load_config + config = load_config() + + # Resolve toolsets (zelfde als 'cli' platform) + from hermes_cli.tools_config import _get_platform_tools + toolsets = sorted(_get_platform_tools(config, 'cli')) + + # 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( + f"Hermes loaded in {elapsed:.2f}s " + f"(toolsets: {toolsets})" + ) + _HERMES_LOADED = True + + +def call_hermes(prompt: str) -> str: + """ + 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() + + # Prefix (zoals /fast) + prefix = os.environ.get('HERMES_RELAY_PREFIX', '/fast') + full_prompt = f"{prefix} {prompt}".strip() if prefix else prompt + + start = time.time() + + # ── Agent initialiseren (eenmalig, cached) ── + 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(f"Hermes error: {e}", exc_info=True) + return f"⚠️ Hermes error: {e}" + + elapsed = time.time() - start + logger.info(f"Hermes responded in {elapsed:.1f}s ({len(result)} chars)") + + return result