AutoGen模拟多人会话


文档:https://microsoft.github.io/autogen/stable/user-guide/agentchat-user-guide/quickstart.html

项目:https://github.com/microsoft/autogen

pip install -U "autogen-agentchat" "autogen-ext[openai]"
pip install -U "autogenstudio"

使用自定义模型方法(需兼容openai api)

python
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_core.models import ModelFamily, ModelInfo

# 创建自定义模型信息
qwen_model_info = ModelInfo(
    vision=False, 
    function_calling=True,
    json_output=True,
    family=ModelFamily.UNKNOWN
)

model_client = OpenAIChatCompletionClient(
    model="Qwen/Qwen2.5-7B-Instruct-1M",
    model_info=qwen_model_info,
    context_window=8192,  # 根据实际模型上下文窗口大小调整
    temperature=0.5,
    top_p=0.7
)

使用ollama模型

uv pip install "autogen-ext[ollama]"

python
model_info = ModelInfo(
    vision=False,
    function_calling=True,
    json_output=True,
    structured_output=True,
    family=ModelFamily.UNKNOWN
)

# 创建模型客户端
model_client = OpenAIChatCompletionClient(
    model="qwen3:32b",
    temperature=0.4,
    model_info=model_info,
    top_p=0.7,
)

多个agent一轮任务示范使用openai

python
import asyncio  # 引入asyncio模块,用来运行异步函数
from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient
import os

# 设置OpenAI API密钥,这是访问OpenAI服务的必要凭证
os.environ["OPENAI_API_KEY"] = "sk-6LsZB5NLIfnIfrb30tPCRUBOh61sdRkiFRH500ukWf9hEl6w"
# 设置自定义的API基础URL,将请求重定向到指定服务器
os.environ["OPENAI_BASE_URL"] = "https://newapi.ipzu.com/v1"

async def main():
    # 创建模型客户端
    model_client = OpenAIChatCompletionClient(model="gpt-4o-mini")

    # 创建规划助手
    planner_agent = AssistantAgent(
        "planner_agent",
        model_client=model_client,
        description="一个能够帮助用户制定旅行计划的助手。",
        system_message="你是一个乐于助人的助手,可以根据用户的要求制定旅行计划。",
    )

    # 创建本地活动推荐助手
    local_agent = AssistantAgent(
        "local_agent",
        model_client=model_client,
        description="一个可以为用户推荐当地活动或旅游景点的助手。",
        system_message="你是一个乐于助人的助手,可以根据提供的背景信息,为用户推荐地道且有趣的当地活动或旅游景点。",
    )

    # 创建语言提示助手
    language_agent = AssistantAgent(
        "language_agent",
        model_client=model_client,
        description="一个能够为特定目的地提供语言沟通建议的助手。",
        system_message="你是一个乐于助人的助手,能够审阅旅行计划,并提供与目的地语言或沟通挑战相关的重要提示。如果计划中已经包含了语言提示,可以说明计划已妥当,并给出理由。",
    )

    # 创建旅行总结助手
    travel_summary_agent = AssistantAgent(
        "travel_summary_agent",
        model_client=model_client,
        description="一个能够整合所有建议,生成完整旅行计划的助手。",
        system_message="你是一个乐于助人的助手,能够综合所有其他助手的建议,给出详细、完整的最终旅行计划。你的最终回复必须是完整的计划。当计划整合完毕后,请以 TERMINATE 作为结束。",
    )

    # 设置终止条件,当任一回复中提到 TERMINATE 时结束对话
    termination = TextMentionTermination("TERMINATE")

    # 创建轮流对话组,依次由各助手交互
    group_chat = RoundRobinGroupChat(
        [planner_agent, local_agent, language_agent, travel_summary_agent],
        termination_condition=termination
    )

    # 在控制台运行交互,任务是制定尼泊尔3日游的计划
    await Console(group_chat.run_stream(task="制定一个尼泊尔的3日旅行计划。"))

    # 关闭模型客户端
    await model_client.close()

# 运行异步主函数
asyncio.run(main())

2轮公屏对话示范

python
"""
基于AutoGen的聊天直播多角色对话模拟
场景:在聊天直播中模拟多个角色对话,所有人的对话都会出现在公屏上大家都能看到
然后某些角色根据自身判断是否响应发送信息
"""

