Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1bac8db3c5 |
@@ -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`
|
Gebruikt een warme Hermes agent (directe Python import) in plaats van
|
||||||
via subprocess → returns the answer as JSON response AND sends it to the
|
subprocess per query. Dit bespaart ~3-5 seconden startup tijd per query.
|
||||||
Telegram chat via `hermes send`.
|
|
||||||
|
Node-RED stuurt POST met msg.payload → Hermes (direct) → Antwoord + Telegram.
|
||||||
|
|
||||||
Endpoints:
|
Endpoints:
|
||||||
POST /ask — main relay endpoint
|
POST /ask — main relay endpoint
|
||||||
GET /health — health check for Node-RED monitoring
|
GET /health — health check
|
||||||
|
|
||||||
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 os
|
||||||
import subprocess
|
|
||||||
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
|
||||||
|
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__)
|
app = Flask(__name__)
|
||||||
|
|
||||||
# ── Config ──────────────────────────────────────────────────────────────
|
# ── Hermes warm houden ──────────────────────────────────────────────────
|
||||||
HERMES_BIN = "/usr/local/bin/hermes"
|
# Import de Hermes client (triggert eenmalige module laden)
|
||||||
TIMEOUT = 120 # seconds — hermes can take a while on complex queries
|
HERMES_BIN = "/usr/local/bin/hermes" # nog steeds nodig voor hermes send
|
||||||
TG_TARGET = "telegram" # hermes send target (home channel)
|
from hermes_client import call_hermes as _hermes_query
|
||||||
|
|
||||||
# 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:
|
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:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
[HERMES_BIN, "send", "--to", TG_TARGET, "--quiet", text],
|
[HERMES_BIN, "send", "--to", TG_TARGET, "--quiet", text],
|
||||||
@@ -51,45 +57,18 @@ def send_telegram(text: str) -> bool:
|
|||||||
)
|
)
|
||||||
return result.returncode == 0
|
return result.returncode == 0
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[hermes-relay] Telegram send failed: {e}", file=sys.stderr)
|
logger.error(f"Telegram send failed: {e}")
|
||||||
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."""
|
||||||
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,15 +78,15 @@ 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(f"Received: {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(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_msg = f"🤖 Hermes Relay\n\nVraag: {prompt}\n\nAntwoord: {answer}"
|
||||||
tg_sent = send_telegram(tg_msg)
|
tg_sent = send_telegram(tg_msg)
|
||||||
|
|
||||||
@@ -121,16 +100,15 @@ 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,
|
|
||||||
"timeout": TIMEOUT,
|
|
||||||
"prompt_prefix": PROMPT_PREFIX or "(none)",
|
|
||||||
"model": MODEL or "(default)",
|
"model": MODEL or "(default)",
|
||||||
|
"prompt_prefix": PROMPT_PREFIX or "(none)",
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
logger.info("Starting Hermes Relay v2.0 (warme agent)...")
|
||||||
app.run(host="0.0.0.0", port=8650, debug=False)
|
app.run(host="0.0.0.0", port=8650, debug=False)
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user