From 5f83c410be8193465a70e7e540f93187dafe5ca1 Mon Sep 17 00:00:00 2001 From: wouser Date: Tue, 2 Jun 2026 14:00:11 +0200 Subject: [PATCH] Initial release v1.0 - Flask bridge tussen Node-RED en Hermes Agent --- .gitignore | 5 ++ README.md | 75 ++++++++++++++++++++++++++ app.py | 136 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + 4 files changed, 218 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 app.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b59e640 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +*.pyc +.env +*.log diff --git a/README.md b/README.md new file mode 100644 index 0000000..f8d34f2 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# Hermes Relay + +Flask bridge tussen Node-RED en Hermes Agent. + +## Architectuur + +``` +Node-RED (192.168.1.125) ──POST /ask──▶ Hermes Relay (192.168.1.74:8650) + │ + ├──▶ hermes chat -q (subprocess) + │ │ + │ ▼ + │ JSON response ──▶ Node-RED + │ + └──▶ hermes send --to telegram + │ + ▼ + Telegram chat +``` + +## Endpoints + +### `POST /ask` +Verwacht JSON body: +```json +{ "payload": "Wat is het weer in Best?" } +``` + +Response: +```json +{ + "status": "ok", + "answer": "Het is 22°C en zonnig in Best...", + "elapsed_seconds": 8.3, + "telegram_sent": true +} +``` + +### `GET /health` +Health check voor monitoring. + +## Setup + +```bash +cd /root/hermes-relay +python3 -m venv venv +source venv/bin/activate +pip install -r requirements.txt + +# Test +python app.py + +# Of als service +sudo systemctl enable hermes-relay +sudo systemctl start hermes-relay +``` + +## Node-RED Nodes + +### Versturen (HTTP Request node) +- **Method:** POST +- **URL:** `http://192.168.1.74:8650/ask` +- **Return:** a parsed JSON object +- **Timeout:** 120000 (ms) — Hermes kan even duren + +### Ontvangen +Het antwoord komt terug als `msg.payload`: +- `msg.payload.answer` — het Hermes antwoord +- `msg.payload.elapsed_seconds` — hoe lang het duurde +- `msg.payload.telegram_sent` — of het ook naar Telegram is gestuurd +- `msg.payload.status` — "ok" of "error" + +### Health check (optioneel) +- **Method:** GET +- **URL:** `http://192.168.1.74:8650/health` diff --git a/app.py b/app.py new file mode 100644 index 0000000..7b06b1c --- /dev/null +++ b/app.py @@ -0,0 +1,136 @@ +""" +Hermes Relay — Flask bridge between Node-RED and Hermes Agent. + +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`. + +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 +""" + +import subprocess +import sys +import time +from flask import Flask, request, jsonify + +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) + + +def send_telegram(text: str) -> bool: + """Send a message via hermes send. Returns True on success.""" + try: + result = subprocess.run( + [HERMES_BIN, "send", "--to", TG_TARGET, "--quiet", text], + capture_output=True, + text=True, + timeout=30, + ) + return result.returncode == 0 + except Exception as e: + print(f"[hermes-relay] Telegram send failed: {e}", file=sys.stderr) + 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.""" + start = time.time() + + # Accept both JSON body and form data + if request.is_json: + data = request.get_json(silent=True) or {} + prompt = data.get("payload", "") + else: + prompt = request.form.get("payload", "") + + if not prompt: + return jsonify({"status": "error", "message": "No payload received"}), 400 + + print(f"[hermes-relay] Received: {prompt[:100]}...") + + # Call Hermes + answer = call_hermes(prompt) + elapsed = round(time.time() - start, 1) + + print(f"[hermes-relay] Answer ({elapsed}s): {answer[:200]}...") + + # Also send to Telegram + tg_msg = f"🤖 Hermes Relay\n\nVraag: {prompt}\n\nAntwoord: {answer}" + tg_sent = send_telegram(tg_msg) + + return jsonify({ + "status": "ok", + "answer": answer, + "elapsed_seconds": elapsed, + "telegram_sent": tg_sent, + }) + + +@app.route("/health", methods=["GET"]) +def health(): + """Health check — use this in Node-RED to monitor relay status.""" + return jsonify({ + "status": "ok", + "service": "hermes-relay", + "hermes_bin": HERMES_BIN, + "timeout": TIMEOUT, + "prompt_prefix": PROMPT_PREFIX or "(none)", + "model": MODEL or "(default)", + }) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8650, debug=False) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..a0d407c --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +flask>=3.0 +requests>=2.31