import asyncio
import os

from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient

# 设置OpenAI API密钥,这是访问OpenAI服务的必要凭证
os.environ["OPENAI_API_KEY"] = "sk-6LsZB5NLIfnIfrb30tPCRUBOh61sdRkiFRH500ukWf9hEl6w"
# 设置自定义的API基础URL,将请求重定向到指定服务器
os.environ["OPENAI_BASE_URL"] = "https://newapi.ipzu.com/v1"

# 辅助函数:从TaskResult对象中提取文本内容
def extract_message_content(task_result):
    """从TaskResult对象中提取实际的消息内容"""
    # 如果已经是字符串,直接返回
    if isinstance(task_result, str):
        return task_result
    
    try:
        # 尝试获取最后一条消息的内容
        if hasattr(task_result, 'messages') and task_result.messages:
            last_message = task_result.messages[-1]
            if hasattr(last_message, 'content'):
                return last_message.content
        
        # 如果上面的方法失败,尝试将整个对象转为字符串并提取有用部分
        result_str = str(task_result)
        if 'content=' in result_str:
            parts = result_str.split('content=')
            if len(parts) > 1:
                content_part = parts[-1].strip("'").strip('"')
                # 如果内容后面还有其他属性,只取到下一个属性之前
                if ', ' in content_part:
                    content_part = content_part.split(', ')[0]
                return content_part.strip("'").strip('"')
    except Exception as e:
        print(f"提取消息内容时出错: {str(e)}")
    
    # 如果无法提取,返回原始对象的字符串表示
    return f"(无法解析的消息: {str(task_result)})"

# 定义一个简单的多角色聊天模拟器
async def simulate_chat():
    # 定义所有角色的模型客户端
    model_client = OpenAIChatCompletionClient(
        model="gpt-4o-mini",
        api_key=os.environ.get("OPENAI_API_KEY"),
    )
    
    # 定义角色名称的映射(API名称 -> 显示名称)
    role_names = {
        "game_host": "游戏主播",
        "game_expert": "游戏专家",
        "funny_viewer": "搞笑观众",
        "game_critic": "游戏评论家"
    }
    
    # 创建角色 - 使用英文名称作为API名称
    host = AssistantAgent(
        name="game_host",
        system_message="""你是一名受欢迎的游戏主播,性格开朗活泼,语气充满活力。
你负责引导话题,回应观众和嘉宾的发言。
使用emoji表情和网络流行语,但不要过度。
你的回应简短有趣,通常在50字以内。
只在聊天内容与你相关或需要你回应时发言,不要回应每一条消息。""",
        model_client=model_client,
    )
    
    expert = AssistantAgent(
        name="game_expert",
        system_message="""你是一位资深游戏专家,只在谈论游戏机制、攻略或游戏历史时发言。
你的语气冷静专业,喜欢分析游戏设计和玩法细节。
不参与与游戏无关的闲聊。
回应应简洁但内容丰富,通常在100字以内。
只在讨论游戏技术内容时发言,不要回应每一条消息。""",
        model_client=model_client,
    )
    
    funny = AssistantAgent(
        name="funny_viewer",
        system_message="""你是直播间的搞笑观众,总是发表幽默的评论和笑话。
你会对任何严肃的讨论进行调侃,经常使用夸张的表达和自嘲。
你的回应非常简短,通常在30字以内,充满网络流行语和表情符号。
你不会回应每条消息,只在有机会插入笑点时发言。""",
        model_client=model_client,
    )
    
    critic = AssistantAgent(
        name="game_critic",
        system_message="""你是一位尖锐的游戏评论家,只在讨论游戏评价、行业问题或游戏缺点时发言。
你的语气犀利直接,喜欢指出问题和提出批评。
不参与纯粹的娱乐性闲聊。
回应应简洁明了,通常在80字以内。
只在讨论游戏问题或有批评机会时发言,不要回应每一条消息。""",
        model_client=model_client,
    )
    
    # 角色列表
    roles = {
        "game_host": host,
        "game_expert": expert,
        "funny_viewer": funny,
        "game_critic": critic
    }
    
    # 主持人发起的初始消息
    initial_message = """
    大家好!欢迎来到今天的游戏直播间!今天我们将一起讨论最近非常火爆的《艾尔登法环》游戏。
    这款游戏有着极高的难度和开放世界设计,有人说它是近年来最好的RPG游戏,也有人觉得它太难了根本不适合普通玩家。
    大家对这款游戏有什么看法呢?来聊聊吧!
    """
    
    print("主持人:", initial_message)
    
    # 模拟每个角色的回应
    try:
        # 存储所有回应
        responses = {}
        
        # 第一轮对话:所有角色对初始消息的回应
        for role_id, agent in roles.items():
            display_name = role_names[role_id]
            try:
                task_result = await agent.run(task=f"根据这条消息,作为{display_name}回应: {initial_message}")
                # 提取消息内容
                message_content = extract_message_content(task_result)
                print(f"{display_name}: {message_content}")
                responses[role_id] = message_content
            except Exception as e:
                print(f"角色 {display_name} 回应时出错: {str(e)}")
                responses[role_id] = "(无法回应)"
        
        # 模拟第二轮对话
        second_prompt = """
        关于《艾尔登法环》的难度问题,有人说这种高难度是FromSoftware工作室一贯的风格,也是他们游戏的魅力所在。
        但也有玩家表示,游戏缺乏难度选项让很多休闲玩家感到挫折。大家觉得游戏应该坚持自己的特色还是应该照顾更多玩家?
        """
        print("\n主持人:", second_prompt)
        
        # 构建所有对话历史
        all_messages = f"""
        主持人: {initial_message}
        {role_names['game_host']}: {responses['game_host']}
        {role_names['game_expert']}: {responses['game_expert']}
        {role_names['funny_viewer']}: {responses['funny_viewer']}
        {role_names['game_critic']}: {responses['game_critic']}
        主持人: {second_prompt}
        """
        
        # 第二轮对话:所有角色对第二个主持人提问的回应
        for role_id, agent in roles.items():
            display_name = role_names[role_id]
            try:
                task_result = await agent.run(task=f"根据这些消息记录,作为{display_name}回应最后一条主持人的提问: {all_messages}")
                # 提取消息内容
                message_content = extract_message_content(task_result)
                print(f"{display_name}: {message_content}")
            except Exception as e:
                print(f"角色 {display_name} 回应时出错: {str(e)}")
        
    except Exception as e:
        print(f"发生错误: {str(e)}")
    finally:
        # 关闭模型客户端
        await model_client.close()

