Files
JARVIS/backend/app/services/agent_runtime/hermes_runtime.py

173 lines
6.7 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
import importlib.util
import sys
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, AsyncGenerator
from app.services.agent_runtime.base import ChatRuntime, RuntimePreparedContext
from app.services.agent_runtime.hermes_session_manager import hermes_session_manager
class HermesRuntimeAdapter(ChatRuntime):
name = "hermes"
def __init__(self) -> None:
self._repo_path = Path(__file__).resolve().parents[4] / ".tmp" / "hermes-agent"
self._agent_class = None
def probe(self) -> dict[str, Any]:
cli_path = self._repo_path / "cli.py"
run_agent_path = self._repo_path / "run_agent.py"
return {
"repo_path": str(self._repo_path),
"repo_exists": self._repo_path.exists(),
"cli_exists": cli_path.exists(),
"run_agent_exists": run_agent_path.exists(),
"supports_single_query": True,
"supports_resume": True,
"integration_mode": "python_ai_agent_bridge",
}
def _load_agent_class(self):
if self._agent_class is not None:
return self._agent_class
run_agent_path = self._repo_path / "run_agent.py"
if not run_agent_path.exists():
raise RuntimeError(f"Hermes run_agent.py 未找到: {run_agent_path}")
repo_path = str(self._repo_path)
if repo_path not in sys.path:
sys.path.insert(0, repo_path)
spec = importlib.util.spec_from_file_location("jarvis_hermes_run_agent", run_agent_path)
if spec is None or spec.loader is None:
raise RuntimeError("无法加载 Hermes run_agent 模块")
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self._agent_class = getattr(module, "AIAgent")
return self._agent_class
def _build_agent(self, prepared: RuntimePreparedContext, session_id: str):
agent_class = self._load_agent_class()
kwargs: dict[str, Any] = {
"session_id": session_id,
"platform": "jarvis",
"user_id": prepared.user.id,
"quiet_mode": True,
"persist_session": True,
"skip_context_files": True,
"max_iterations": 30,
}
if prepared.model_name:
kwargs["model"] = prepared.model_name
return agent_class(**kwargs)
def _build_system_message(self, prepared: RuntimePreparedContext) -> str:
parts = [
"You are Hermes running inside the Jarvis chat runtime.",
"Return normal assistant text for the user. Do not mention internal bridge details unless asked.",
]
if prepared.memory_context:
parts.append(prepared.memory_context)
return "\n\n".join(parts)
async def chat_stream(
self,
prepared: RuntimePreparedContext,
) -> AsyncGenerator[dict[str, Any], None]:
handle = hermes_session_manager.get_or_create(
conversation_id=prepared.conversation.id,
user_id=prepared.user.id,
)
async with handle.lock:
yield {
"type": "progress",
"stage": "planning",
"label": "Hermes 正在准备会话",
"agent": "hermes",
"step": "加载 Hermes runtime",
"steps": [
"恢复会话上下文",
"调用 Hermes AIAgent",
"回传流式回复",
],
}
queue: asyncio.Queue[dict[str, Any] | None] = asyncio.Queue()
loop = asyncio.get_running_loop()
result_box: dict[str, Any] = {"content": None, "error": None, "model": prepared.model_name or "hermes"}
def stream_callback(delta: str) -> None:
loop.call_soon_threadsafe(queue.put_nowait, {"type": "chunk", "content": delta})
def run_sync() -> None:
try:
agent = self._build_agent(prepared, handle.hermes_session_id)
result = agent.run_conversation(
prepared.full_message,
system_message=self._build_system_message(prepared),
stream_callback=stream_callback,
)
result_box["content"] = str(result.get("final_response") or "")
result_box["model"] = getattr(agent, "model", prepared.model_name or "hermes")
except Exception as exc: # pragma: no cover - surfaced through queue
result_box["error"] = f"Hermes 执行失败: {exc}"
loop.call_soon_threadsafe(
queue.put_nowait,
{"type": "error", "error": result_box["error"]},
)
finally:
loop.call_soon_threadsafe(queue.put_nowait, None)
worker = asyncio.create_task(asyncio.to_thread(run_sync))
streamed_text = ""
while True:
event = await queue.get()
if event is None:
break
if event.get("type") == "chunk":
streamed_text += str(event.get("content", ""))
yield event
await worker
handle.last_used_at = datetime.now(UTC)
handle.metadata = {
"session_id": handle.hermes_session_id,
"model": result_box["model"],
"last_error": result_box["error"],
}
final_text = result_box["content"] or streamed_text
if final_text and final_text != streamed_text:
yield {"type": "chunk", "content": final_text}
async def chat_once(self, prepared: RuntimePreparedContext) -> tuple[str, str | None]:
handle = hermes_session_manager.get_or_create(
conversation_id=prepared.conversation.id,
user_id=prepared.user.id,
)
async with handle.lock:
agent = await asyncio.to_thread(self._build_agent, prepared, handle.hermes_session_id)
result = await asyncio.to_thread(
agent.run_conversation,
prepared.full_message,
self._build_system_message(prepared),
)
handle.last_used_at = datetime.now(UTC)
resolved_model = getattr(agent, "model", prepared.model_name or "hermes")
handle.metadata = {
"session_id": handle.hermes_session_id,
"model": resolved_model,
"last_error": None,
}
return str(result.get("final_response") or ""), resolved_model
hermes_runtime_adapter = HermesRuntimeAdapter()