Files
moss-ai/app/backend-python/services/conductor_service.py
雷雨 8635b84b2d init
2025-12-15 22:05:56 +08:00

326 lines
12 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.
"""
Conductor Agent 服务
负责与 Conductor Agent 通信
"""
import logging
import httpx
import json
from typing import Dict, Any
from uuid import uuid4
from models.chat import A2ARequest, A2AMessage, A2AMessagePart
from config import settings
from services.chat_history_service import chat_history_service
logger = logging.getLogger(__name__)
class ConductorService:
"""Conductor Agent 通信服务"""
def __init__(self):
self.base_url = settings.CONDUCTOR_AGENT_URL
self.timeout = settings.CONDUCTOR_TIMEOUT
self._request_id_counter = 1
def _generate_message_id(self) -> str:
"""生成消息 ID"""
return f"msg-{uuid4().hex[:16]}"
def _build_a2a_request(self, user_message: str, system_user_id: int, context_id: str) -> Dict[str, Any]:
"""构建 A2A 协议请求"""
# 在消息中注入用户ID让 Agent 知道当前用户
message_with_user_id = f"[SYSTEM_USER_ID:{system_user_id}] {user_message}"
return {
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"context_id": context_id,
"role": "user",
"parts": [
{
"kind": "text",
"text": message_with_user_id
}
],
"message_id": self._generate_message_id()
}
},
"id": self._request_id_counter
}
async def _save_operation_record_if_present(self, content: str, system_user_id: int, context_id: str):
"""
尝试从响应内容中提取operation_record并保存
如果content中包含JSON格式的operation_record提取并保存到数据库
"""
try:
# 尝试解析content为JSON
content_json = json.loads(content)
# 检查是否包含operation_record
if isinstance(content_json, dict) and "operation_record" in content_json:
operation_record = content_json["operation_record"]
# 更新记录中的用户ID和上下文ID以确保一致性
operation_record["system_user_id"] = system_user_id
operation_record["context_id"] = context_id
# 导入并使用设备操作服务
from services.device_operation_service import get_device_operation_service
device_operation_service = get_device_operation_service()
# 保存操作记录
success = device_operation_service.save_operation_record(operation_record)
if success:
device_type = operation_record.get('device_type', 'unknown')
action = operation_record.get('action', 'unknown')
logger.info(f"💾 已保存设备操作记录: device={device_type}, action={action}")
else:
logger.warning(f"⚠️ 保存设备操作记录失败")
except json.JSONDecodeError:
# content不是JSON格式跳过
pass
except Exception as e:
logger.error(f"❌ 提取或保存操作记录时出错: {e}", exc_info=True)
def _extract_content_from_response(self, response: Dict[str, Any]) -> tuple[str, str, bool]:
"""
从 A2A 响应中提取内容
Returns:
tuple[str, str, bool]: (content, status, is_error)
- content: 响应内容
- status: 状态 (success, failed, error)
- is_error: 是否为错误
"""
try:
result = response.get("result", {})
# 先检查任务状态,如果失败直接返回错误
status = result.get("status", {})
state = status.get("state", "")
if state == "failed":
# 尝试从错误信息中提取详细错误
error_msg = "❌ 任务执行失败"
# 从history中查找错误信息
history = result.get("history", [])
for item in reversed(history):
if item.get("role") == "agent":
for part in item.get("parts", []):
if text := part.get("text"):
if "错误" in text or "失败" in text or "error" in text.lower():
error_msg = text
break
return error_msg, "failed", True
# 1. 从 artifacts 提取
artifacts = result.get("artifacts", [])
if artifacts:
contents = []
for artifact in artifacts:
for part in artifact.get("parts", []):
if text := part.get("text"):
contents.append(text)
if contents:
return "\n\n".join(contents), "success", False
# 2. 从 history 提取 agent 回复
history = result.get("history", [])
agent_messages = []
for item in history:
if item.get("role") == "agent":
for part in item.get("parts", []):
if text := part.get("text"):
agent_messages.append(text)
if agent_messages:
return "\n\n".join(agent_messages), "success", False
# 3. 如果已完成但没有内容
if state == "completed":
return "✅ 任务已完成", "success", False
# 4. 默认返回
return "已收到响应", "success", False
except Exception as e:
logger.error(f"提取响应内容失败: {e}", exc_info=True)
return f"处理响应时出错: {str(e)}", "error", True
async def send_message(self, user_message: str, system_user_id: int, context_id: str) -> Dict[str, Any]:
"""
发送消息到 Conductor Agent
Args:
user_message: 用户消息
system_user_id: 系统用户ID
context_id: 会话上下文ID
Returns:
包含响应内容的字典
"""
# 先保存用户消息到数据库(保存原始用户消息,不带系统前缀)
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=context_id,
role="user",
content=user_message,
status="success"
)
logger.info(f"💾 已保存用户消息: context={context_id}, content={user_message[:50]}...")
try:
# 构建请求注入用户ID
request_data = self._build_a2a_request(user_message, system_user_id, context_id)
logger.info(f"📤 发送消息到 Conductor Agent: {user_message[:50]}...")
# 发送请求
async with httpx.AsyncClient(timeout=self.timeout) as client:
response = await client.post(
f"{self.base_url}/",
json=request_data,
headers={"Content-Type": "application/json"}
)
response.raise_for_status()
response_data = response.json()
logger.info(f"📥 收到 Conductor Agent 响应")
# 递增请求ID
self._request_id_counter += 1
# 提取内容(带错误检测)
content, msg_status, is_error = self._extract_content_from_response(response_data)
# 提取任务ID和上下文ID
result = response_data.get("result", {})
task_id = result.get("id")
response_context_id = result.get("contextId", context_id)
# 尝试从content中提取operation_record并保存
await self._save_operation_record_if_present(content, system_user_id, response_context_id)
# 保存agent响应到数据库
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=response_context_id,
role="agent",
content=content,
task_id=task_id,
status=msg_status,
error_message=content if is_error else None
)
logger.info(f"💾 已保存Agent回复: context={response_context_id}, content={content[:50]}...")
# 如果是错误,返回错误状态
if is_error:
return {
"content": content,
"context_id": response_context_id,
"task_id": task_id,
"status": msg_status # failed 或 error
}
return {
"content": content,
"context_id": response_context_id,
"task_id": task_id,
"status": "success"
}
except httpx.TimeoutException as e:
error_msg = "⏱️ 请求超时Agent 可能正在处理复杂任务"
logger.error(error_msg)
# 保存错误消息到数据库
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=context_id,
role="system",
content=error_msg,
status="error",
error_message=str(e)
)
raise Exception(error_msg)
except httpx.ConnectError as e:
error_msg = f"🔌 无法连接到 Conductor Agent请确保服务已启动 ({self.base_url})"
logger.error(error_msg)
# 保存错误消息到数据库
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=context_id,
role="system",
content=error_msg,
status="error",
error_message=str(e)
)
raise Exception(error_msg)
except httpx.HTTPStatusError as e:
error_msg = f"❌ Agent 返回错误: {e.response.status_code}"
logger.error(error_msg)
# 保存错误消息到数据库
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=context_id,
role="system",
content=error_msg,
status="error",
error_message=str(e)
)
raise Exception(error_msg)
except Exception as e:
error_msg = f"💥 发送消息失败: {str(e)}"
logger.error(error_msg, exc_info=True)
# 保存错误消息到数据库
await chat_history_service.save_message(
system_user_id=system_user_id,
context_id=context_id,
role="system",
content=error_msg,
status="error",
error_message=str(e)
)
raise Exception(error_msg)
async def test_connection(self) -> bool:
"""测试与 Conductor Agent 的连接"""
try:
async with httpx.AsyncClient(timeout=5.0) as client:
response = await client.get(
f"{self.base_url}/.well-known/agent-card.json"
)
response.raise_for_status()
data = response.json()
logger.info(f"✅ Conductor Agent 连接正常: {data.get('name', 'Unknown')}")
return True
except Exception as e:
logger.error(f"❌ Conductor Agent 连接失败: {e}")
return False
# 创建全局服务实例
conductor_service = ConductorService()