173 lines
6.7 KiB
Python
173 lines
6.7 KiB
Python
|
|
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()
|