rtc-voice-chat/backend/tools/builtin/api_tools.py
2026-04-02 09:40:23 +08:00

205 lines
7.3 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.

"""
可声明式批量注册的 HTTP API 工具。
添加新接口只需在 API_ENDPOINTS 列表中追加一条配置,无需编写函数代码。
URL 中支持 {param} 路径参数GET 自动走 query stringPOST/PUT 自动走 JSON body。
"""
import json
import re
import httpx
from tools.registry import tool_registry
# ── 公共默认值(所有接口共享) ─────────────────────────────
DEFAULT_BASE_URL = "https://hskuaikuai.com:9000/test" # TODO: 替换为实际服务地址
DEFAULT_HEADERS = {
"Content-Type": "application/json",
"token": "265a068eb7af455ca97f1a5063561a5d", # TODO: 后续由前端传入
}
DEFAULT_TIMEOUT = 10 # 秒
# ── API 端点声明 ─────────────────────────────────────────
#
# 字段说明:
# name ─ 工具名称(英文,唯一)
# description ─ 工具描述LLM 据此判断何时调用)
# method ─ HTTP 方法GET / POST / PUT / DELETE
# path ─ 请求路径(自动拼接 DEFAULT_BASE_URL可含 {param} 占位符
# headers ─ 可选,额外请求头(与 DEFAULT_HEADERS 合并)
# parameters ─ OpenAI function calling parameters schema
#
# 路由规则:
# - path 自动拼接 DEFAULT_BASE_URL
# - {param} 占位符 → 从参数中取出并替换到 URL
# - GET → 剩余参数作为 query string
# - POST / PUT → 剩余参数作为 JSON body
#
API_ENDPOINTS: list[dict] = [
# ── 示例:签到汇总 ──
{
"name": "checkin_sum",
"description": "查询出勤人数",
"method": "POST",
"path": "/checkin/sum",
"parameters": {
"type": "object",
"properties": {},
"required": [],
},
},
# ── APP获取不同部门考勤打卡信息 ──
{
"name": "get_checkin_app_dept",
"description": "获取不同部门的考勤打卡信息包括出勤率、总人数、打卡人数等。deptName传'全部'可一次查询所有部门汇总。",
"method": "POST",
"path": "/checkin/app/dept",
"parameters": {
"type": "object",
"properties": {
"deptName": {
"type": "string",
"description": "部门名称,传'全部'查所有部门",
"enum": ["养护部", "工程部", "环卫部", "办公室", "全部"],
},
},
"required": ["deptName"],
},
},
]
def _summarize_checkin_dept(raw: str) -> str:
"""将 get_checkin_app_dept 的完整返回精简为摘要,大幅减少 token 消耗。"""
try:
obj = json.loads(raw)
data = obj.get("data")
if not data or "workerList" not in data:
return raw
workers = data["workerList"]
attendance = data.get("attendance", "")
worker_num = data.get("workerNum", 0)
checkin_num = data.get("checkinNum", 0)
# 最早打卡
checked_in = [w for w in workers if w.get("checkin")]
earliest = None
if checked_in:
checked_in.sort(key=lambda w: w["checkin"])
e = checked_in[0]
t = e["checkin"].split("T")[1][:5] if "T" in e["checkin"] else e["checkin"]
earliest = f"{e['name']} {t}({e.get('deptName', '')})"
# 未打卡/请假
un_checked = [
f"{w['name']}({w.get('checkinState', '未打卡')})"
for w in workers if not w.get("checkin")
]
# 各子部门打卡人数汇总
dept_stats = {}
for w in workers:
dept = w.get("deptName", "未知")
if dept not in dept_stats:
dept_stats[dept] = {"total": 0, "checked": 0}
dept_stats[dept]["total"] += 1
if w.get("checkin"):
dept_stats[dept]["checked"] += 1
summary = {
"出勤率": f"{attendance}%",
"总人数": worker_num,
"已打卡": checkin_num,
"最早到岗": earliest,
"未到岗": un_checked if un_checked else "全部已打卡",
"子部门": {k: f"{v['checked']}/{v['total']}" for k, v in dept_stats.items()},
}
return json.dumps(summary, ensure_ascii=False)
except Exception:
return raw
# 工具名 → 后处理函数
_RESULT_SUMMARIZERS = {
"get_checkin_app_dept": _summarize_checkin_dept,
}
# ── 通用请求执行器 ────────────────────────────────────────
_PATH_PARAM_RE = re.compile(r"\{(\w+)\}")
def _call_api(endpoint: dict, **kwargs) -> str:
method = endpoint["method"].upper()
headers = {**DEFAULT_HEADERS, **endpoint.get("headers", {})}
# "全部"部门:批量查询所有部门,合并摘要后返回
if endpoint["name"] == "get_checkin_app_dept" and kwargs.get("deptName") == "全部":
all_depts = ["养护部", "工程部", "环卫部", "办公室"]
combined = {}
for dept in all_depts:
try:
url = DEFAULT_BASE_URL.rstrip("/") + endpoint["path"]
resp = httpx.request(method, url, headers=headers, json={"deptName": dept}, timeout=DEFAULT_TIMEOUT)
resp.raise_for_status()
summary = _summarize_checkin_dept(resp.text)
combined[dept] = json.loads(summary)
except Exception as exc:
combined[dept] = f"查询失败: {exc}"
result = json.dumps(combined, ensure_ascii=False)
print(f"全部部门摘要: {result[:300]}...")
return result
url = DEFAULT_BASE_URL.rstrip("/") + endpoint["path"]
# 1) 路径参数替换:{user_id} → kwargs["user_id"]
path_params = set(_PATH_PARAM_RE.findall(url))
for p in path_params:
value = kwargs.pop(p, "")
url = url.replace(f"{{{p}}}", str(value))
# 2) 分流GET → query stringPOST/PUT/DELETE → JSON body
try:
if method == "GET":
resp = httpx.request(
method, url, headers=headers, params=kwargs, timeout=DEFAULT_TIMEOUT
)
else:
resp = httpx.request(
method, url, headers=headers, json=kwargs or None, timeout=DEFAULT_TIMEOUT
)
resp.raise_for_status()
result = resp.text
print(f"API 调用成功 ({endpoint['name']}): {result[:200]}...")
# 对特定工具的返回做摘要,减少 LLM 上下文消耗
summarizer = _RESULT_SUMMARIZERS.get(endpoint["name"])
if summarizer:
result = summarizer(result)
print(f"摘要后 ({endpoint['name']}): {result}")
return result
except Exception as exc:
return f"API 调用失败 ({endpoint['name']}): {exc}"
# ── 自动注册所有声明的端点 ────────────────────────────────
for _ep in API_ENDPOINTS:
def _make_handler(ep=_ep):
def handler(**kw) -> str:
return _call_api(ep, **kw)
return handler
tool_registry.register(
name=_ep["name"],
description=_ep["description"],
parameters=_ep["parameters"],
)(_make_handler())