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