Files
aiagent/saars/backend/app/services/agent_proxy.py
2026-03-07 13:59:49 +08:00

136 lines
5.4 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.
"""
知你客服 Agent 代理方式一App 后端代理)
调用低代码平台执行 API登录拿 Token -> 创建执行 -> 轮询状态 -> 取 output_data 作为回复。
"""
import time
import logging
import requests
logger = logging.getLogger(__name__)
# 内存缓存platform_token, token_expires_at (简单实现,生产可用 Redis)
_platform_token = None
_token_expires_at = 0
TOKEN_BUFFER_SECONDS = 300 # 提前 5 分钟视为过期
def _get_platform_token(base_url, username, password):
"""登录平台获取 access_token带简单内存缓存。"""
global _platform_token, _token_expires_at
if _platform_token and time.time() < _token_expires_at - TOKEN_BUFFER_SECONDS:
return _platform_token
url = f"{base_url.rstrip('/')}/api/v1/auth/login"
try:
# 8037 使用 form 登录;先 form失败再试 JSON
r = requests.post(
url,
data={"username": username, "password": password},
headers={"Content-Type": "application/x-www-form-urlencoded"},
timeout=10,
)
if r.status_code == 422:
logger.warning("平台登录 form 返回 422尝试 JSON: %s", r.text[:200] if r.text else "")
r = requests.post(
url,
json={"username": username, "password": password},
headers={"Content-Type": "application/json"},
timeout=10,
)
if r.status_code == 401:
logger.warning("平台登录仍 401请检查 PLATFORM_USERNAME/PLATFORM_PASSWORD 及平台账号: %s", r.text[:200] if r.text else "")
raise ValueError(
"知你客服平台登录失败(401),请确认 8037 服务已启动且 PLATFORM_USERNAME/PLATFORM_PASSWORD 正确"
)
r.raise_for_status()
data = r.json()
token = data.get("access_token")
if not token:
raise ValueError("平台登录响应无 access_token")
_platform_token = token
_token_expires_at = time.time() + 3600 # 假设 1 小时有效
return token
except requests.exceptions.HTTPError as e:
if e.response is not None and e.response.status_code == 401:
raise ValueError(
"知你客服平台登录失败(401),请确认 8037 服务已启动且账号配置正确"
)
logger.exception("平台登录失败: %s", e)
raise
except ValueError:
raise
except Exception as e:
logger.exception("平台登录失败: %s", e)
raise
def _create_execution(base_url, token, agent_id, input_data):
"""POST /api/v1/executions"""
url = f"{base_url.rstrip('/')}/api/v1/executions"
r = requests.post(
url,
json={"agent_id": agent_id, "input_data": input_data},
headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
timeout=15,
)
r.raise_for_status()
return r.json()
def _get_execution_status(base_url, token, execution_id):
"""GET /api/v1/executions/{id}/status"""
url = f"{base_url.rstrip('/')}/api/v1/executions/{execution_id}/status"
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10)
r.raise_for_status()
return r.json()
def _get_execution(base_url, token, execution_id):
"""GET /api/v1/executions/{id}"""
url = f"{base_url.rstrip('/')}/api/v1/executions/{execution_id}"
r = requests.get(url, headers={"Authorization": f"Bearer {token}"}, timeout=10)
r.raise_for_status()
return r.json()
def _extract_reply(output_data):
"""从 output_data 中提取回复文本(知你客服 End 节点输出结构以实际为准)。"""
if output_data is None:
return ""
if isinstance(output_data, str):
return output_data
for key in ("reply", "content", "output", "text", "message", "result"):
if key in output_data and output_data[key]:
v = output_data[key]
return v if isinstance(v, str) else str(v)
if isinstance(output_data, dict):
for v in output_data.values():
if isinstance(v, str) and v.strip():
return v
return str(output_data)
def chat_with_agent(base_url, username, password, agent_id, message, user_id, poll_interval=0.8, poll_timeout=60):
"""
与知你客服对话:创建执行 -> 轮询直到 completed/failed -> 返回回复文本。
"""
token = _get_platform_token(base_url, username, password)
input_data = {"query": message, "user_id": user_id or "default"}
exec_body = _create_execution(base_url, token, agent_id, input_data)
execution_id = exec_body.get("id")
if not execution_id:
raise ValueError("创建执行未返回 id")
status = exec_body.get("status", "pending")
deadline = time.time() + poll_timeout
status_body = None
while status in ("pending", "running") and time.time() < deadline:
time.sleep(poll_interval)
status_body = _get_execution_status(base_url, token, execution_id)
status = status_body.get("status", status)
if status != "completed":
detail = _get_execution(base_url, token, execution_id)
err = detail.get("error_message") or (status_body or {}).get("error_message") or "执行未完成或失败"
raise ValueError(err)
detail = _get_execution(base_url, token, execution_id)
output_data = detail.get("output_data")
return _extract_reply(output_data)