""" Agent HTTP 客户端模块 负责与远程 ADK Agent 服务通信,包括: - HTTP 客户端生命周期管理 - 用户会话管理(创建、缓存、清除) - SSE 流式调用(主方案) - 同步调用(降级方案) """ import json import logging from typing import Optional import httpx from config import Config from bot.reply_parser import extract_final_reply logger = logging.getLogger(__name__) # 用户会话映射: user_id -> session_id _user_sessions: dict[str, str] = {} # Agent HTTP 客户端 _http_client: Optional[httpx.AsyncClient] = None def get_http_client() -> httpx.AsyncClient: """获取或创建 HTTP 客户端(全局单例)""" global _http_client if _http_client is None: _http_client = httpx.AsyncClient( base_url=Config.AGENT_BASE_URL, timeout=Config.AGENT_TIMEOUT, ) return _http_client async def close_http_client(): """关闭 HTTP 客户端""" global _http_client if _http_client: await _http_client.aclose() _http_client = None # ============================================================ # 会话管理 # ============================================================ async def get_or_create_session(user_id: str) -> str: """获取或创建用户的 Agent 会话""" if user_id in _user_sessions: return _user_sessions[user_id] client = get_http_client() try: url = f"/apps/{Config.AGENT_APP_NAME}/users/{user_id}/sessions" resp = await client.post(url, json={}) resp.raise_for_status() data = resp.json() session_id = data["id"] _user_sessions[user_id] = session_id logger.info("为用户 %s 创建 Agent 会话: %s", user_id, session_id) return session_id except Exception as e: logger.error("创建 Agent 会话失败: %s", e) raise def clear_session(user_id: str): """清除用户的会话缓存(会话失效时调用)""" _user_sessions.pop(user_id, None) # ============================================================ # Agent 调用 # ============================================================ async def call_agent_stream( ws_client, frame: dict, user_id: str, message: str, ) -> str: """ 通过 SSE 流式调用 Agent,实现打字机效果。 失败时降级为同步调用。 返回完整的回复文本。 """ from aibot import generate_req_id session_id = await get_or_create_session(user_id) client = get_http_client() try: req_body = { "app_name": Config.AGENT_APP_NAME, "user_id": user_id, "session_id": session_id, "new_message": { "role": "user", "parts": [{"text": message}], }, "streaming": True, } async with client.stream( "POST", "/run_sse", json=req_body, timeout=Config.AGENT_TIMEOUT, ) as response: if response.status_code != 200: logger.warning( "SSE 流式调用返回 %d,降级为同步调用", response.status_code, ) return await call_agent_sync(user_id, message) stream_id = generate_req_id("stream") full_text = "" has_sent_final = False has_sent_thinking = False async for line in response.aiter_lines(): if not line.startswith("data: "): continue data_str = line[6:] try: data = json.loads(data_str) except json.JSONDecodeError: continue # 错误处理 if "error" in data: logger.error("Agent SSE 错误: %s", data["error"]) if "not found" in data["error"].lower(): clear_session(user_id) return "抱歉,处理消息时出现错误,请稍后重试。" # 收集所有文本(含思考过程) parts = data.get("content", {}).get("parts", []) for part in parts: text = part.get("text", "") if not text: continue if part.get("thought"): continue full_text += text # 只发送一次"正在思考"状态 if full_text and not has_sent_thinking and not has_sent_final: try: await ws_client.reply_stream( frame, stream_id, "💭 正在思考...", False ) has_sent_thinking = True except Exception: pass # 最终完成时提取正式回复并发送 is_final = data.get("finishReason") == "STOP" if is_final and full_text and not has_sent_final: clean_reply = extract_final_reply(full_text) logger.info("Agent 回复完成,长度: %d", len(clean_reply)) try: await ws_client.reply_stream( frame, stream_id, clean_reply, True ) has_sent_final = True except Exception as e: logger.warning("推送最终回复失败: %s", e) # 兜底:如果流结束但未发送最终回复 if full_text and not has_sent_final: clean_reply = extract_final_reply(full_text) logger.info("Agent SSE 兜底发送,长度: %d", len(clean_reply)) try: await ws_client.reply_stream( frame, stream_id, clean_reply, True ) except Exception as e: logger.warning("推送兜底回复失败: %s", e) return extract_final_reply(full_text) if full_text else "" except httpx.ConnectError: logger.error("无法连接到 Agent 服务: %s", Config.AGENT_BASE_URL) return "抱歉,Agent 服务连接失败,请稍后重试。" except Exception as e: logger.error("SSE 流式调用异常,降级为同步: %s", e) return await call_agent_sync(user_id, message) async def call_agent_sync(user_id: str, message: str) -> str: """ 同步调用 Agent(/run 接口),作为流式调用的降级方案。 """ session_id = await get_or_create_session(user_id) client = get_http_client() def _build_body(sid: str) -> dict: return { "app_name": Config.AGENT_APP_NAME, "user_id": user_id, "session_id": sid, "new_message": { "role": "user", "parts": [{"text": message}], }, } def _extract_reply(events: list) -> str: """从 Event[] 中提取最终回复(跳过 thought)""" for event in events: parts = event.get("content", {}).get("parts", []) for part in parts: if part.get("text") and not part.get("thought"): return part["text"] return "" try: resp = await client.post("/run", json=_build_body(session_id)) resp.raise_for_status() return _extract_reply(resp.json()) except httpx.HTTPStatusError as e: if e.response.status_code == 404: clear_session(user_id) new_session = await get_or_create_session(user_id) resp = await client.post("/run", json=_build_body(new_session)) resp.raise_for_status() return _extract_reply(resp.json()) logger.error("Agent HTTP 错误 [%d]: %s", e.response.status_code, e) return "抱歉,服务暂时不可用,请稍后重试。" except Exception as e: logger.error("同步调用 Agent 失败: %s", e) return "抱歉,处理消息时出现错误,请稍后重试。"