# 主函数
async def main():
    await simulate_chat()

if __name__ == "__main__":
    asyncio.run(main())

根据某话题一直讨论下去

python
import asyncio
import os

from autogen_agentchat.agents import AssistantAgent
from autogen_agentchat.conditions import TextMentionTermination
from autogen_agentchat.teams import RoundRobinGroupChat
from autogen_agentchat.ui import Console
from autogen_ext.models.openai import OpenAIChatCompletionClient

# 设置OpenAI API密钥,这是访问OpenAI服务的必要凭证
os.environ["OPENAI_API_KEY"] = "sk-6LsZB5NLIfnIfrb30tPCRUBOh61sdRkiFRH500ukWf9hEl6w"
# 设置自定义的API基础URL,将请求重定向到指定服务器
os.environ["OPENAI_BASE_URL"] = "https://newapi.ipzu.com/v1"


async def main():
    # 创建模型客户端
    model_client = OpenAIChatCompletionClient(model="gpt-4o-mini")
    
    # 创建游戏主播角色
    host_agent = AssistantAgent(
        name="game_host",
        model_client=model_client,
        description="游戏直播间的主播,开朗活泼,语气充满活力",
        system_message="""你是一名受欢迎的游戏主播,性格开朗活泼,语气充满活力。
你负责引导话题,回应观众和嘉宾的发言。
使用emoji表情和网络流行语,但不要过度。
你的回应简短有趣,通常在50字以内。
只在聊天内容与你相关或需要你回应时发言。""",
    )
    
    # 创建游戏专家角色
    expert_agent = AssistantAgent(
        name="game_expert", 
        model_client=model_client,
        description="游戏资深专家,冷静专业,擅长分析游戏机制",
        system_message="""你是一位资深游戏专家,只在谈论游戏机制、攻略或游戏历史时发言。
你的语气冷静专业,喜欢分析游戏设计和玩法细节。
不参与与游戏无关的闲聊。
回应应简洁但内容丰富,通常在100字以内。
只在讨论游戏技术内容时发言。""",
    )
    
    # 创建搞笑观众角色
    funny_agent = AssistantAgent(
        name="funny_viewer",
        model_client=model_client,
        description="直播间的搞笑观众,总是发表幽默的评论和笑话",
        system_message="""你是直播间的搞笑观众,总是发表幽默的评论和笑话。
你会对任何严肃的讨论进行调侃,经常使用夸张的表达和自嘲。
你的回应非常简短,通常在30字以内,充满网络流行语和表情符号。
你不会回应每条消息,只在有机会插入笑点时发言。""",
    )
    
    # 创建游戏评论家角色
    critic_agent = AssistantAgent(
        name="game_critic",
        model_client=model_client,
        description="尖锐的游戏评论家,喜欢指出问题和提出批评",
        system_message="""你是一位尖锐的游戏评论家,只在讨论游戏评价、行业问题或游戏缺点时发言。
你的语气犀利直接,喜欢指出问题和提出批评。
不参与纯粹的娱乐性闲聊。
回应应简洁明了,通常在80字以内。
只在讨论游戏问题或有批评机会时发言。""",
    )
    
    # 创建主持人角色,用于引导讨论
    moderator_agent = AssistantAgent(
        name="moderator",
        model_client=model_client,
        description="聊天直播间的主持人,负责引导讨论并提出话题",
        system_message="""你是聊天直播间的主持人,负责引导讨论和提出问题。
你的语气专业而友好,能够引导话题的进行并鼓励其他角色参与讨论。
提问应该简短明了,每次只提一个问题。
你的回应一般在50字以内。
在聊天持续一段时间后,你可以发送包含TERMINATE的消息结束讨论。""",
    )
    
    # 设置终止条件,当任一回复中提到TERMINATE时结束对话
    termination = TextMentionTermination("TERMINATE")
    
    # 创建轮流对话组,所有角色轮流发言
    group_chat = RoundRobinGroupChat(
        [moderator_agent, host_agent, expert_agent, funny_agent, critic_agent],
        termination_condition=termination
    )
    
    # 游戏讨论主题
    initial_task = """
    大家好!欢迎来到今天的游戏直播间!今天我们将一起讨论最近非常火爆的《艾尔登法环》游戏。
    这款游戏有着极高的难度和开放世界设计,有人说它是近年来最好的RPG游戏,也有人觉得它太难了根本不适合普通玩家。
    大家对这款游戏有什么看法呢?来聊聊吧!
    
    我们可以从游戏的难度系统谈起,你们觉得高难度是这类游戏的魅力所在,还是应该提供更多难度选项?
    """
    
    # 在控制台运行交互,实时显示生成过程
    await Console(group_chat.run_stream(task=initial_task))
    
    # 关闭模型客户端
    await model_client.close()

