Files
chat_app/backend/app/routes/glm_routes.py
2025-12-15 17:44:35 +08:00

461 lines
20 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.
from fastapi import APIRouter, HTTPException, Depends
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
import httpx
import os
import logging
from app.services.glm_service import get_glm_service, GLMService
from app.models.base import BaseOut
# 创建API路由,使用/glm前缀
router = APIRouter(prefix="/glm", tags=["GLM聊天"])
logger = logging.getLogger(__name__)
# 数据模型
class Message(BaseModel):
role: str
content: str
class ChatRequest(BaseModel):
message: str
conversation_id: Optional[str] = None
enhanced_prompt: Optional[str] = None # 前端生成的完整提示词
customer_tags: Optional[List[Dict[str, Any]]] = None # 客户标签
message_history: Optional[List[Dict[str, Any]]] = None # 企业微信历史消息
workflow_context: Optional[Dict[str, Any]] = None # 工作流上下文
class ChatResponse(BaseModel):
response: str
conversation_id: str
class RoleConfig(BaseModel):
name: str
description: str
skills: str # 多行文本字符串
restrictions: str # 多行文本字符串
# 默认角色配置(使用字符串格式)
DEFAULT_ROLE_CONFIG = RoleConfig(
name="兜兜",
description="你是兜兜,1989年出生,有一个5岁的女儿(2018年出生)。你是一位对用户负责、选品严格的宝妈客服。当用户给你发来疑问时,你总能第一时间回复,并以亲切温暖的风格与用户交流。",
skills="亲切温暖的沟通\n轻松友好的语气\n简洁明了的描述\n亲昵且口语化的用词\n乱码话术重复问询",
restrictions="回复必须基于提供的知识库中的内容\n每条回复应保持在80字以内,使用口语化语言\n回复中可以适当加入表情符号,但不要过度使用\n只讨论与宝妈相关的主题,如育儿、选品、团购等\n始终以用户的需求和满意度为优先",
)
# 全局变量存储对话历史(暂时保持内存存储,根据用户要求不持久化)
conversations = {}
def build_system_prompt(role_config: dict) -> str:
"""构建系统提示词
Args:
role_config: 角色配置字典,包含name, description, skills, restrictions字段
Returns:
系统提示词字符串
"""
# 解析技能字符串为列表
skills_list = [
skill.strip() for skill in role_config["skills"].split("\n") if skill.strip()
]
restrictions_list = [
restriction.strip()
for restriction in role_config["restrictions"].split("\n")
if restriction.strip()
]
# 构建技能部分
skills_section = ""
for i, skill in enumerate(skills_list, 1):
skills_section += f"\n### 技能 {i}: {skill}"
# 添加技能描述(这里可以根据技能名称添加不同的描述)
if "亲切温暖" in skill:
skills_section += '\n- **风格**:你的风格非常亲切,像是一个知心朋友在和大家聊天,给人一种温暖的感觉。你经常使用"宝"、"美妈"等亲昵的称呼,拉近了与用户的距离。'
skills_section += "\n- **耐心细致**:你总是耐心解答用户的问题,尤其是对于新用户或不熟悉流程的用户,你会反复提醒他们查看群公告,保持安静等待开团。"
skills_section += "\n- **负责靠谱**:你给人一种非常靠谱的感觉,尤其是在售后问题上,你会主动承担责任,承诺包处理满意,给用户很强的信任感。"
elif "轻松友好" in skill:
skills_section += '\n- **轻松友好**:你的语气非常轻松,像是在和朋友聊天,不会给人压力。比如"不着急哦"、"爱你!"等表达方式,让人感到轻松愉快。'
skills_section += '\n- **安抚性**:当用户有疑问或遇到问题时,你的语气总是充满安抚性,比如"美妈不用担心"、"兜兜来处理哈",给人一种安心感。'
skills_section += '\n- **鼓励性**:你经常用"不着急"、"等开团的时候会有统一说明"等话语,鼓励用户耐心等待,保持积极的心态。'
elif "简洁明了" in skill:
skills_section += '\n- **简洁明了**:你的描述非常简洁,直接回答用户的问题,不会绕弯子。比如"贝亲是咱们深度合作品牌,经常上团的",直接给出了明确的答案。'
skills_section += '\n- **细节丰富**:在回答问题时,你会提供一些细节信息,比如"一般2天左右就会来"、"奶粉类的,贝亲,世喜,赫根,等常见品牌都有",让用户对情况有更清晰的了解。'
skills_section += '\n- **引导性**:你经常引导用户查看群公告,保持安静等待开团,比如"不熟悉的可以看一下群公告哦"、"现在安静的等开团就行啦",帮助用户更好地参与活动。'
elif "亲昵且口语化" in skill:
skills_section += '\n- **亲昵称呼**:你经常使用"宝"、"美妈"、"大家"等亲昵的称呼,拉近与用户的距离,给人一种亲切感。'
skills_section += '\n- **口语化**:你的用词非常口语化,像是日常聊天,比如"不着急哦"、"爱你!"、"兜兜来处理哈",给人一种轻松自然的感觉,称呼自己为"兜兜"或者"我"。'
skills_section += '\n- **积极正面**:你的用词总是积极正面的,比如"不用担心"、"包处理满意"、"很快"、"常见品牌都有",给用户传递了积极的信息。'
elif "乱码话术" in skill:
skills_section += '\n- **确认问题**:用户说的看不懂的情况来进行再次确认,比如 "45615465" "分隔符规划规范的" "8分隔符公司的"等跟常规问题没关联的话术来重复确认,例如:"宝看不懂你说的是什么啊,能在具体点嘛"'
else:
skills_section += f"\n- **{skill}**:请根据这个技能特点来回答问题。"
# 构建限制部分
restrictions_section = "\n".join(
[f"- {restriction}" for restriction in restrictions_list]
)
prompt = f"""# 角色
{role_config["description"]}
## 技能{skills_section}
## 限制
{restrictions_section}
请按照以上角色设定和限制来回答用户的问题。"""
return prompt
def get_deepseek_api_key() -> str:
"""获取DeepSeek API密钥"""
api_key = os.getenv("DEEPSEEK_API_KEY", "your-deepseek-api-key")
return api_key
def is_api_key_configured() -> bool:
"""检查API密钥是否已配置"""
api_key = get_deepseek_api_key()
return api_key != "your-deepseek-api-key"
async def call_deepseek_api(messages: List[dict]) -> str:
"""调用DeepSeek API"""
# 直接使用环境变量获取API密钥
api_key = get_deepseek_api_key()
if not is_api_key_configured():
# 模拟响应用于演示
return "宝,这个问题兜兜需要查一下资料哦,稍等哈~ 😊"
url = "https://api.deepseek.com/v1/chat/completions"
headers = {"Content-Type": "application/json", "Authorization": f"Bearer {api_key}"}
data = {
"model": "deepseek-chat",
"messages": messages,
"stream": False,
"max_tokens": 500,
}
try:
async with httpx.AsyncClient() as client:
response = await client.post(url, headers=headers, json=data, timeout=30.0)
response.raise_for_status()
result = response.json()
return result["choices"][0]["message"]["content"]
except Exception as e:
raise HTTPException(status_code=500, detail=f"API调用失败: {str(e)}")
@router.get("/")
async def root():
return {"message": "DeepSeek AI Chat Assistant API"}
@router.get("/role", response_model=BaseOut)
async def get_role_config(service: GLMService = Depends(get_glm_service)):
"""获取当前角色配置"""
try:
role = await service.get_current_role()
return BaseOut(code=200, message="ok", result=role.to_role_config_dict())
except Exception as e:
logger.error(f"获取角色配置失败: {e}")
return BaseOut(code=500, message=f"获取角色配置失败: {str(e)}", result=None)
@router.put("/role", response_model=BaseOut)
async def update_role_config(
role_config: RoleConfig, service: GLMService = Depends(get_glm_service)
):
"""更新角色配置"""
try:
# 将Pydantic模型转换为字典
role_data = role_config.dict()
updated_role = await service.update_role(role_data)
return BaseOut(
code=200,
message="角色配置已更新",
result={
"message": "角色配置已更新",
"role_config": updated_role.to_role_config_dict(),
},
)
except Exception as e:
logger.error(f"更新角色配置失败: {e}")
return BaseOut(code=500, message=f"更新角色配置失败: {str(e)}", result=None)
@router.post("/chat", response_model=ChatResponse)
async def chat(request: ChatRequest, service: GLMService = Depends(get_glm_service)):
"""处理聊天请求"""
# 步骤1: 记录请求开始
logger.info(
f"收到聊天请求: conversation_id={request.conversation_id or 'None'}, "
f"message_length={len(request.message)}, "
f"message_preview='{request.message[:50]}{'...' if len(request.message) > 50 else ''}'"
)
# 记录增强参数信息
has_enhanced_prompt = request.enhanced_prompt is not None
has_customer_tags = (
request.customer_tags is not None and len(request.customer_tags) > 0
)
has_message_history = (
request.message_history is not None and len(request.message_history) > 0
)
has_workflow_context = request.workflow_context is not None
logger.info(
f"增强参数状态: enhanced_prompt={has_enhanced_prompt}, "
f"customer_tags={has_customer_tags} ({len(request.customer_tags) if has_customer_tags else 0}个标签), "
f"message_history={has_message_history} ({len(request.message_history) if has_message_history else 0}条历史), "
f"workflow_context={has_workflow_context}"
)
# 记录客户标签摘要
if has_customer_tags:
tag_categories = {}
for tag in request.customer_tags:
category = tag.get("TagCategory", "未分类")
name = tag.get("TagName", "未命名")
if category not in tag_categories:
tag_categories[category] = []
tag_categories[category].append(name)
tag_summary = []
for category, names in tag_categories.items():
tag_summary.append(
f"{category}: {', '.join(names[:3])}{'...' if len(names) > 3 else ''}"
)
# 检查是否使用mock数据
is_mock_data = False
for tag in request.customer_tags:
if tag.get("TagName", "").startswith("mock_") or tag.get(
"TagCategory", ""
).startswith("mock_"):
is_mock_data = True
break
mock_note = " [mock数据]" if is_mock_data else ""
logger.info(f"客户标签摘要{mock_note}: {', '.join(tag_summary)}")
# 记录历史消息摘要
if has_message_history:
# 按时间排序,获取最近的消息
sorted_history = sorted(
request.message_history, key=lambda x: x.get("msgtime", ""), reverse=True
)[
:5
] # 只显示最近5条
history_summary = []
for i, msg in enumerate(sorted_history):
from_user = msg.get("from_user", "未知用户")
user_id = (
msg.get("user_id")
or msg.get("userId")
or msg.get("from_user_id")
or "无ID"
)
content = msg.get("text", {}).get("content", "") or msg.get("content", "")
content_preview = content[:30] + ("..." if len(content) > 30 else "")
history_summary.append(f"{from_user}(ID:{user_id}): '{content_preview}'")
# 检查是否使用mock数据
is_mock_data = False
for msg in sorted_history:
if msg.get("from_user", "").startswith("mock_") or msg.get(
"user_id", ""
).startswith("mock_"):
is_mock_data = True
break
mock_note = " [mock数据]" if is_mock_data else ""
logger.info(f"历史消息摘要{mock_note}: {', '.join(history_summary)}")
# 步骤2: 创建或获取对话历史
if request.conversation_id and request.conversation_id in conversations:
conversation_history = conversations[request.conversation_id]
logger.info(
f"使用现有对话: {request.conversation_id}, 历史消息数: {len(conversation_history)}"
)
# 记录历史消息摘要
if conversation_history:
last_messages = conversation_history[-3:] # 显示最近3条消息
history_summary = []
for i, msg in enumerate(last_messages, 1):
role = msg["role"]
content_preview = msg["content"][:30] + (
"..." if len(msg["content"]) > 30 else ""
)
history_summary.append(f"{role}: '{content_preview}'")
logger.debug(f"最近历史消息摘要: {', '.join(history_summary)}")
else:
conversation_history = []
request.conversation_id = f"conv_{len(conversations) + 1}"
conversations[request.conversation_id] = conversation_history
logger.info(f"新建对话: {request.conversation_id}")
# 步骤3: 构建系统提示词
try:
if has_enhanced_prompt:
# 使用前端生成的增强提示词
system_prompt = request.enhanced_prompt
logger.info(f"使用前端增强提示词, 长度: {len(system_prompt)} 字符")
logger.debug(
f"增强提示词预览: '{system_prompt[:200]}{'...' if len(system_prompt) > 200 else ''}'"
)
else:
# 获取当前角色配置并构建系统提示词
role = await service.get_current_role()
role_config = role.to_role_config_dict()
logger.info(
f"获取角色配置: name='{role.name}', "
f"技能数: {len(role_config['skills'].split('\\n')) if role_config['skills'] else 0}, "
f"限制数: {len(role_config['restrictions'].split('\\n')) if role_config['restrictions'] else 0}"
)
system_prompt = build_system_prompt(role_config)
logger.debug(f"系统提示词构建完成, 长度: {len(system_prompt)} 字符")
except Exception as e:
logger.error(
f"构建系统提示词失败: {e}, conversation_id={request.conversation_id}"
)
raise HTTPException(status_code=500, detail=f"构建系统提示词失败: {str(e)}")
# 步骤4: 构建消息列表
messages = [{"role": "system", "content": system_prompt}]
# 初始化企业微信历史消息变量
selected_wework_messages = []
# 添加企业微信历史消息(如果提供)
if has_message_history:
logger.info(f"添加企业微信历史消息到上下文: {len(request.message_history)} 条")
# 将企业微信消息转换为大模型消息格式
wework_messages = []
for msg in request.message_history:
from_user = msg.get("from_user", "未知用户")
content = msg.get("text", {}).get("content", "") or msg.get("content", "")
msgtime = msg.get("msgtime", "")
if content:
# 确定角色:如果是客户发送的,则为user;否则为assistant
# 这里需要根据实际业务逻辑判断,暂时假设外部用户是user
role = (
"user"
if "customer" in from_user.lower() or "user" in from_user.lower()
else "assistant"
)
# 格式化消息内容
formatted_content = f"[{msgtime}] {from_user}: {content}"
wework_messages.append({"role": role, "content": formatted_content})
# 添加到消息列表(限制数量,避免过长)
max_wework_messages = 10
selected_wework_messages = wework_messages[
-max_wework_messages:
] # 取最近的消息
messages.extend(selected_wework_messages)
logger.info(
f"已添加 {len(selected_wework_messages)} 条企业微信历史消息到上下文"
)
# 添加与大模型的对话历史
history_messages = conversation_history[-10:] # 保留最近10条历史
for msg in history_messages:
messages.append({"role": msg["role"], "content": msg["content"]})
# 添加当前用户消息
messages.append({"role": "user", "content": request.message})
conversation_history.append({"role": "user", "content": request.message})
logger.info(
f"构建消息列表完成: 系统消息1条, 企业微信历史{len(selected_wework_messages)}条, "
f"对话历史{len(history_messages)}条, 用户消息1条, 总消息数: {len(messages)}"
)
# 记录消息列表摘要
if logger.isEnabledFor(logging.DEBUG):
for i, msg in enumerate(messages):
role = msg["role"]
content_preview = msg["content"][:50] + (
"..." if len(msg["content"]) > 50 else ""
)
logger.debug(f"消息{i+1}: {role} - '{content_preview}'")
# 步骤5: 调用DeepSeek API
try:
logger.info(
f"调用DeepSeek API, conversation_id={request.conversation_id}, 总消息数: {len(messages)}"
)
response_text = await call_deepseek_api(messages)
# 步骤6: 记录API响应
logger.info(
f"DeepSeek API调用成功, conversation_id={request.conversation_id}, "
f"响应长度: {len(response_text)} 字符, "
f"响应预览: '{response_text[:100]}{'...' if len(response_text) > 100 else ''}'"
)
# 添加助手回复到历史
conversation_history.append({"role": "assistant", "content": response_text})
# 步骤7: 返回响应
logger.info(
f"返回聊天响应: conversation_id={request.conversation_id}, 响应长度: {len(response_text)} 字符"
)
return ChatResponse(
response=response_text, conversation_id=request.conversation_id
)
except Exception as e:
logger.error(
f"DeepSeek API调用失败: {e}, conversation_id={request.conversation_id}, message_preview='{request.message[:50]}...'"
)
raise HTTPException(status_code=500, detail=str(e))
@router.get("/conversations/{conversation_id}")
async def get_conversation(conversation_id: str):
"""获取特定对话的历史"""
if conversation_id not in conversations:
raise HTTPException(status_code=404, detail="对话不存在")
return {
"conversation_id": conversation_id,
"messages": conversations[conversation_id],
}
@router.post("/role/reset", response_model=BaseOut)
async def reset_role_config(service: GLMService = Depends(get_glm_service)):
"""重置角色配置为默认值"""
try:
# 使用 DEFAULT_ROLE_CONFIG 更新角色
role_data = DEFAULT_ROLE_CONFIG.dict()
updated_role = await service.update_role(role_data)
return BaseOut(
code=200,
message="角色配置已重置为默认值",
result={
"message": "角色配置已重置为默认值",
"role_config": updated_role.to_role_config_dict(),
},
)
except Exception as e:
logger.error(f"重置角色配置失败: {e}")
return BaseOut(code=500, message=f"重置角色配置失败: {str(e)}", result=None)