feat(backend): add office router and agent runtime services
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
172
backend/app/services/agent_runtime/hermes_runtime.py
Normal file
172
backend/app/services/agent_runtime/hermes_runtime.py
Normal file
@@ -0,0 +1,172 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user