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()