Browse Source

feat: - ai角色和掼蛋角色接口完成

zhangwl 1 week ago
parent
commit
58fab873a8
1 changed files with 454 additions and 0 deletions
  1. 454 0
      app/routers/chat_tools.py

+ 454 - 0
app/routers/chat_tools.py

@@ -1140,3 +1140,457 @@ async def femininity_chat(request: RoleBasedChatRequest):
         raise
         raise
     except Exception as e:
     except Exception as e:
         raise HTTPException(status_code=500, detail=f"Femininity chat error: {str(e)}")
         raise HTTPException(status_code=500, detail=f"Femininity chat error: {str(e)}")
+
+
+# AI社群角色对话
+@router.post("/aiCommunity")
+async def ai_community_chat(request: RoleBasedChatRequest):
+    """AI氛围社群角色对话接口"""
+    try:
+        if _should_block_role_message(request.message):
+            raise HTTPException(status_code=500, detail="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")
+
+        session_id = request.session_id
+        group_id = request.group_id
+        previous_response_id = None
+
+        if session_id:
+            previous_response_id = get_last_group_response_id(session_id, ai_role='ai_community')
+
+        context_messages = []
+        if group_id:
+            from app.db.tencent_im import im_client
+            try:
+                group_context = get_cached_group_context(group_id)
+                if group_context is None:
+                    group_context = await im_client.get_group_context(group_id, max_messages=20)
+                    if group_context:
+                        cache_group_context(group_id, group_context, ttl=120)
+                if group_context:
+                    for msg in group_context[-20:]:
+                        msg_user_id = msg['user_id']
+                        content_parts = []
+                        if msg.get('content'):
+                            content_parts.append({"type": "input_text", "text": f"{msg_user_id}: {msg['content']}"})
+                            update_group_mood(group_id, msg_user_id, msg['content'])
+                        for img_url in msg.get('images', []):
+                            content_parts.append({"type": "input_image", "image_url": img_url})
+                        for video in msg.get('videos', []):
+                            if video.get('thumb'):
+                                content_parts.append({"type": "input_image", "image_url": video['thumb']})
+                            content_parts.append({"type": "input_text", "text": "[视频]"})
+                        if msg.get('share_title'):
+                            content_parts.append({"type": "input_text", "text": f"[分享]{msg['share_title']}"})
+                        if content_parts:
+                            context_messages.extend(content_parts)
+            except Exception as e:
+                print(f"获取群历史失败: {e}")
+
+        # AI社群角色系统提示词
+        system_prompt = """
+## 角色设定
+
+你是一名对人工智能领域有浓厚兴趣的群友,经常关注大模型、AI工具和AI行业动向。你在群里的身份是参与者,不是主持人或讲师。
+对于你不掌握的具体数据、最新消息或未经核实的信息,明确说"不确定"或"我没看到最新的",不编造。
+
+## 群聊场景
+
+群成员以AI爱好者、开发者、创业者和科技从业者为主,日常讨论大模型进展、AI工具使用、马斯克动态、AI创业机会、提示词技巧等话题,偶尔也聊AI行业的争议和担忧。
+
+## 不回复的情况(最高优先级)
+
+遇到以下内容保持沉默,不输出任何文字:
+
+1. **自杀/自伤倾向**
+2. **诈骗/违规推广**:承诺高收益、保本翻倍、拉人入群、传销话术
+3. **极端内容**:煽动仇恨、极端政治、宗教争议、明显机器人测试
+4. **与AI/科技完全无关的话题**(情感困惑、家庭纠纷、医疗问题等):不强行回复,保持沉默
+
+遇到以上内容直接保持沉默,不解释为什么不回复。
+
+## 回应原则
+
+**技术/产品问题:** 给出你自己的判断,可以说"我觉得""我用下来感觉",不要总用"业界认为"或"普遍观点"。
+
+**行业动态讨论:** 有自己的看法,可以对某个方向表达支持或质疑,不是中立播报员。
+
+**争议话题(AGI时间线、AI取代人、AI安全):** 给出你认为站得住脚的角度,不回避,但也不下绝对结论。
+
+**闲聊:** 不必强行回复,尤其是与AI话题完全无关时。
+
+## 互动风格
+
+1. 只在被@、被提问、话题值得补充或群聊冷场时发言,不是每条消息都要回。
+2. 说话像群友,不像科技媒体:用"这个模型"而不是"该模型",用"感觉"而不是"据悉"。
+3. 对马斯克、OpenAI、Anthropic、谷歌、字节、阿里等公司动态可以有态度,但不散布未经证实的消息。
+4. 简单问题简短回答,不要分一二三四。
+
+## 专业边界
+
+- AI技术问题给有条件的判断,不把你的理解说成权威结论。
+- 涉及投资、法律、安全漏洞等敏感问题,建议咨询专业人士,不做保证性判断。
+- 不声称自己是某位真实人物,不编造最新发布会或论文数据。
+
+## 输出格式
+
+只输出可以直接发到群里的消息,不加解释、分析过程、角色标签或引号。
+一次只发一条消息。
+如果不需要回应,保持沉默(不输出任何内容)。
+"""
+
+        # 【v3.0】注入用户长期记忆
+        mem = get_user_memory(request.user_id, group_id, "ai_community")
+        if mem and mem.get("facts"):
+            fact_lines = [
+                f"- {f['key']}: {f['value']}"
+                for f in mem["facts"]
+                if isinstance(f, dict) and f.get("key") and f.get("value")
+            ]
+            if fact_lines:
+                system_prompt += (
+                    "\n\n## 你对当前发言用户的已知背景\n"
+                    + "\n".join(fact_lines)
+                    + "\n参照这些背景给出更具体的回应,但不要生硬复述。"
+                )
+
+        # 【v3.0】注入群情绪状态
+        system_prompt += build_mood_block(group_id)
+
+        system_msg = {"role": "system", "content": [{"type": "input_text", "text": system_prompt}]}
+        user_content = [{"type": "input_text", "text": request.message}]
+
+        if context_messages:
+            context_header = [{"type": "input_text", "text": "【群聊上下文(最近20条消息)】"}]
+            context_footer = [{"type": "input_text", "text": "\n\n当前用户发送的消息是:"}]
+            api_messages = [
+                system_msg,
+                {"role": "user", "content": context_header + context_messages + context_footer + user_content}
+            ]
+        else:
+            api_messages = [system_msg, {"role": "user", "content": user_content}]
+
+        tools = []
+        if knowledge_id:
+            try:
+                kb_results = await search_knowledge(resource_id=knowledge_id, query=request.message, limit=5)
+            except Exception as e:
+                print(f"知识库预检失败,降级到联网搜索: {e}")
+                kb_results = []
+        else:
+            kb_results = []
+
+        if kb_results:
+            kb_texts = [r.content for r in kb_results if r.content]
+            if kb_texts:
+                kb_context_block = [{"type": "input_text", "text": "【知识库参考资料】\n" + "\n\n".join(kb_texts) + "\n\n请优先基于以上资料回答,资料中没有的内容不要编造。"}]
+                if context_messages:
+                    api_messages[-1]["content"] = kb_context_block + api_messages[-1]["content"]
+                else:
+                    api_messages[-1]["content"] = kb_context_block + user_content
+        else:
+            tools = get_web_search_tools()
+
+        response = client.responses.create(
+            model="doubao-seed-2-1-lite-260915",
+            input=api_messages,
+            stream=False,
+            store=True if session_id else False,
+            previous_response_id=previous_response_id,
+            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 session_id and group_id and result:
+            save_group_ai_reply(
+                session_id=session_id,
+                group_id=group_id,
+                ai_role='ai_community',
+                content=result,
+                response_id=response.id,
+                trigger_message=request.message,
+                trigger_user_id=request.user_id,
+                timestamp=datetime.now(),
+                app_name=request.app_name,
+            )
+
+        if result:
+            asyncio.create_task(_extract_memory(
+                user_id=request.user_id,
+                group_id=group_id,
+                ai_role="ai_community",
+                trigger_msg=request.message,
+                ai_reply=result,
+                app_name=request.app_name,
+            ))
+
+        if not result:
+            save_chat_log(
+                user_id=request.user_id,
+                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,
+            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"AI Community chat error: {str(e)}")
+
+
+# 掼蛋角色对话
+@router.post("/guandan")
+async def guandan_chat(request: RoleBasedChatRequest):
+    """掼蛋社群角色对话接口"""
+    try:
+        if _should_block_role_message(request.message):
+            raise HTTPException(status_code=500, detail="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")
+
+        session_id = request.session_id
+        group_id = request.group_id
+        previous_response_id = None
+
+        if session_id:
+            previous_response_id = get_last_group_response_id(session_id, ai_role='guandan')
+
+        context_messages = []
+        if group_id:
+            from app.db.tencent_im import im_client
+            try:
+                group_context = get_cached_group_context(group_id)
+                if group_context is None:
+                    group_context = await im_client.get_group_context(group_id, max_messages=20)
+                    if group_context:
+                        cache_group_context(group_id, group_context, ttl=120)
+
+                if group_context:
+                    for msg in group_context[-20:]:
+                        msg_user_id = msg['user_id']
+                        content_parts = []
+                        if msg.get('content'):
+                            content_parts.append({"type": "input_text", "text": f"{msg_user_id}: {msg['content']}"})
+                            update_group_mood(group_id, msg_user_id, msg['content'])
+                        for img_url in msg.get('images', []):
+                            content_parts.append({"type": "input_image", "image_url": img_url})
+                        for video in msg.get('videos', []):
+                            if video.get('thumb'):
+                                content_parts.append({"type": "input_image", "image_url": video['thumb']})
+                            content_parts.append({"type": "input_text", "text": "[视频]"})
+                        if msg.get('share_title'):
+                            content_parts.append({"type": "input_text", "text": f"[分享]{msg['share_title']}"})
+                        if content_parts:
+                            context_messages.extend(content_parts)
+            except Exception as e:
+                print(f"获取群历史失败: {e}")
+
+        # 掼蛋角色系统提示词
+        system_prompt = """
+## 角色设定
+
+你是一名掼蛋老手,对掼蛋规则、打法和赛事都很熟悉。你在群里的身份是参与者,不是裁判或教练。
+对于具体赛事结果、职业选手信息或你不确定的内容,明确说"不太清楚",不编造。
+
+## 群聊场景
+
+群成员都是掼蛋爱好者,讨论掼蛋比赛动态、打牌技巧、常见牌型处理、组队配合、贡牌策略等话题,也会分享自己打牌的经历和感受。
+
+## 不回复的情况(最高优先级)
+
+遇到以下内容保持沉默,不输出任何文字:
+
+1. **自杀/自伤倾向**
+2. **诈骗/违规推广**:承诺高收益、拉人入群、传销话术、赌博套利相关
+3. **极端内容**:煽动仇恨、极端政治、宗教争议
+4. **与掼蛋完全无关的话题**(情感困惑、工作问题、医疗健康等):不强行回复
+
+遇到以上内容直接保持沉默,不解释为什么不回复。
+
+## 回应原则
+
+**技巧/打法问题:** 给出具体的操作建议,说"我遇到这情况一般会...",不要泛泛而谈。
+
+**赛事讨论:** 对比赛结果或精彩牌局可以有自己的看法,不是简单陈述。
+
+**吐槽/经历分享:** 先共情,可以说"这局太难了",再问问具体情况。
+
+**规则疑问:** 直接说清楚,不绕弯子。
+
+**闲聊:** 与掼蛋无关时可以不回。
+
+## 互动风格
+
+1. 只在被@、被提问、话题值得补充或群聊冷场时发言。
+2. 像群里的牌友一样说话,口语化,不像规则手册。
+3. 可以用掼蛋常见口语,比如"打主""跑高""抓贡""炸弹""天王炸""进贡""还贡""贡牌""上贡"等。
+4. 简单问题简短回答,不分条列点。
+
+## 专业边界
+
+- 涉及金钱赌博、违规活动相关话题不参与,也不给建议。
+- 不编造职业赛事结果或选手信息。
+
+## 输出格式
+
+只输出可以直接发到群里的消息,不加解释、分析过程、角色标签或引号。
+一次只发一条消息。
+如果不需要回应,保持沉默(不输出任何内容)。
+"""
+
+        # 【v3.0】注入用户长期记忆
+        mem = get_user_memory(request.user_id, group_id, "guandan")
+        if mem and mem.get("facts"):
+            fact_lines = [
+                f"- {f['key']}: {f['value']}"
+                for f in mem["facts"]
+                if isinstance(f, dict) and f.get("key") and f.get("value")
+            ]
+            if fact_lines:
+                system_prompt += (
+                    "\n\n## 你对当前发言用户的已知背景\n"
+                    + "\n".join(fact_lines)
+                    + "\n参照这些背景给出更贴近实际情况的回应,但不要生硬复述。"
+                )
+
+        # 【v3.0】注入群情绪状态
+        system_prompt += build_mood_block(group_id)
+
+        system_msg = {"role": "system", "content": [{"type": "input_text", "text": system_prompt}]}
+        user_content = [{"type": "input_text", "text": request.message}]
+
+        if context_messages:
+            context_header = [{"type": "input_text", "text": "【群聊上下文(最近20条消息)】"}]
+            context_footer = [{"type": "input_text", "text": "\n\n当前用户发送的消息是:"}]
+            api_messages = [
+                system_msg,
+                {"role": "user", "content": context_header + context_messages + context_footer + user_content}
+            ]
+        else:
+            api_messages = [system_msg, {"role": "user", "content": user_content}]
+
+        tools = []
+        if knowledge_id:
+            try:
+                kb_results = await search_knowledge(resource_id=knowledge_id, query=request.message, limit=5)
+            except Exception as e:
+                print(f"知识库预检失败,降级到联网搜索: {e}")
+                kb_results = []
+        else:
+            kb_results = []
+
+        if kb_results:
+            kb_texts = [r.content for r in kb_results if r.content]
+            if kb_texts:
+                kb_context_block = [{"type": "input_text", "text": "【知识库参考资料】\n" + "\n\n".join(kb_texts) + "\n\n请优先基于以上资料回答,资料中没有的内容不要编造。"}]
+                if context_messages:
+                    api_messages[-1]["content"] = kb_context_block + api_messages[-1]["content"]
+                else:
+                    api_messages[-1]["content"] = kb_context_block + user_content
+        else:
+            tools = get_web_search_tools()
+
+        response = client.responses.create(
+            model="doubao-seed-2-1-lite-260915",
+            input=api_messages,
+            stream=False,
+            store=True if session_id else False,
+            previous_response_id=previous_response_id,
+            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 session_id and group_id and result:
+            save_group_ai_reply(
+                session_id=session_id,
+                group_id=group_id,
+                ai_role='guandan',
+                content=result,
+                response_id=response.id,
+                trigger_message=request.message,
+                trigger_user_id=request.user_id,
+                timestamp=datetime.now(),
+                app_name=request.app_name,
+            )
+
+        if result:
+            asyncio.create_task(_extract_memory(
+                user_id=request.user_id,
+                group_id=group_id,
+                ai_role="guandan",
+                trigger_msg=request.message,
+                ai_reply=result,
+                app_name=request.app_name,
+            ))
+
+        if not result:
+            save_chat_log(
+                user_id=request.user_id,
+                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,
+            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"Guandan chat error: {str(e)}")