This commit is contained in:
2026-04-08 11:00:31 +08:00
parent fb21a127b5
commit 7019cf180d
17 changed files with 960 additions and 1 deletions

View File

@@ -0,0 +1,237 @@
"""
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
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_final:
try:
await ws_client.reply_stream(
frame, stream_id, "💭 正在思考...", False
)
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 "抱歉,处理消息时出现错误,请稍后重试。"