Files
DPM/backend/app/llm.py
T
Pine 18d28be943 feat: 添加后端部署和API基本检索的Docker Compose配置
- 引入新的`docker-compose.yml`文件,便于后端和MQTT代理(EMQX)的部署。
- 更新多个组件中的API基本检索,使用函数进行动态解析。
- 加强了多个组件中API调用的错误处理和日志记录。
- 优化了AI聊天面板离线场景的回退响应。
- 更新DataScreen和MediaScreen组件中的数据显示和统计,以反映准确的指标。
- 重构MQTT连接逻辑,以支持动态凭证和客户端ID。
2026-08-18 06:50:50 +08:00

210 lines
7.5 KiB
Python

# -*- coding: utf-8 -*-
"""AI 对话引擎 —— 阿里云 DashScope(通义千问)+ 函数调用
工具调用执行后通过 MQTT 广播,所有大屏同步响应
DashScope 不可用时自动回退本地规则引擎(离线兜底)
"""
import json
import logging
import ssl
import urllib.error
import urllib.request
import certifi
from .config import settings
log = logging.getLogger("dpm.llm")
# macOS 系统 Python 无系统 CA,使用 certifi 提供的根证书
_SSL_CTX = ssl.create_default_context(cafile=certifi.where())
SYSTEM_PROMPT = (
"你是「昆明市大学生创业园 · OPC 智能园区」的 AI 智能助手,运行在大屏展播系统上。"
"园区提供空间、孵化、融资、政策、资源、AI 赋能、综合服务七位一体服务。"
"回答园区相关问题时,必须严格依据【园区知识库】中的标准资料口径回答,"
"不得使用资料外的数字;涉及运营数据时请区分口径(官方统计/省级绩效累计/AI平台指标),"
"并在需要时调用 get_park_overview 工具获取实时准确数据。"
"当用户要求「切换页面 / 控制播放 / 展示卡片 / 弹出通知」时,必须调用对应工具;"
"其余园区相关问题用简洁、专业的中文回答,可适当使用列表。"
)
# 暴露给大模型的工具定义(执行时经 MQTT 广播到前端)
TOOLS = [
{
"type": "function",
"function": {
"name": "navigate_page",
"description": "切换大屏展示页面(数据大屏 / 数字孪生 / AI 助手 / 媒体轮播)",
"parameters": {
"type": "object",
"properties": {
"page": {"type": "string", "enum": ["/", "/twin", "/ai", "/screen"],
"description": "目标页面路径"},
},
"required": ["page"],
},
},
},
{
"type": "function",
"function": {
"name": "media_control",
"description": "控制媒体播放(播放/暂停/下一项/上一项)",
"parameters": {
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["play", "pause", "next", "prev"]},
},
"required": ["action"],
},
},
},
{
"type": "function",
"function": {
"name": "show_card",
"description": "在大屏上展示信息卡片(企业分布 / 分区介绍 / 园区总览 / 自定义内容)",
"parameters": {
"type": "object",
"properties": {
"card": {"type": "string", "enum": ["companies", "zones", "overview", "custom"]},
"title": {"type": "string"},
"content": {"type": "string"},
},
"required": ["card"],
},
},
},
{
"type": "function",
"function": {
"name": "send_alert",
"description": "在大屏上弹出通知提示",
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"},
},
"required": ["title", "content"],
},
},
},
]
_TOOL_MAP = {
"navigate_page": "navigate",
"media_control": "control",
"show_card": "show_card",
"send_alert": "alert",
}
def _chat_once(messages, with_tools=True):
payload = {
"model": settings.LLM_MODEL,
"messages": messages,
"temperature": 0.6,
}
if with_tools:
payload["tools"] = TOOLS
req = urllib.request.Request(
f"{settings.LLM_BASE_URL}/chat/completions",
data=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}",
},
)
with urllib.request.urlopen(req, timeout=60, context=_SSL_CTX) as resp:
return json.loads(resp.read().decode("utf-8"))
def _fallback(messages):
"""DashScope 不可用时回退本地规则引擎"""
from .ai_tools import run_chat as rule_run
result = rule_run(messages)
# 规则引擎产出的工具同样执行(MQTT 广播),并汇总发布结果
_, all_ok = _exec_all(result.get("tools", []))
result["model"] = "rule-engine"
result["mqtt_published"] = all_ok
return result
def _exec_all(tools):
"""执行工具列表,返回 (results, all_ok)"""
from .ai_tools import run_tools
results, all_ok = run_tools(tools)
return results, all_ok
def run_chat(messages):
"""入口:{reply, tools, model}
tools 已在后端执行(MQTT 广播),返回值供请求端本地同步执行"""
if not settings.DASHSCOPE_API_KEY:
log.warning("未配置 DASHSCOPE_API_KEY,使用本地规则引擎")
return _fallback(messages)
# 注入园区知识库摘要(标准资料口径),确保回答准确
system = SYSTEM_PROMPT
try:
from .rag import brief
kb = brief()
if kb:
system += f"\n\n【园区知识库】\n{kb}"
except Exception as e: # noqa: BLE001
log.warning("llm: 知识库注入失败(仅用基础提示词): %s", e)
msgs = [{"role": "system", "content": system}] + [
{"role": m.get("role") == "me" and "user" or m.get("role", "user"), "content": m.get("content", "")}
for m in (messages or [])
]
try:
data = _chat_once(msgs)
choice = data["choices"][0]["message"]
reply = choice.get("content") or ""
tool_calls = choice.get("tool_calls") or []
executed = []
if tool_calls:
for tc in tool_calls:
fn = tc.get("function", {})
name = fn.get("name", "")
try:
args = json.loads(fn.get("arguments") or "{}")
except json.JSONDecodeError:
args = {}
tool = {"type": _TOOL_MAP.get(name, name), "params": args}
executed.append(tool)
msgs.append({"role": "assistant", "content": None, "tool_calls": tool_calls})
msgs.append({
"role": "tool",
"tool_call_id": tc.get("id", ""),
"content": json.dumps({"ok": True}, ensure_ascii=False),
})
# 工具执行(MQTT 广播),并汇总发布结果
_, all_ok = _exec_all(executed)
# 二次调用:携带工具结果生成最终回复
try:
data2 = _chat_once(msgs, with_tools=False)
reply = data2["choices"][0]["message"].get("content") or reply
except Exception as e: # noqa: BLE001
log.warning("LLM 二次调用失败(保留工具回复): %s", e)
else:
all_ok = True
if not reply:
reply = "已完成操作。您还可以让我切换页面、控制播放或展示园区卡片。"
return {"reply": reply, "tools": executed, "model": settings.LLM_MODEL, "mqtt_published": all_ok}
except urllib.error.HTTPError as e:
log.warning("DashScope HTTP %s: %s", e.code, e.read()[:300])
except Exception as e: # noqa: BLE001
log.warning("DashScope 调用失败,回退规则引擎: %s", e)
return _fallback(messages)