2026-06-14 16:49:18 +08:00
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 企微IT智能服务台 — AI Wingman 服务(坐席智能副驾驶)
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 说明:复用 Dify 基础设施,使用独立的 Wingman Agent(Agent 2),
|
|
|
|
|
|
# 与员工端 AI(Agent 1)共用知识库但 system prompt 不同。
|
|
|
|
|
|
#
|
|
|
|
|
|
# 核心能力:
|
|
|
|
|
|
# 1. 生成 AI 草稿回复 — 基于对话上下文为坐席生成专业回复
|
|
|
|
|
|
# 2. 生成会话自动摘要 — 结单时自动提取问题/原因/解决方案
|
|
|
|
|
|
# 3. 生成自动标签建议 — 基于对话内容建议标签分类
|
|
|
|
|
|
#
|
|
|
|
|
|
# 降级策略:Wingman Agent 不可用时返回友好错误信息,不抛异常
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import logging
|
|
|
|
|
|
from typing import Any, Dict, List, Optional
|
|
|
|
|
|
|
|
|
|
|
|
import httpx
|
|
|
|
|
|
|
|
|
|
|
|
from app.config import settings
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class WingmanService:
|
|
|
|
|
|
"""AI Wingman 服务 — 坐席智能副驾驶。
|
|
|
|
|
|
|
|
|
|
|
|
复用 Dify 基础设施,使用独立的 Wingman Agent(Agent 2),
|
|
|
|
|
|
与员工端 AI(Agent 1)共用知识库但 system prompt 不同。
|
|
|
|
|
|
|
|
|
|
|
|
三个核心方法使用不同的 system prompt:
|
|
|
|
|
|
- 草稿生成:生成坐席可采纳的专业回复
|
|
|
|
|
|
- 摘要生成:提取结构化的会话摘要
|
|
|
|
|
|
- 标签建议:建议标签分类和优先级
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# System Prompt 定义
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
_DRAFT_SYSTEM_PROMPT: str = (
|
|
|
|
|
|
"你是一个IT服务坐席助手,基于对话上下文为坐席生成专业、准确的回复草稿。"
|
|
|
|
|
|
"直接输出回复内容,不要解释。"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_SUMMARY_SYSTEM_PROMPT: str = (
|
|
|
|
|
|
"你是一个IT服务分析助手,基于完整对话生成结构化摘要,"
|
|
|
|
|
|
"包含:问题、原因、解决方案。以JSON格式输出。"
|
|
|
|
|
|
"输出格式:{\"problem\": \"问题描述\", \"cause\": \"原因\", \"solution\": \"解决方案\"}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
_TAGS_SYSTEM_PROMPT: str = (
|
|
|
|
|
|
"你是一个IT服务分类助手,基于对话内容建议标签分类。以JSON格式输出。"
|
|
|
|
|
|
"输出格式:{\"suggested_tags\": [\"标签1\", \"标签2\"], \"category\": \"分类\", \"priority\": \"low/medium/high\"}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-09 11:47:16 +08:00
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 知识建议生成专用 Prompt(Tier0 / T03 — 通道 A/B 复用)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
_KNOWLEDGE_SUGGESTION_PROMPT: str = (
|
|
|
|
|
|
"你是一个IT知识库优化助手,基于对话上下文分析知识库的不足,"
|
|
|
|
|
|
"生成结构化的知识库优化建议。\n\n"
|
|
|
|
|
|
"分析以下对话,判断是否需要新增FAQ或更新已有知识条目。\n"
|
|
|
|
|
|
"如果AI回复被标记为无用,生成更新建议;如果AI无法解决需转人工,生成新增FAQ建议。\n\n"
|
|
|
|
|
|
"必须以JSON格式输出,包含以下字段:\n"
|
|
|
|
|
|
"- suggestion_type: 建议类型,\"new_faq\" 或 \"update\"\n"
|
|
|
|
|
|
"- title: 问题标题(简洁明了)\n"
|
|
|
|
|
|
"- content: 答案内容(分步骤、可操作)\n"
|
|
|
|
|
|
"- category: 分类,只能是 硬件/软件/网络/安全/账号/其他 之一\n"
|
|
|
|
|
|
"- tags: 标签列表,如 [\"VPN\", \"连接\"]\n"
|
|
|
|
|
|
"- confidence: 你对这个建议的信心度,0.0-1.0之间\n"
|
|
|
|
|
|
"- issue: 对应的Neo4j图问题节点名称,如\"VPN问题\"\n"
|
|
|
|
|
|
"- action: 对应的Neo4j图动作节点名称,如\"VPN连接修复\"\n"
|
|
|
|
|
|
"- relation_type: 图关系类型,\"LEADS_TO\" 或 \"RELATES_TO\"\n"
|
|
|
|
|
|
"- parent_issue: 父问题名称(如果没有则为空字符串)\n\n"
|
|
|
|
|
|
"输出格式示例:\n"
|
|
|
|
|
|
"{\"suggestion_type\": \"new_faq\", \"title\": \"VPN连不上怎么办\", "
|
|
|
|
|
|
"\"content\": \"1.检查网络连接 2.重启VPN客户端 3.联系IT支持\", "
|
|
|
|
|
|
"\"category\": \"网络\", \"tags\": [\"VPN\", \"连接\"], "
|
|
|
|
|
|
"\"confidence\": 0.86, \"issue\": \"VPN问题\", "
|
|
|
|
|
|
"\"action\": \"VPN连接修复\", \"relation_type\": \"LEADS_TO\", "
|
|
|
|
|
|
"\"parent_issue\": \"网络问题\"}"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
def __init__(self):
|
|
|
|
|
|
"""初始化 Wingman 服务。
|
|
|
|
|
|
|
|
|
|
|
|
从配置读取 Wingman Agent 的 API 地址和认证信息。
|
|
|
|
|
|
独立于 AIService,使用自己的 httpx 客户端。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# Wingman Agent 专用 API 端点
|
|
|
|
|
|
self.api_url = settings.dify_wingman_api_url
|
|
|
|
|
|
# Wingman Agent API Key
|
|
|
|
|
|
self.api_key = settings.dify_wingman_api_key
|
|
|
|
|
|
# 请求超时(秒)
|
|
|
|
|
|
self.timeout = settings.dify_wingman_timeout
|
|
|
|
|
|
|
|
|
|
|
|
# httpx 异步客户端(复用连接池)
|
|
|
|
|
|
self._client: Optional[httpx.AsyncClient] = None
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_client(self) -> httpx.AsyncClient:
|
|
|
|
|
|
"""获取或创建 httpx 异步客户端(懒加载)。
|
|
|
|
|
|
|
|
|
|
|
|
复用连接池,避免每次请求都创建新连接。
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
httpx.AsyncClient: 异步 HTTP 客户端实例
|
|
|
|
|
|
"""
|
|
|
|
|
|
if self._client is None or self._client.is_closed:
|
|
|
|
|
|
self._client = httpx.AsyncClient(
|
|
|
|
|
|
timeout=httpx.Timeout(self.timeout),
|
|
|
|
|
|
headers={
|
|
|
|
|
|
"Authorization": f"Bearer {self.api_key}",
|
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
|
|
|
|
|
return self._client
|
|
|
|
|
|
|
|
|
|
|
|
async def close(self):
|
|
|
|
|
|
"""关闭 httpx 客户端,释放连接池资源。"""
|
|
|
|
|
|
if self._client and not self._client.is_closed:
|
|
|
|
|
|
await self._client.aclose()
|
|
|
|
|
|
self._client = None
|
|
|
|
|
|
logger.debug("WingmanService httpx client closed")
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 核心方法 1:生成 AI 草稿回复
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def generate_draft(
|
|
|
|
|
|
self,
|
|
|
|
|
|
conversation_id: str,
|
|
|
|
|
|
messages: List[Dict[str, Any]],
|
|
|
|
|
|
db: Any = None,
|
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
|
"""生成 AI 草稿回复。
|
|
|
|
|
|
|
|
|
|
|
|
传入当前会话的完整消息历史,让 Wingman Agent 基于上下文
|
|
|
|
|
|
生成坐席可以采纳的草稿回复。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
conversation_id: 会话ID
|
|
|
|
|
|
messages: 会话消息历史列表,每条消息包含 sender_type/content 等
|
|
|
|
|
|
db: 数据库会话(可选,当前未使用)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: {
|
|
|
|
|
|
"content": str, # 草稿内容
|
|
|
|
|
|
"confidence": float, # 置信度(0-1)
|
|
|
|
|
|
"reasoning": str, # 生成推理说明
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 构建对话上下文消息列表
|
|
|
|
|
|
context_messages = self._build_context_messages(
|
|
|
|
|
|
messages, self._DRAFT_SYSTEM_PROMPT
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await self._call_wingman_api(context_messages)
|
|
|
|
|
|
|
|
|
|
|
|
if result is None:
|
|
|
|
|
|
return {
|
|
|
|
|
|
"content": "",
|
|
|
|
|
|
"confidence": 0.0,
|
|
|
|
|
|
"reasoning": "Wingman 服务暂不可用",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reply_content = result
|
|
|
|
|
|
|
|
|
|
|
|
# 基于回复长度和内容质量估算置信度
|
|
|
|
|
|
confidence = self._estimate_confidence(reply_content)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"content": reply_content,
|
|
|
|
|
|
"confidence": confidence,
|
|
|
|
|
|
"reasoning": f"基于最近 {len(messages)} 条对话上下文生成",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Wingman 草稿生成失败: {e}")
|
|
|
|
|
|
return {
|
|
|
|
|
|
"content": "",
|
|
|
|
|
|
"confidence": 0.0,
|
|
|
|
|
|
"reasoning": f"AI 服务异常: {str(e)}",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 核心方法 2:生成会话自动摘要
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def generate_summary(
|
|
|
|
|
|
self,
|
|
|
|
|
|
conversation_id: str,
|
|
|
|
|
|
messages: List[Dict[str, Any]],
|
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
|
"""生成会话自动摘要。
|
|
|
|
|
|
|
|
|
|
|
|
基于完整对话生成结构化摘要,包含问题、原因、解决方案。
|
|
|
|
|
|
结单时自动调用。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
conversation_id: 会话ID
|
|
|
|
|
|
messages: 会话消息历史列表
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: {
|
|
|
|
|
|
"problem": str, # 问题描述
|
|
|
|
|
|
"cause": str, # 原因分析
|
|
|
|
|
|
"solution": str, # 解决方案
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
context_messages = self._build_context_messages(
|
|
|
|
|
|
messages, self._SUMMARY_SYSTEM_PROMPT
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 默认摘要(降级时使用)
|
|
|
|
|
|
default_summary = {
|
|
|
|
|
|
"problem": "无法自动生成摘要",
|
|
|
|
|
|
"cause": "",
|
|
|
|
|
|
"solution": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await self._call_wingman_api(context_messages)
|
|
|
|
|
|
|
|
|
|
|
|
if result is None:
|
|
|
|
|
|
return default_summary
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试解析 JSON 格式的摘要
|
|
|
|
|
|
parsed = self._parse_json_response(result, default_summary)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"problem": parsed.get("problem", default_summary["problem"]),
|
|
|
|
|
|
"cause": parsed.get("cause", default_summary["cause"]),
|
|
|
|
|
|
"solution": parsed.get("solution", default_summary["solution"]),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Wingman 摘要生成失败: {e}")
|
|
|
|
|
|
return default_summary
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 核心方法 3:生成自动标签建议
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def suggest_tags(
|
|
|
|
|
|
self,
|
|
|
|
|
|
conversation_id: str,
|
|
|
|
|
|
messages: List[Dict[str, Any]],
|
|
|
|
|
|
existing_tags: Dict[str, Any] = None,
|
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
|
"""生成自动标签建议。
|
|
|
|
|
|
|
|
|
|
|
|
基于对话内容建议标签分类,包含标签列表、分类和优先级。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
conversation_id: 会话ID
|
|
|
|
|
|
messages: 会话消息历史列表
|
|
|
|
|
|
existing_tags: 已有标签(可选,用于避免重复建议)
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: {
|
|
|
|
|
|
"suggested_tags": list[str], # 建议标签列表
|
|
|
|
|
|
"category": str, # 分类
|
|
|
|
|
|
"priority": str, # 优先级: low/medium/high
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
context_messages = self._build_context_messages(
|
|
|
|
|
|
messages, self._TAGS_SYSTEM_PROMPT
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 默认标签建议(降级时使用)
|
|
|
|
|
|
default_tags = {
|
|
|
|
|
|
"suggested_tags": [],
|
|
|
|
|
|
"category": "",
|
|
|
|
|
|
"priority": "medium",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await self._call_wingman_api(context_messages)
|
|
|
|
|
|
|
|
|
|
|
|
if result is None:
|
|
|
|
|
|
return default_tags
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试解析 JSON 格式的标签建议
|
|
|
|
|
|
parsed = self._parse_json_response(result, default_tags)
|
|
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
"suggested_tags": parsed.get("suggested_tags", []),
|
|
|
|
|
|
"category": parsed.get("category", ""),
|
|
|
|
|
|
"priority": parsed.get("priority", "medium"),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Wingman 标签建议失败: {e}")
|
|
|
|
|
|
return default_tags
|
|
|
|
|
|
|
2026-07-09 11:47:16 +08:00
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 核心方法 4:生成知识库优化建议(Tier0 / T03 — 复用现有范式)
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
async def generate_knowledge_suggestion(
|
|
|
|
|
|
self,
|
|
|
|
|
|
context_messages: List[Dict[str, Any]],
|
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
|
"""生成知识库优化建议(用于知识库自动迭代)。
|
|
|
|
|
|
|
|
|
|
|
|
传入对话上下文,让 Wingman Agent 分析并生成结构化的知识库优化建议。
|
|
|
|
|
|
复用 _build_context_messages + _call_wingman_api + _parse_json_response 范式。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
context_messages: 对话消息历史列表
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: {
|
|
|
|
|
|
"suggestion_type": str, # "new_faq" / "update"
|
|
|
|
|
|
"title": str, # 问题标题
|
|
|
|
|
|
"content": str, # 答案内容
|
|
|
|
|
|
"category": str, # 分类
|
|
|
|
|
|
"tags": list[str], # 标签列表
|
|
|
|
|
|
"confidence": float, # 置信度(0.0-1.0)
|
|
|
|
|
|
"issue": str, # 图节点:问题名称
|
|
|
|
|
|
"action": str, # 图节点:动作名称
|
|
|
|
|
|
"relation_type": str, # 图关系类型
|
|
|
|
|
|
"parent_issue": str, # 父 Issue 名称
|
|
|
|
|
|
}
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 构建对话上下文消息列表(使用知识建议专用 Prompt)
|
|
|
|
|
|
context = self._build_context_messages(
|
|
|
|
|
|
context_messages, self._KNOWLEDGE_SUGGESTION_PROMPT
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# 默认建议(降级时使用)
|
|
|
|
|
|
default_suggestion: Dict[str, Any] = {
|
|
|
|
|
|
"suggestion_type": "new_faq",
|
|
|
|
|
|
"title": "",
|
|
|
|
|
|
"content": "",
|
|
|
|
|
|
"category": "其他",
|
|
|
|
|
|
"tags": [],
|
|
|
|
|
|
"confidence": 0.0,
|
|
|
|
|
|
"issue": "",
|
|
|
|
|
|
"action": "",
|
|
|
|
|
|
"relation_type": "LEADS_TO",
|
|
|
|
|
|
"parent_issue": "",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
result = await self._call_wingman_api(context)
|
|
|
|
|
|
|
|
|
|
|
|
if result is None:
|
|
|
|
|
|
logger.warning("Wingman 知识建议生成失败(API 返回 None)")
|
|
|
|
|
|
return default_suggestion
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试解析 JSON 格式的建议
|
|
|
|
|
|
parsed = self._parse_json_response(result, default_suggestion)
|
|
|
|
|
|
|
|
|
|
|
|
# 规范化字段
|
|
|
|
|
|
suggestion: Dict[str, Any] = {
|
|
|
|
|
|
"suggestion_type": parsed.get(
|
|
|
|
|
|
"suggestion_type", default_suggestion["suggestion_type"]
|
|
|
|
|
|
),
|
|
|
|
|
|
"title": parsed.get("title", default_suggestion["title"]),
|
|
|
|
|
|
"content": parsed.get("content", default_suggestion["content"]),
|
|
|
|
|
|
"category": parsed.get("category", default_suggestion["category"]),
|
|
|
|
|
|
"tags": parsed.get("tags", default_suggestion["tags"]),
|
|
|
|
|
|
"confidence": float(parsed.get("confidence", 0.0)),
|
|
|
|
|
|
"issue": parsed.get("issue", default_suggestion["issue"]),
|
|
|
|
|
|
"action": parsed.get("action", default_suggestion["action"]),
|
|
|
|
|
|
"relation_type": parsed.get(
|
|
|
|
|
|
"relation_type", default_suggestion["relation_type"]
|
|
|
|
|
|
),
|
|
|
|
|
|
"parent_issue": parsed.get(
|
|
|
|
|
|
"parent_issue", default_suggestion["parent_issue"]
|
|
|
|
|
|
),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
# 如果 Dify 未返回 confidence,使用启发式估算
|
|
|
|
|
|
if suggestion["confidence"] == 0.0:
|
|
|
|
|
|
suggestion["confidence"] = self._estimate_confidence(
|
|
|
|
|
|
suggestion["content"]
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
|
f"知识建议生成完成: type={suggestion['suggestion_type']}, "
|
|
|
|
|
|
f"title={suggestion['title'][:50]}, "
|
|
|
|
|
|
f"confidence={suggestion['confidence']}"
|
|
|
|
|
|
)
|
|
|
|
|
|
return suggestion
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Wingman 知识建议生成异常: {e}")
|
|
|
|
|
|
return default_suggestion
|
|
|
|
|
|
|
2026-06-14 16:49:18 +08:00
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
# 内部方法
|
|
|
|
|
|
# --------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
def _build_context_messages(
|
|
|
|
|
|
self,
|
|
|
|
|
|
messages: List[Dict[str, Any]],
|
|
|
|
|
|
system_prompt: str,
|
|
|
|
|
|
) -> List[Dict[str, str]]:
|
|
|
|
|
|
"""构建发送给 Wingman Agent 的消息列表。
|
|
|
|
|
|
|
|
|
|
|
|
将数据库中的消息历史转换为 OpenAI Chat Completions 格式的
|
|
|
|
|
|
messages 列表,包含 system prompt 和对话上下文。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
messages: 数据库消息列表
|
|
|
|
|
|
system_prompt: 当前场景的 system prompt
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
List[Dict]: OpenAI 格式的消息列表
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 构建上下文消息列表
|
|
|
|
|
|
context: List[Dict[str, str]] = [
|
|
|
|
|
|
{"role": "system", "content": system_prompt}
|
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
|
|
# 角色映射:数据库 sender_type → OpenAI role
|
|
|
|
|
|
role_map = {
|
|
|
|
|
|
"employee": "user", # 员工消息 → user
|
|
|
|
|
|
"agent": "assistant", # 坐席消息 → assistant
|
|
|
|
|
|
"ai": "assistant", # AI消息 → assistant
|
|
|
|
|
|
"system": "system", # 系统消息 → system
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
for msg in messages:
|
|
|
|
|
|
role = role_map.get(msg.get("sender_type", ""), "user")
|
|
|
|
|
|
content = msg.get("content", "")
|
|
|
|
|
|
if content:
|
|
|
|
|
|
# 跳过系统消息(已有 system prompt)
|
|
|
|
|
|
if msg.get("sender_type") == "system":
|
|
|
|
|
|
continue
|
|
|
|
|
|
context.append({"role": role, "content": content})
|
|
|
|
|
|
|
|
|
|
|
|
return context
|
|
|
|
|
|
|
|
|
|
|
|
async def _call_wingman_api(
|
|
|
|
|
|
self,
|
|
|
|
|
|
context_messages: List[Dict[str, str]],
|
|
|
|
|
|
) -> Optional[str]:
|
|
|
|
|
|
"""调用 Wingman Agent API(非流式)。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
context_messages: OpenAI 格式的消息列表
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Optional[str]: AI 回复内容,失败时返回 None
|
|
|
|
|
|
"""
|
|
|
|
|
|
payload = {
|
|
|
|
|
|
"model": "Chat",
|
|
|
|
|
|
"messages": context_messages,
|
|
|
|
|
|
"stream": False,
|
|
|
|
|
|
"temperature": 0.3, # 适中的温度,保证准确性同时有一定灵活性
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
client = await self._get_client()
|
|
|
|
|
|
logger.info(f"调用 Wingman API: messages_count={len(context_messages)}")
|
|
|
|
|
|
response = await client.post(self.api_url, json=payload)
|
|
|
|
|
|
response.raise_for_status()
|
|
|
|
|
|
data = response.json()
|
|
|
|
|
|
|
|
|
|
|
|
# 解析 OpenAI 兼容格式的返回
|
|
|
|
|
|
choices = data.get("choices", [])
|
|
|
|
|
|
if not choices:
|
|
|
|
|
|
logger.warning("Wingman API 返回空 choices")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
reply_content = choices[0]["message"]["content"]
|
|
|
|
|
|
logger.info(f"Wingman API 返回: content_length={len(reply_content)}")
|
|
|
|
|
|
|
|
|
|
|
|
return reply_content
|
|
|
|
|
|
|
|
|
|
|
|
except httpx.TimeoutException:
|
|
|
|
|
|
logger.error("Wingman API 超时")
|
|
|
|
|
|
return None
|
|
|
|
|
|
except httpx.HTTPStatusError as e:
|
|
|
|
|
|
logger.error(f"Wingman API HTTP 错误: status={e.response.status_code}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.error(f"Wingman API 调用失败: {e}")
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_json_response(
|
|
|
|
|
|
self,
|
|
|
|
|
|
content: str,
|
|
|
|
|
|
default: Dict[str, Any],
|
|
|
|
|
|
) -> Dict[str, Any]:
|
|
|
|
|
|
"""解析 AI 返回的 JSON 内容。
|
|
|
|
|
|
|
|
|
|
|
|
Wingman Agent 可能返回带 markdown 代码块的 JSON,
|
|
|
|
|
|
也可能返回纯 JSON。此方法尝试多种解析方式。
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
content: AI 返回的原始文本
|
|
|
|
|
|
default: 解析失败时的默认值
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
Dict: 解析后的字典,失败时返回默认值
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not content:
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试 1:直接解析
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(content)
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试 2:提取 markdown 代码块中的 JSON
|
|
|
|
|
|
# AI 可能返回 ```json ... ``` 格式
|
|
|
|
|
|
import re
|
|
|
|
|
|
json_match = re.search(r'```(?:json)?\s*\n?(.*?)\n?```', content, re.DOTALL)
|
|
|
|
|
|
if json_match:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(json_match.group(1).strip())
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# 尝试 3:查找第一个 { 到最后一个 } 之间的内容
|
|
|
|
|
|
start = content.find('{')
|
|
|
|
|
|
end = content.rfind('}')
|
|
|
|
|
|
if start != -1 and end != -1 and end > start:
|
|
|
|
|
|
try:
|
|
|
|
|
|
return json.loads(content[start:end + 1])
|
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
logger.warning(f"Wingman JSON 解析失败,使用默认值: {content[:200]}")
|
|
|
|
|
|
return default
|
|
|
|
|
|
|
|
|
|
|
|
def _estimate_confidence(self, content: str) -> float:
|
|
|
|
|
|
"""估算 AI 草稿回复的置信度。
|
|
|
|
|
|
|
|
|
|
|
|
基于回复长度和内容特征估算一个粗略的置信度值。
|
|
|
|
|
|
- 回复过短(< 10 字符):低置信度
|
|
|
|
|
|
- 回复包含不确定措辞:降低置信度
|
|
|
|
|
|
- 回复长度适中、内容具体:高置信度
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
content: AI 回复内容
|
|
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
|
float: 置信度(0.0 - 1.0)
|
|
|
|
|
|
"""
|
|
|
|
|
|
if not content or len(content.strip()) < 5:
|
|
|
|
|
|
return 0.2
|
|
|
|
|
|
|
|
|
|
|
|
confidence = 0.8 # 基础置信度
|
|
|
|
|
|
|
|
|
|
|
|
# 回复过短降低置信度
|
|
|
|
|
|
if len(content) < 10:
|
|
|
|
|
|
confidence -= 0.3
|
|
|
|
|
|
elif len(content) < 30:
|
|
|
|
|
|
confidence -= 0.1
|
|
|
|
|
|
|
|
|
|
|
|
# 包含不确定措辞降低置信度
|
|
|
|
|
|
uncertain_phrases = ["可能", "大概", "也许", "不确定", "建议您"]
|
|
|
|
|
|
for phrase in uncertain_phrases:
|
|
|
|
|
|
if phrase in content:
|
|
|
|
|
|
confidence -= 0.05
|
|
|
|
|
|
|
|
|
|
|
|
# 包含具体步骤或链接提高置信度
|
|
|
|
|
|
confident_phrases = ["步骤", "请按以下", "点击", "打开", "http"]
|
|
|
|
|
|
for phrase in confident_phrases:
|
|
|
|
|
|
if phrase in content:
|
|
|
|
|
|
confidence += 0.05
|
|
|
|
|
|
|
|
|
|
|
|
# 限制在 0.0 - 1.0 范围内
|
|
|
|
|
|
return max(0.0, min(1.0, confidence))
|