Compare commits
5 Commits
v1.0
..
58ca990673
| Author | SHA1 | Date | |
|---|---|---|---|
| 58ca990673 | |||
| e05fa9c5c1 | |||
| 1780eed406 | |||
| 75311845ee | |||
| 1bac8db3c5 |
@@ -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`
|
||||
via subprocess → returns the answer as JSON response AND sends it to the
|
||||
Telegram chat via `hermes send`.
|
||||
Gebruikt de voice-assistant profiel (minimale chatbot, max_turns=10).
|
||||
Geen prefix/model override nodig — profiel handelt dit af.
|
||||
|
||||
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
|
||||
Node-RED stuurt POST met msg.payload → Hermes (direct) → Alleen antwoord terug.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
# ── 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)
|
||||
# ── Hermes warm houden ──────────────────────────────────────────────────
|
||||
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)
|
||||
# Laad gateway config eenmalig voor Telegram verzenden
|
||||
_gw_config_loaded = False
|
||||
_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:
|
||||
"""Send a message via hermes send. Returns True on success."""
|
||||
def send_telegram(text):
|
||||
"""Stuur bericht via gateway's send_message_tool (gebruikt gateway config met echte token)."""
|
||||
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)
|
||||
logger.info("Telegram send: starting...")
|
||||
_ensure_gw_config()
|
||||
logger.info("Telegram send: gw config loaded")
|
||||
from tools.send_message_tool import send_message_tool
|
||||
import json
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
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"
|
||||
logger.info("Telegram send: success=%s", result.get("success", False))
|
||||
return result.get("success", False)
|
||||
except Exception as e:
|
||||
return f"⚠️ Error: {e}"
|
||||
logger.error("Telegram send failed with exception: %s", e, exc_info=True)
|
||||
return False
|
||||
|
||||
|
||||
# ── Routes ──────────────────────────────────────────────────────────────
|
||||
|
||||
@app.route("/ask", methods=["POST"])
|
||||
def ask():
|
||||
"""Main relay endpoint. Expects JSON body with 'payload' field."""
|
||||
"""Main relay endpoint — retourneert alleen het antwoord."""
|
||||
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,17 +102,16 @@ def ask():
|
||||
if not prompt:
|
||||
return jsonify({"status": "error", "message": "No payload received"}), 400
|
||||
|
||||
print(f"[hermes-relay] Received: {prompt[:100]}...")
|
||||
logger.info("Received: %s...", 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("Answer (%.1fs): %s...", elapsed, answer[:200])
|
||||
|
||||
# Also send to Telegram
|
||||
tg_msg = f"🤖 Hermes Relay\n\nVraag: {prompt}\n\nAntwoord: {answer}"
|
||||
tg_sent = send_telegram(tg_msg)
|
||||
# Stuur ook naar Telegram (alleen het antwoord)
|
||||
tg_sent = send_telegram(answer)
|
||||
|
||||
return jsonify({
|
||||
"status": "ok",
|
||||
@@ -121,16 +123,16 @@ 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)",
|
||||
"version": "2.1",
|
||||
"mode": "python-import",
|
||||
"profile": "voice-assistant",
|
||||
})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logger.info("Starting Hermes Relay v2.1 (voice-assistant profiel)...")
|
||||
app.run(host="0.0.0.0", port=8650, debug=False)
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user