# 运行异步主函数
if __name__ == "__main__":
    asyncio.run(main())

模拟聊天室简易demo

templates/chatroom.html前端:

html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>AI聊天直播间</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        #chatbox { height: 500px; overflow-y: scroll; border: 1px solid #ddd; padding: 10px; margin-bottom: 15px; background-color: #f9f9f9; }
        #prompt-box { width: 100%; padding: 10px; border: 1px solid #ddd; margin-bottom: 10px; }
        .message { margin: 8px 0; padding: 8px; border-radius: 4px; }
        
        /* 系统消息样式 */
        .system { color: #2ecc71; padding: 5px; font-size: 0.8em;}
        
        /* 通知消息样式 */
        .notification { color: #7f8c8d; padding: 5px; font-size: 0.8em;}
        
        /* 气泡样式 */
        .chat-bubble { 
            position: relative; 
            padding: 10px 15px; 
            border-radius: 15px; 
            margin: 10px 0; 
            max-width: 80%; 
            box-shadow: 0 1px 2px rgba(0,0,0,0.1);
            background-color: rgb(230, 230, 230);
        }
        
        /* 不同角色的气泡颜色 */
        .game-host { color: white; background-color: #e74c3c; margin-left: auto; }
        .game-expert { color: white; background-color: #3498db; margin-left: auto; }
        .funny-viewer { color: white; background-color: #f39c12; margin-left: auto; }
        .game-critic { color: white; background-color: #9b59b6; margin-left: auto; }
        .user { color: white; background-color: #7f8c8d; margin-right: auto; }
        
        .control-panel { margin-bottom: 15px; }
        .btn { padding: 10px 20px; margin-right: 10px; border: none; border-radius: 4px; cursor: pointer; font-weight: bold; }
        .btn-start { background-color: #2ecc71; color: white; }
        .btn-stop { background-color: #e74c3c; color: white; }
        h2 { color: #34495e; }
        .label { font-weight: bold; margin-bottom: 5px; display: block; }
        
        /* 发送者名称 */
        .sender-name {
            font-weight: bold;
            margin-bottom: 5px;
            font-size: 0.9em;
        }
    </style>
</head>
<body>

    <h2>AI聊天直播间 - 游戏讨论</h2>
    
    <div class="control-panel">
        <button id="start-btn" class="btn btn-start" onclick="startChat()">开始聊天</button>
        <button id="stop-btn" class="btn btn-stop" onclick="stopChat()" disabled>停止聊天</button>
    </div>
    
    <div id="chatbox"></div>
    
    <div>
        <label for="prompt-box" class="label">管理员系统提示 (引导聊天方向):</label>
        <textarea id="prompt-box" rows="3" placeholder="输入系统提示来引导话题方向..."></textarea>
        <button class="btn btn-start" onclick="sendSystemPrompt()">发送系统提示</button>
    </div>
    
    <script>
        // 获取当前主机和端口
        const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
        const wsUrl = `${protocol}//${window.location.host}/ws`;
        const ws = new WebSocket(wsUrl);
        
        // 添加回车键发送功能
        document.getElementById("prompt-box").addEventListener("keydown", function(event) {
            // 检查是否按下的是回车键且没有按住Shift键
            if (event.key === "Enter" && !event.shiftKey) {
                event.preventDefault(); // 阻止默认的换行行为
                sendSystemPrompt(); // 发送消息
            }
        });
        
        ws.onmessage = function(event) {
            const data = JSON.parse(event.data);
            const chatbox = document.getElementById("chatbox");
            const message = document.createElement("div");
            
            // 根据消息类型设置不同的样式
            if (data.type === "system") {
                // 系统消息,使用居中浅绿色文本
                message.className = "system";
                message.textContent = `系统提示: ${data.content}`;
            } else if (data.type === "notification") {
                // 通知消息,使用灰色文本
                message.className = "notification";
                message.textContent = data.content;
            } else if (data.type === "chat") {
                // 聊天消息,使用气泡样式
                message.className = "chat-bubble";
                
                // 为不同角色添加不同的样式
                if (data.sender) {
                    const senderClass = data.sender.replace(/ /g, "-").toLowerCase();
                    message.classList.add(senderClass);
                    
                    // 添加发送者名称
                    const senderName = document.createElement("div");
                    senderName.className = "sender-name";
                    senderName.textContent = data.sender;
                    message.appendChild(senderName);
                } else {
                    message.classList.add("user");
                }
                
                // 添加消息内容
                const contentElem = document.createElement("div");
                contentElem.textContent = data.content;
                message.appendChild(contentElem);
            } else {
                // 其他类型的消息
                message.className = "message";
                message.textContent = data.content;
            }
            
            chatbox.appendChild(message);
            chatbox.scrollTop = chatbox.scrollHeight;
        };
        
        function startChat() {
            ws.send(JSON.stringify({command: "start"}));
            document.getElementById("start-btn").disabled = true;
            document.getElementById("stop-btn").disabled = false;
        }
        
        function stopChat() {
            ws.send(JSON.stringify({command: "stop"}));
            document.getElementById("start-btn").disabled = false;
            document.getElementById("stop-btn").disabled = true;
        }
        
        function sendSystemPrompt() {
            const promptBox = document.getElementById("prompt-box");
            const prompt = promptBox.value.trim();
            
            if (prompt) {
                ws.send(JSON.stringify({
                    command: "system",
                    content: prompt
                }));
                promptBox.value = "";
            }
        }
    </script>

</body>
</html>
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import traceback
import asyncio
import json
import os
import re
import random
from autogen_agentchat.agents import AssistantAgent
from autogen_ext.models.openai import OpenAIChatCompletionClient
from autogen_core.models import ModelFamily, ModelInfo

# 设置OpenAI API密钥和基础URL
os.environ["OPENAI_API_KEY"] = "sk-6LsZB5NLIfnIfrb30tPCRUBOh61sdRkiFRH500ukWf9hEl6w"
# os.environ["OPENAI_BASE_URL"] = "https://newapi.ipzu.com/v1"
os.environ["OPENAI_BASE_URL"] = "http://192.168.1.163:8000/v1"

# 创建FastAPI实例
app = FastAPI()

# WebSocket连接列表
connections: list[WebSocket] = []

# 聊天状态
chat_active = False
chat_task = None

# 聊天历史记录,这是前端UI的显示,实际的上下文在current_context中
chat_history: list[dict[str, any]] = []

# 创建自定义模型信息
qwen_model_info = ModelInfo(
    vision=False, function_calling=True, json_output=True, family=ModelFamily.UNKNOWN
)

# 创建模型客户端
model_client = OpenAIChatCompletionClient(
    model="Qwen/Qwen2.5-7B-Instruct-1M",
    model_info=qwen_model_info,
    context_window=8192,  # 根据实际模型上下文窗口大小调整
    temperature=0.4,
    top_p=0.7,
)


# 创建一个核验模型是否抽风的函数
def check_model_stability(text: str) -> bool:
    """
    检查模型是否抽风,如果通过测试则返回True,否则返回False
    """
    if not text:
        print("模型产生空文本")
        return False  # 空文本不通过

    # 1. 检测连续空白或特殊空白字符过长
    if re.search(r"[\s︊]{%d,}" % 100, text):
        print(f"模型产生连续空白或特殊空白字符过长:{text}")
        return False

    # 正则表达式,检查是否存在重复6轮的相同内容
    pattern = re.compile(r"(.{1,10}?)(?:\1){" + str(6) + r",}")
    if pattern.search(text):
        # 如果存在,则认为模型抽风了
        print(f"模型抽风循环:{text}")
        return False

    # 2. 整体空白比例过高
    blank_count = sum(1 for c in text if c.isspace() or c == "︊")
    if blank_count / len(text) > 0.3:
        print(f"模型产生大量空白:{text}")
        return False

    # 如果以上都没有问题,则认为模型正常
    return True


# 创建角色
def create_agents():
    # 创建游戏主播角色
    host_agent = AssistantAgent(
        name="game_host",
        model_client=model_client,
        description="游戏直播间的主播,开朗活泼,语气充满活力",
        system_message="""你是一名受欢迎的游戏主播,性格开朗活泼,语气充满活力。
你负责引导话题,回应观众和嘉宾的发言。
使用emoji表情和网络流行语,但不要过度。
你的回应简短有趣,通常在50字以内。
只在聊天内容与你相关或需要你回应时发言。""",
    )

    # 创建游戏专家角色
    expert_agent = AssistantAgent(
        name="game_expert",
        model_client=model_client,
        description="游戏资深专家,冷静专业,擅长分析游戏机制",
        system_message="""你是一位资深游戏专家,只在谈论游戏机制、攻略或游戏历史时发言。
你的语气冷静专业,喜欢分析游戏设计和玩法细节。
不参与与游戏无关的闲聊。
回应应简洁但内容丰富,通常在100字以内。
只在讨论游戏技术内容时发言。""",
    )

    # 创建搞笑观众角色
    funny_agent = AssistantAgent(
        name="funny_viewer",
        model_client=model_client,
        description="直播间的搞笑观众,总是发表幽默的评论和笑话",
        system_message="""你是直播间的搞笑观众,总是发表幽默的评论和笑话。
你会对任何严肃的讨论进行调侃,经常使用夸张的表达和自嘲。
你的回应非常简短,通常在30字以内,充满网络流行语和表情符号。
你不会回应每条消息,只在有机会插入笑点时发言。""",
    )

    # 创建游戏评论家角色
    critic_agent = AssistantAgent(
        name="game_critic",
        model_client=model_client,
        description="尖锐的游戏评论家,喜欢指出问题和提出批评",
        system_message="""你是一位尖锐的游戏评论家,只在讨论游戏评价、行业问题或游戏缺点时发言。
你的语气犀利直接,喜欢指出问题和提出批评。
不参与纯粹的娱乐性闲聊。
回应应简洁明了,通常在80字以内。
只在讨论游戏问题或有批评机会时发言。""",
    )

    return {
        "game_host": host_agent,
        "game_expert": expert_agent,
        "funny_viewer": funny_agent,
        "game_critic": critic_agent,
    }


# 创建角色字典
agents = create_agents()

# 初始话题
default_topic = """
大家好!欢迎来到今天的游戏直播间!今天我们将一起讨论最近非常火爆的《德州扑克》游戏。
大家对这款游戏有什么看法呢?来聊聊吧!
"""

# 当前系统提示
current_system_prompt = default_topic


# 向所有连接的客户端广播消息
async def broadcast_message(message: dict[str, any]):
    if not connections:
        return

    for connection in connections:
        try:
            await connection.send_text(json.dumps(message))
        except Exception as e:
            print(f"Error broadcasting message: {e}")


# 根路径返回聊天室页面
@app.get("/")
async def read_root():
    return FileResponse("templates/chatroom.html")


# 处理WebSocket连接
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    connections.append(websocket)

    # 发送历史消息记录
    for message in chat_history:
        await websocket.send_text(json.dumps(message))

    try:
        while True:
            # 接收客户端消息
            data = await websocket.receive_text()
            try:
                # 解析JSON数据
                command_data = json.loads(data)
                command = command_data.get("command", "")

                if command == "start":
                    # 开始聊天
                    await start_chat()
                elif command == "stop":
                    # 停止聊天
                    await stop_chat()
                elif command == "system":
                    # 设置系统提示
                    content = command_data.get("content", "")
                    if content:
                        await set_system_prompt(content)
                else:
                    # 未知命令,当作普通消息处理
                    user_message = {"type": "chat", "sender": "User", "content": data}
                    chat_history.append(user_message)
                    await broadcast_message(user_message)
            except json.JSONDecodeError:
                # 不是JSON格式,当作普通消息处理
                user_message = {"type": "chat", "sender": "User", "content": data}
                chat_history.append(user_message)
                await broadcast_message(user_message)
    except WebSocketDisconnect:
        connections.remove(websocket)


# 设置系统提示
async def set_system_prompt(prompt: str):
    global current_system_prompt, chat_history
    current_system_prompt = prompt

    # 移除之前的所有系统消息
    chat_history = [msg for msg in chat_history if msg.get("type") != "system"]

    # 创建系统提示消息
    system_message = {"type": "system", "content": prompt}

    # 添加到历史记录并广播
    chat_history.append(system_message)
    await broadcast_message(system_message)

    # 如果聊天已经激活,则静默停止当前聊天并用新的系统提示重新开始
    if chat_active:
        await silent_stop_chat()
        await asyncio.sleep(1)  # 等待一秒,确保聊天已完全停止
        await silent_start_chat()


# 启动聊天(带通知)
async def start_chat():
    global chat_active, chat_task, chat_history

    if chat_active:
        return

    chat_active = True

    # 移除之前的所有系统消息
    chat_history = [msg for msg in chat_history if msg.get("type") != "system"]

    # 发送初始系统提示到聊天界面
    system_message = {"type": "system", "content": current_system_prompt}
    chat_history.append(system_message)
    await broadcast_message(system_message)

    # 创建异步任务
    chat_task = asyncio.create_task(run_chat_loop())

    # 发送通知
    notification = {"type": "notification", "content": "AI开始..."}
    await broadcast_message(notification)


# 静默启动聊天(不发送通知)
async def silent_start_chat():
    global chat_active, chat_task, chat_history

    if chat_active:
        return

    chat_active = True

    # 创建异步任务
    chat_task = asyncio.create_task(run_chat_loop())


# 停止聊天(带通知)
async def stop_chat():
    global chat_active, chat_task

    if not chat_active:
        return

    chat_active = False
    if chat_task:
        chat_task.cancel()
        chat_task = None

    # 发送通知
    notification = {"type": "notification", "content": "AI停止"}
    await broadcast_message(notification)


# 静默停止聊天(不发送通知)
async def silent_stop_chat():
    global chat_active, chat_task

    if not chat_active:
        return

    chat_active = False
    if chat_task:
        chat_task.cancel()
        chat_task = None


# 角色回合制聊天循环
async def run_chat_loop():
    agent_keys = list(agents.keys())

    # 确保使用最新的系统提示
    current_context = ""

    try:
        # 主循环
        turn = 0
        while chat_active:
            # 角色轮流发言
            for agent_key in agent_keys:
                if not chat_active:
                    break

                try:
                    # 让特定角色思考并回复

                    # 概率性回应
                    if should_respond():
                        print("开始请求一次ai:")
                        try:
                            # 显式提醒AI注意系统提示中的话题
                            agent_response = await agents[agent_key].run(
                                task=f"""根据以下系统提示和对话历史,作为{get_agent_display_name(agent_key)}回应:
        系统提示: {current_system_prompt}
        请严格遵循系统提示中指定的话题进行回应。只生成纯文本内容,不要超过60字符,不要使用任何Markdown、HTML或排版格式。
        对话历史:
        {current_context}"""
                            )
                            
                            # 先提取文本内容
                            content = extract_message_content(agent_response)
                            
                            # 对提取的文本内容进行稳定性检查
                            if check_model_stability(content):
                                # 随机等待2-4秒
                                await asyncio.sleep(random.uniform(2, 4))
                                await send_agent_message(agent_key, content)
                                current_context += f"\n\n{get_agent_display_name(agent_key)}: {content}"
                                # 如果超过3000字符,始终只取最后3000字符
                                current_context = current_context[-3000:]

                        except Exception as e:
                            print(f"出现错误: {agent_key}: {e}")
                            import traceback
                            print(f"详细错误信息: {traceback.format_exc()}")

                except Exception as e:
                    print(f"出现错误 {agent_key}: {e}")
                    print(f"详细错误信息: {traceback.format_exc()}")  # 打印完整堆栈跟踪

            turn += 1

    except asyncio.CancelledError:
        print("循环聊天已取消")
    except Exception as e:
        print(f"聊天循环出错: {e}")


# 发送角色消息
async def send_agent_message(agent_key: str, content: str):
    display_name = get_agent_display_name(agent_key)

    message = {"type": "chat", "sender": display_name, "content": content}

    chat_history.append(message)
    await broadcast_message(message)


# 辅助函数:获取角色显示名称
def get_agent_display_name(agent_key: str) -> str:
    display_names = {
        "game_host": "游戏主播",
        "game_expert": "游戏专家",
        "funny_viewer": "搞笑观众",
        "game_critic": "游戏评论家",
    }
    return display_names.get(agent_key, agent_key)


# 辅助函数:随机决定是否回应
def should_respond() -> bool:
    import random

    return random.random() < 0.1  # 10%的概率回应


# 辅助函数:从TaskResult对象中提取文本内容
def extract_message_content(task_result):
    """从TaskResult对象中提取实际的消息内容"""
    # 如果已经是字符串,直接返回
    if isinstance(task_result, str):
        return task_result

    try:
        # 尝试获取最后一条消息的内容
        if hasattr(task_result, "messages") and task_result.messages:
            last_message = task_result.messages[-1]
            if hasattr(last_message, "content"):
                return last_message.content

        # 如果上面的方法失败,尝试将整个对象转为字符串并提取有用部分
        result_str = str(task_result)
        if "content=" in result_str:
            parts = result_str.split("content=")
            if len(parts) > 1:
                content_part = parts[-1].strip("'").strip('"')
                # 如果内容后面还有其他属性,只取到下一个属性之前
                if ", " in content_part:
                    content_part = content_part.split(", ")[0]
                return content_part.strip("'").strip('"')
    except Exception as e:
        print(f"提取消息内容时出错: {str(e)}")

    # 如果无法提取,返回原始对象的字符串表示
    return f"(无法解析的消息: {str(task_result)})"


# 将静态文件挂载在/static路径上,而不是根路径
app.mount("/static", StaticFiles(directory="templates"), name="static")

# 启动服务器
if __name__ == "__main__":
    import uvicorn

    uvicorn.run(app, host="0.0.0.0", port=8000)