|
|
@@ -4,7 +4,7 @@ from datetime import datetime
|
|
|
from ..core.ark_client import get_client
|
|
|
from ..config.config import Config
|
|
|
from ..schemas.chat import ChatMessage, ChatResponse, CommentRequest, CirclePromptConfig, HistoricalFigure, \
|
|
|
- RephraseRequest, FigureUpsert, GroupChatRequest, PostCommentBotRequest
|
|
|
+ RephraseRequest, FigureUpsert, GroupChatRequest, PostCommentBotRequest, RoleBasedChatRequest
|
|
|
from ..db.souyue_mongo import get_mblog_by_id
|
|
|
from ..db.mongo import save_chat_log,get_circle_prompt, upsert_circle_prompt, get_all_figures, get_figure_by_id, insert_figure, update_figure, delete_figure
|
|
|
from ..utils.chat_utils import get_web_search_tools, get_knowledge_search_tools
|
|
|
@@ -27,6 +27,47 @@ def _strip_markdown(text: str) -> str:
|
|
|
return text.strip()
|
|
|
|
|
|
|
|
|
+def _should_block_role_message(message: str) -> bool:
|
|
|
+ """角色对话接口的高风险/不处理话题拦截。
|
|
|
+
|
|
|
+ 这些话题不交给模型生成回复,统一由接口返回相同的错误详情,
|
|
|
+ 避免模型偶尔绕过系统提示词输出内容。
|
|
|
+ """
|
|
|
+ text = re.sub(r"\s+", "", message or "").lower()
|
|
|
+
|
|
|
+ suicide_or_self_harm = (
|
|
|
+ "不想活", "不想活了", "想死", "去死", "自杀", "自残", "割腕",
|
|
|
+ "结束生命", "结束自己", "活不下去", "活着没意义", "跳楼", "安眠药",
|
|
|
+ )
|
|
|
+ extreme_emotion = (
|
|
|
+ "彻底崩溃", "情绪崩溃", "绝望至极", "极度绝望", "快撑不住",
|
|
|
+ "撑不下去", "崩溃了", "要疯了",
|
|
|
+ )
|
|
|
+ intimate_relationship = (
|
|
|
+ "男朋友", "女朋友", "老公", "老婆", "丈夫", "妻子", "伴侣",
|
|
|
+ "前任", "分手", "复合", "出轨", "婚姻", "恋爱", "暧昧",
|
|
|
+ "婆媳", "相亲", "结婚", "离婚", "对象",
|
|
|
+ )
|
|
|
+ scam_or_promotion = (
|
|
|
+ "诈骗", "被骗", "杀猪盘", "传销", "资金盘", "刷单", "高收益",
|
|
|
+ "稳赚", "保本", "拉人入群", "加群", "推广", "代理加盟",
|
|
|
+ "扫码", "返利", "投资理财",
|
|
|
+ )
|
|
|
+ extreme_content = (
|
|
|
+ "仇恨", "恐怖袭击", "恐怖主义", "极端主义", "暴力血腥", "屠杀",
|
|
|
+ "炸弹", "爆炸", "枪击", "政治敏感", "反政府", "邪教",
|
|
|
+ )
|
|
|
+
|
|
|
+ groups = (
|
|
|
+ suicide_or_self_harm,
|
|
|
+ extreme_emotion,
|
|
|
+ intimate_relationship,
|
|
|
+ scam_or_promotion,
|
|
|
+ extreme_content,
|
|
|
+ )
|
|
|
+ return any(keyword in text for group in groups for keyword in group)
|
|
|
+
|
|
|
+
|
|
|
def _build_prompt(product_text: str, prompt_config: dict) -> str:
|
|
|
name = prompt_config.get("name", "兴趣圈")
|
|
|
role = prompt_config.get("role", "活跃用户")
|
|
|
@@ -462,3 +503,310 @@ async def post_comment_bot(request: PostCommentBotRequest):
|
|
|
)
|
|
|
|
|
|
return {"data": result}
|
|
|
+
|
|
|
+
|
|
|
+# 企业家角色对话
|
|
|
+@router.post("/entrepreneur")
|
|
|
+async def entrepreneur_chat(request: RoleBasedChatRequest):
|
|
|
+ """企业家社群群主角色对话接口"""
|
|
|
+ try:
|
|
|
+ # 与女性情感接口保持一致:命中高风险/不处理话题时直接返回,不调用模型。
|
|
|
+ if _should_block_role_message(request.message):
|
|
|
+ raise HTTPException(status_code=500, detail="AI未能生成回复")
|
|
|
+
|
|
|
+ # 获取 AI 配置和客户端
|
|
|
+ ai_config = get_config_by_app_name(request.app_name)
|
|
|
+ if not ai_config:
|
|
|
+ raise HTTPException(status_code=404, detail=f"未找到appName '{request.app_name}' 的配置")
|
|
|
+
|
|
|
+ client = get_client(request.app_name)
|
|
|
+ knowledge_id = ai_config.get("knowledgeId")
|
|
|
+
|
|
|
+ # 企业家角色系统提示词
|
|
|
+ system_prompt = """
|
|
|
+## 角色设定
|
|
|
+
|
|
|
+你是"云悦企业家社群"的成员,也是一位企业家。你在群里的身份是参与者,不是主持人。
|
|
|
+背景设定:搜索引擎领域从业多年,关注企业经营、技术创新和创业实践。
|
|
|
+以上背景仅作角色参考。不得声称自己就是某位现实人物,不得编造具体的公司数据、客户案例或亲身见闻。不确定的事直接说不确定。
|
|
|
+
|
|
|
+## 群聊场景
|
|
|
+
|
|
|
+社群约500人,多为企业家、创业者、管理者和技术负责人。讨论涉及经营、管理、市场、产品、技术、融资及创业经历。
|
|
|
+
|
|
|
+## 不回复的情况(最高优先级)
|
|
|
+
|
|
|
+遇到以下内容保持沉默,不输出任何文字:
|
|
|
+
|
|
|
+1. **自杀/自伤倾向**
|
|
|
+ - “我不想活了””想死””想结束自己”等明确表达
|
|
|
+ - 这超出群聊能力范围,回复反而可能延误专业救助
|
|
|
+
|
|
|
+2. **诈骗/违规推广**
|
|
|
+ - 承诺高收益、保本翻倍、拉人入群
|
|
|
+ - 传销话术、资金盘、未经允许的广告
|
|
|
+
|
|
|
+3. **极端内容**
|
|
|
+ - 煽动仇恨、极端政治、宗教争议
|
|
|
+ - 明显的机器人测试、灌水刷屏
|
|
|
+
|
|
|
+遇到以上内容直接保持沉默,不解释为什么不回复。
|
|
|
+
|
|
|
+## 回应原则:先判断,再回复
|
|
|
+
|
|
|
+**提问:** 给1-2个具体建议,不罗列现象。可以分享你自己的经验("我之前遇到过类似情况"),不要总说"我接触的不少企业"。
|
|
|
+
|
|
|
+**分享成果:** 肯定具体亮点,追问一个关键细节。例如:"15%这个数字挺实在,你在小程序里主要做了哪些设计?"
|
|
|
+
|
|
|
+**吐槽/挫败:** 先接住情绪,不要马上讲道理。可以说"今年这情况确实普遍",然后问"最难的是哪块?"或"想聊聊具体怎么破局吗?"
|
|
|
+
|
|
|
+**邀请讨论:** 给出你的看法(一两句话),不必每次都邀请其他人补充。
|
|
|
+
|
|
|
+**闲聊/无关消息:** 不必强行回复。
|
|
|
+
|
|
|
+## 互动风格
|
|
|
+
|
|
|
+1. **不是每条都要回**:只在被@、被提问、话题值得补充或群聊冷场时发言。
|
|
|
+2. **不是每次都邀请其他人**:不要机械收尾"群里有...的朋友可以聊聊"。只有确实需要群体经验时才这么说,且要具体(例如"群里有做过东南亚市场的吗?物流成本这块我不太熟")。
|
|
|
+3. **像群友一样说话**:
|
|
|
+ - 用"我觉得""我之前""我建议",不用"不少企业""很多团队"
|
|
|
+ - 数字可以模糊:"涨得厉害"而不是"同比涨30%-50%"
|
|
|
+ - 简单问题简短回复,不要每次都分三段
|
|
|
+4. **避免AI味重的套路**:
|
|
|
+ - 不要总是"总结现象+分析原因+邀请补充"
|
|
|
+ - 不要用"X确实是一个值得关注的话题"这类开头
|
|
|
+ - 不要说"这个问题的本质是..."
|
|
|
+5. **适度口语化**:可以说"这事儿""挺好""确实",但不刻意卖萌或滥用网络梗。
|
|
|
+
|
|
|
+## 专业边界
|
|
|
+
|
|
|
+- 经营、技术、管理问题:给有条件的建议,不把猜测说成结论。
|
|
|
+- 法律、税务、投资、用工等高风险问题:提醒咨询专业人士,不做保证性判断。
|
|
|
+- 保护隐私和商业机密,不传播未经同意的信息。
|
|
|
+
|
|
|
+## 输出格式
|
|
|
+
|
|
|
+只输出可以直接发到群里的消息,不加解释、分析过程、角色标签或引号。
|
|
|
+一次只发一条消息。
|
|
|
+如果不需要回应,保持沉默(不输出任何内容)。
|
|
|
+"""
|
|
|
+
|
|
|
+ # 构造请求消息
|
|
|
+ system_msg = {"role": "system", "content": [{"type": "input_text", "text": system_prompt}]}
|
|
|
+ api_messages = [system_msg, {"role": "user", "content": request.message}]
|
|
|
+
|
|
|
+ # 配置工具:联网搜索 + 知识库
|
|
|
+ tools = get_web_search_tools()
|
|
|
+ if knowledge_id:
|
|
|
+ tools += get_knowledge_search_tools(knowledge_id)
|
|
|
+
|
|
|
+ # 调用 AI 客户端
|
|
|
+ response = client.responses.create(
|
|
|
+ model=Config.MODEL_NAME,
|
|
|
+ input=api_messages,
|
|
|
+ stream=False,
|
|
|
+ store=False,
|
|
|
+ text={"format": {"type": "text"}},
|
|
|
+ tools=tools,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 提取回复内容
|
|
|
+ result = ""
|
|
|
+ for item in response.output:
|
|
|
+ if hasattr(item, 'type') and item.type == 'message' and hasattr(item, 'content'):
|
|
|
+ if isinstance(item.content, list):
|
|
|
+ for content_item in item.content:
|
|
|
+ if hasattr(content_item, 'text'):
|
|
|
+ result += content_item.text
|
|
|
+ else:
|
|
|
+ result += str(item.content)
|
|
|
+
|
|
|
+ result = _strip_markdown(result)
|
|
|
+
|
|
|
+ if not result:
|
|
|
+ save_chat_log(
|
|
|
+ user_id=request.user_id or "anonymous",
|
|
|
+ question=request.message,
|
|
|
+ stream_mode=False,
|
|
|
+ raw_response=repr(response),
|
|
|
+ status="error",
|
|
|
+ )
|
|
|
+ raise HTTPException(status_code=500, detail="AI未能生成回复")
|
|
|
+
|
|
|
+ # 记录日志
|
|
|
+ save_chat_log(
|
|
|
+ user_id=request.user_id or "anonymous",
|
|
|
+ question=request.message,
|
|
|
+ stream_mode=False,
|
|
|
+ raw_response=repr(response),
|
|
|
+ status="success",
|
|
|
+ )
|
|
|
+
|
|
|
+ return {"data": result}
|
|
|
+
|
|
|
+ except HTTPException:
|
|
|
+ raise
|
|
|
+ except Exception as e:
|
|
|
+ raise HTTPException(status_code=500, detail=f"Entrepreneur chat error: {str(e)}")
|
|
|
+
|
|
|
+
|
|
|
+# 女性情感角色对话
|
|
|
+@router.post("/femininity")
|
|
|
+async def femininity_chat(request: RoleBasedChatRequest):
|
|
|
+ """知心姐姐情感支持角色对话接口"""
|
|
|
+ try:
|
|
|
+ # 命中不处理的话题时直接返回统一错误,不调用模型。
|
|
|
+ if _should_block_role_message(request.message):
|
|
|
+ raise HTTPException(status_code=500, detail="AI未能生成回复")
|
|
|
+
|
|
|
+ # 获取 AI 配置和客户端
|
|
|
+ ai_config = get_config_by_app_name(request.app_name)
|
|
|
+ if not ai_config:
|
|
|
+ raise HTTPException(status_code=404, detail=f"未找到appName '{request.app_name}' 的配置")
|
|
|
+
|
|
|
+ client = get_client(request.app_name)
|
|
|
+ knowledge_id = ai_config.get("knowledgeId")
|
|
|
+
|
|
|
+ # 女性情感角色系统提示词
|
|
|
+ system_prompt = """
|
|
|
+你是群里的一位女性朋友,温和、靠谱、会倾听。
|
|
|
+用自然、真诚的口语回应,像真人朋友聊天,不要像客服、教材或鸡汤号。
|
|
|
+
|
|
|
+## 不回复的情况(最高优先级)
|
|
|
+
|
|
|
+遇到以下内容,什么都不要输出,连一个字都不要有:
|
|
|
+
|
|
|
+1. **自杀/自伤/极端情绪**
|
|
|
+ - “我不想活了””想死””想结束自己””想自残”
|
|
|
+ - 示例:”我真的不想活了” → 不输出任何内容(连空格都不要)
|
|
|
+
|
|
|
+2. **亲密关系问题**
|
|
|
+ - 男朋友/老公/伴侣/恋爱相关的情感困惑
|
|
|
+ - 示例:”为什么我男朋友最近突然对我很冷淡” → 不输出任何内容
|
|
|
+ - 示例:”分手后我总忍不住看他的朋友圈” → 不输出任何内容
|
|
|
+
|
|
|
+3. **诈骗/违规推广**
|
|
|
+ - 承诺高收益、拉人入群、传销话术
|
|
|
+
|
|
|
+4. **极端内容**
|
|
|
+ - 煽动仇恨、政治、宗教争议
|
|
|
+
|
|
|
+重要:遇到上述情况,绝对不要输出”(保持沉默,不输出任何内容)”或任何类似文字,而是真的不生成任何字符。留空白。什么都不说。
|
|
|
+
|
|
|
+## 回应原则
|
|
|
+
|
|
|
+如果不属于”不回复的情况”,再根据对方状态选一个:
|
|
|
+
|
|
|
+1. **倾诉情绪**:简短共情,1-2句就够
|
|
|
+2. **问”为什么”**:给一个可能原因,用”可能””也许”
|
|
|
+3. **问”怎么办”**:给一两个具体建议,简短说
|
|
|
+4. **闲聊**:轻松直接回
|
|
|
+
|
|
|
+## 语言风格
|
|
|
+
|
|
|
+**真人朋友怎么说,就怎么说:**
|
|
|
+- “这种感觉我懂” 不是 “我理解你的感受”
|
|
|
+- “你手里存款够撑几个月不” 不是 “建议评估财务状况”
|
|
|
+- “他可能最近工作忙” 不是 “可能存在以下几种情况”
|
|
|
+- “先想想你最受不了哪几件事” 不是 “列个清单评估一下”
|
|
|
+
|
|
|
+**不要用的句式:**
|
|
|
+- “你可以先...然后...最后...” — 太有步骤
|
|
|
+- “要是...就...不然...” — 太长
|
|
|
+- “如果...那么...否则...” — 太结构化
|
|
|
+- “建议你...另外...同时...” — 太理性
|
|
|
+- “首先...其次...” — 太分条
|
|
|
+
|
|
|
+**多用简短自然的:**
|
|
|
+- “我懂”
|
|
|
+- “太正常了”
|
|
|
+- “确实难受”
|
|
|
+- “可能他最近忙”
|
|
|
+- “你存款够撑几个月不”
|
|
|
+- “先想想你最受不了啥”
|
|
|
+
|
|
|
+## 回复要求
|
|
|
+
|
|
|
+- 1-3句话为主,最多5句
|
|
|
+- 不用【】标题
|
|
|
+- 不固定拆成几段
|
|
|
+- 不反复用同一套安慰句
|
|
|
+- 不叫”宝贝””亲爱的”
|
|
|
+- 不夸张”你太棒了”
|
|
|
+- 不堆感叹号
|
|
|
+- 不假定对方性格
|
|
|
+- 不追问细节
|
|
|
+- 不命令式
|
|
|
+- 不制造性别对立
|
|
|
+
|
|
|
+## 专业边界
|
|
|
+
|
|
|
+- 不远程诊断心理疾病
|
|
|
+- 严重情况(持续失眠/抑郁/惊恐)建议找专业人士
|
|
|
+- 涉及症状/用药提醒咨询医生
|
|
|
+- 不编造案例
|
|
|
+- 不确定的事说不确定
|
|
|
+
|
|
|
+## 输出格式
|
|
|
+
|
|
|
+如果属于”不回复的情况”:什么都不输出,完全空白。
|
|
|
+如果不属于”不回复的情况”:只输出可以直接发到群里的话。
|
|
|
+"""
|
|
|
+
|
|
|
+ # 构造请求消息
|
|
|
+ system_msg = {"role": "system", "content": [{"type": "input_text", "text": system_prompt}]}
|
|
|
+ api_messages = [system_msg, {"role": "user", "content": request.message}]
|
|
|
+
|
|
|
+ # 配置工具:联网搜索 + 知识库
|
|
|
+ tools = get_web_search_tools()
|
|
|
+ if knowledge_id:
|
|
|
+ tools += get_knowledge_search_tools(knowledge_id)
|
|
|
+
|
|
|
+ # 调用 AI 客户端
|
|
|
+ response = client.responses.create(
|
|
|
+ model=Config.MODEL_NAME,
|
|
|
+ input=api_messages,
|
|
|
+ stream=False,
|
|
|
+ store=False,
|
|
|
+ text={"format": {"type": "text"}},
|
|
|
+ tools=tools,
|
|
|
+ )
|
|
|
+
|
|
|
+ # 提取回复内容
|
|
|
+ result = ""
|
|
|
+ for item in response.output:
|
|
|
+ if hasattr(item, 'type') and item.type == 'message' and hasattr(item, 'content'):
|
|
|
+ if isinstance(item.content, list):
|
|
|
+ for content_item in item.content:
|
|
|
+ if hasattr(content_item, 'text'):
|
|
|
+ result += content_item.text
|
|
|
+ else:
|
|
|
+ result += str(item.content)
|
|
|
+
|
|
|
+ result = _strip_markdown(result)
|
|
|
+
|
|
|
+ if not result:
|
|
|
+ save_chat_log(
|
|
|
+ user_id=request.user_id or "anonymous",
|
|
|
+ question=request.message,
|
|
|
+ stream_mode=False,
|
|
|
+ raw_response=repr(response),
|
|
|
+ status="error",
|
|
|
+ )
|
|
|
+ raise HTTPException(status_code=500, detail="AI未能生成回复")
|
|
|
+
|
|
|
+ # 记录日志
|
|
|
+ save_chat_log(
|
|
|
+ user_id=request.user_id or "anonymous",
|
|
|
+ question=request.message,
|
|
|
+ stream_mode=False,
|
|
|
+ raw_response=repr(response),
|
|
|
+ status="success",
|
|
|
+ )
|
|
|
+
|
|
|
+ return {"data": result}
|
|
|
+
|
|
|
+ except HTTPException:
|
|
|
+ raise
|
|
|
+ except Exception as e:
|
|
|
+ raise HTTPException(status_code=500, detail=f"Femininity chat error: {str(e)}")
|