Files
X-Financial/server/src/app/services/system_hermes.py

166 lines
4.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import shutil
import subprocess
from dataclasses import dataclass
from pathlib import Path
from typing import Mapping
from app.core.config import SERVER_DIR
@dataclass(frozen=True, slots=True)
class HermesCliResult:
response_text: str
session_id: str = ""
command: tuple[str, ...] = ()
@dataclass(frozen=True, slots=True)
class HermesProcessHandle:
pid: int
command: tuple[str, ...] = ()
stdout_path: str = ""
stderr_path: str = ""
class SystemHermesService:
def __init__(self) -> None:
configured_bin = str(os.getenv("HERMES_BIN", "")).strip()
self.hermes_bin = configured_bin or shutil.which("hermes") or "/usr/local/bin/hermes"
def is_available(self) -> bool:
return Path(self.hermes_bin).exists()
def run_query(
self,
query: str,
*,
source: str = "tool",
max_turns: int = 1,
timeout_seconds: int = 180,
skills: tuple[str, ...] = (),
env_overrides: Mapping[str, str] | None = None,
yolo: bool = False,
) -> HermesCliResult:
if not self.is_available():
raise RuntimeError(f"未找到系统 Hermes CLI{self.hermes_bin}")
command = self._build_command(
query,
source=source,
max_turns=max_turns,
skills=skills,
yolo=yolo,
)
env = os.environ.copy()
if env_overrides:
env.update({str(key): str(value) for key, value in env_overrides.items()})
completed = subprocess.run(
command,
capture_output=True,
text=True,
timeout=timeout_seconds,
check=False,
env=env,
)
if completed.returncode != 0:
detail = (completed.stderr or completed.stdout or "").strip()
raise RuntimeError(detail or "Hermes CLI 返回非 0 状态码。")
return self._parse_output(completed.stdout, command=command)
def start_query_background(
self,
query: str,
*,
source: str = "tool",
max_turns: int = 1,
skills: tuple[str, ...] = (),
env_overrides: Mapping[str, str] | None = None,
log_prefix: str = "hermes",
yolo: bool = False,
) -> HermesProcessHandle:
if not self.is_available():
raise RuntimeError(f"未找到系统 Hermes CLI{self.hermes_bin}")
command = self._build_command(
query,
source=source,
max_turns=max_turns,
skills=skills,
yolo=yolo,
)
env = os.environ.copy()
if env_overrides:
env.update({str(key): str(value) for key, value in env_overrides.items()})
log_dir = SERVER_DIR / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
safe_prefix = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "-" for ch in log_prefix)
stdout_path = log_dir / f"{safe_prefix}.out.log"
stderr_path = log_dir / f"{safe_prefix}.err.log"
stdout_file = stdout_path.open("ab")
stderr_file = stderr_path.open("ab")
process = subprocess.Popen(
command,
stdout=stdout_file,
stderr=stderr_file,
env=env,
start_new_session=True,
)
stdout_file.close()
stderr_file.close()
return HermesProcessHandle(
pid=process.pid,
command=command,
stdout_path=str(stdout_path),
stderr_path=str(stderr_path),
)
def _build_command(
self,
query: str,
*,
source: str,
max_turns: int,
skills: tuple[str, ...],
yolo: bool,
) -> tuple[str, ...]:
command_parts = [
self.hermes_bin,
"chat",
"-Q",
"--source",
source,
"--max-turns",
str(max_turns),
]
for skill in skills:
normalized_skill = str(skill or "").strip()
if normalized_skill:
command_parts.extend(["--skills", normalized_skill])
if yolo:
command_parts.append("--yolo")
command_parts.extend(["-q", query])
return tuple(command_parts)
@staticmethod
def _parse_output(stdout: str, *, command: tuple[str, ...]) -> HermesCliResult:
lines = [line.rstrip() for line in str(stdout or "").splitlines()]
session_id = ""
response_lines: list[str] = []
for line in lines:
if line.startswith("session_id:"):
session_id = line.split(":", 1)[1].strip()
continue
response_lines.append(line)
response_text = "\n".join(line for line in response_lines if line.strip()).strip()
return HermesCliResult(
response_text=response_text,
session_id=session_id,
command=command,
)