feat(v9): Phase 2+3 — harter Cutover auf Hermes-Agent (:8642)
Mission Control proxyt jetzt zum Nous-Hermes-Agent-Server statt zum Eigenbau-Agenten: - routers/hermes.py: Chat-WS streamt als Proxy zu :8642 (OpenAI-API), WS-Contract (thinking/token/done/error) beibehalten -> HermesPanel unveraendert, kein Frontend-Build noetig. Status meldet Hermes-Health. - config.py: HERMES_API_URL + HERMES_API_KEY (Key aus ~/.hermes/.env); HERMES_SIMPLE/COMPLEX_MODEL entfernt (Routing macht Hermes selbst). - hermes_agent.py geloescht (ReAct-Loop, tote Tools, DuckDuckGo, Fake-Stream). - CLAUDE/README/ROADMAP aktualisiert; Phasen 2+3 abgehakt. Lokaler Smoke-Test: App importiert, /api/hermes/status liefert 200. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+78
-11
@@ -18,6 +18,7 @@ import threading
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import httpx
|
||||
from fastapi import APIRouter, Depends, UploadFile, File, WebSocket, WebSocketDisconnect
|
||||
from fastapi.responses import Response, JSONResponse
|
||||
|
||||
@@ -25,8 +26,8 @@ from auth import auth
|
||||
from config import (
|
||||
PIPER_BIN, PIPER_VOICE, WHISPER_MODEL_SIZE,
|
||||
HERMES_WINDOWS_HOST, HERMES_WINDOWS_USER, HERMES_SSH_KEY,
|
||||
HERMES_API_URL, HERMES_API_KEY,
|
||||
)
|
||||
from hermes_agent import run_agent
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
@@ -100,7 +101,12 @@ def _tts(text: str) -> Optional[bytes]:
|
||||
|
||||
@router.websocket("/hermes/chat")
|
||||
async def hermes_chat(websocket: WebSocket):
|
||||
"""Streaming Chat mit dem Hermes Agent via WebSocket."""
|
||||
"""Streaming-Chat: proxyt zum Hermes-Agent-Server (:8642, OpenAI-kompatibel).
|
||||
|
||||
Behaelt den WS-Contract des Frontends bei (thinking/token/done/error), damit
|
||||
die HermesPanel unveraendert bleibt. Modell-Wahl, Tools, Gedaechtnis und
|
||||
Mehr-Schritt-Logik macht der Hermes-Agent selbst.
|
||||
"""
|
||||
# Manuelle Token-Auth (WS kann keine HTTP-Header senden)
|
||||
from auth import TOKEN
|
||||
token_param = websocket.query_params.get("token", "")
|
||||
@@ -121,18 +127,71 @@ async def hermes_chat(websocket: WebSocket):
|
||||
if not message:
|
||||
continue
|
||||
|
||||
async def send(obj: dict) -> None:
|
||||
await websocket.send_json(obj)
|
||||
|
||||
await run_agent(message, send)
|
||||
await websocket.send_json({"type": "thinking"})
|
||||
try:
|
||||
await _proxy_chat(message, websocket)
|
||||
except Exception as exc:
|
||||
await websocket.send_json(
|
||||
{"type": "error", "content": f"Hermes nicht erreichbar: {exc}"}
|
||||
)
|
||||
await websocket.send_json({"type": "done"})
|
||||
|
||||
except WebSocketDisconnect:
|
||||
pass
|
||||
except Exception as exc:
|
||||
try:
|
||||
await websocket.send_json({"type": "error", "content": str(exc)})
|
||||
except Exception:
|
||||
pass
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
async def _proxy_chat(message: str, websocket: WebSocket) -> None:
|
||||
"""Eine Antwort vom Hermes-Agent-Server holen und Tokens ans WS relayen.
|
||||
|
||||
Unterstuetzt sowohl SSE-Streaming als auch eine nicht-streamende
|
||||
JSON-Antwort — in beiden Faellen genau EIN Agent-Lauf (kein Doppel-Call).
|
||||
"""
|
||||
headers = {"Content-Type": "application/json"}
|
||||
if HERMES_API_KEY:
|
||||
headers["Authorization"] = f"Bearer {HERMES_API_KEY}"
|
||||
body = {
|
||||
"model": "hermes-agent",
|
||||
"messages": [{"role": "user", "content": message}],
|
||||
"stream": True,
|
||||
}
|
||||
timeout = httpx.Timeout(300.0, connect=10.0)
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
async with client.stream(
|
||||
"POST", f"{HERMES_API_URL}/chat/completions", headers=headers, json=body
|
||||
) as resp:
|
||||
if resp.status_code != 200:
|
||||
detail = (await resp.aread()).decode(errors="replace")[:300]
|
||||
await websocket.send_json(
|
||||
{"type": "error", "content": f"HTTP {resp.status_code}: {detail}"}
|
||||
)
|
||||
return
|
||||
|
||||
if "text/event-stream" in resp.headers.get("content-type", ""):
|
||||
async for line in resp.aiter_lines():
|
||||
if not line.startswith("data:"):
|
||||
continue
|
||||
data = line[5:].strip()
|
||||
if data == "[DONE]":
|
||||
break
|
||||
try:
|
||||
delta = json.loads(data)["choices"][0].get("delta", {})
|
||||
piece = delta.get("content") or ""
|
||||
except Exception:
|
||||
piece = ""
|
||||
if piece:
|
||||
await websocket.send_json({"type": "token", "content": piece})
|
||||
else:
|
||||
# Server streamt nicht → ganze JSON-Antwort als ein Token senden
|
||||
raw = await resp.aread()
|
||||
try:
|
||||
d = json.loads(raw)
|
||||
content = d["choices"][0]["message"].get("content", "") or ""
|
||||
except Exception:
|
||||
content = raw.decode(errors="replace")
|
||||
if content:
|
||||
await websocket.send_json({"type": "token", "content": content})
|
||||
|
||||
|
||||
@router.post("/hermes/transcribe", dependencies=[Depends(auth)])
|
||||
@@ -206,7 +265,15 @@ def hermes_status():
|
||||
except Exception:
|
||||
ssh_ok = False
|
||||
|
||||
# Hermes-Agent-Server (:8642) — erreichbar?
|
||||
try:
|
||||
httpx.get(HERMES_API_URL.rsplit("/v1", 1)[0] + "/", timeout=2)
|
||||
hermes = "ready"
|
||||
except Exception:
|
||||
hermes = "offline"
|
||||
|
||||
return {
|
||||
"hermes": hermes,
|
||||
"whisper": whisper,
|
||||
"piper": piper,
|
||||
"ssh_configured": ssh_configured,
|
||||
|
||||
Reference in New Issue
Block a user