Spaces:
Sleeping
Sleeping
Commit
·
e7d5fd1
0
Parent(s):
Initial commit for Hugging Face Space
Browse files- .gitignore +21 -0
- Dockerfile +29 -0
- README.md +202 -0
- docs/修改记录.md +53 -0
- docs/更新计划.md +104 -0
- docs/项目规划.md +141 -0
- requirements.txt +4 -0
- src/app.py +473 -0
- src/debate_controller.py +454 -0
- src/model_interface.py +376 -0
- src/test.py +0 -0
- src/旧文件/1_辩论系统.py +414 -0
- src/旧文件/4_显示界面.py +347 -0
- src/旧文件/5_GUI测试.py +289 -0
- src/旧文件/6_辩论系统GUI.py +732 -0
- templates/index.html +178 -0
.gitignore
ADDED
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 忽略Python的缓存文件
|
2 |
+
__pycache__/
|
3 |
+
*.pyc
|
4 |
+
*.pyo
|
5 |
+
|
6 |
+
# 忽略日志文件和目录
|
7 |
+
logs/
|
8 |
+
*.log
|
9 |
+
|
10 |
+
# 忽略输出目录
|
11 |
+
output/
|
12 |
+
|
13 |
+
# 忽略数据目录(如果数据文件很大或不需要在线上运行)
|
14 |
+
data/
|
15 |
+
|
16 |
+
# 忽略本地开发环境文件
|
17 |
+
.venv/
|
18 |
+
venv/
|
19 |
+
env/
|
20 |
+
*.env
|
21 |
+
.vscode/
|
Dockerfile
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 使用官方Python 3.9 slim版本作为基础镜像
|
2 |
+
FROM python:3.9-slim
|
3 |
+
|
4 |
+
# 创建一个非root用户,并切换到该用户,增强安全性
|
5 |
+
RUN useradd -m -u 1000 user
|
6 |
+
USER user
|
7 |
+
ENV PATH="/home/user/.local/bin:$PATH"
|
8 |
+
|
9 |
+
# 设置容器内的工作目录
|
10 |
+
WORKDIR /app
|
11 |
+
|
12 |
+
# 将依赖文件复制到工作目录
|
13 |
+
# 使用 --chown=user 确保文件所有者为我们创建的非root用户
|
14 |
+
COPY --chown=user ./requirements.txt requirements.txt
|
15 |
+
|
16 |
+
# 安装项目依赖
|
17 |
+
RUN pip install --no-cache-dir --upgrade -r requirements.txt
|
18 |
+
|
19 |
+
# 将src目录下的所有代码复制到工作目录的src子目录中
|
20 |
+
COPY --chown=user ./src /app/src
|
21 |
+
|
22 |
+
# 暴露端口,让Hugging Face平台可以访问我们的应用
|
23 |
+
EXPOSE 7860
|
24 |
+
|
25 |
+
# 容器启动时运行的命令
|
26 |
+
# 运行src目录下的app.py文件中的app实例
|
27 |
+
# --host 0.0.0.0 使其可以从外部访问
|
28 |
+
# --port 7860 使用Hugging Face指定的端口
|
29 |
+
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "7860"]
|
README.md
ADDED
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI大模型辩论系统
|
2 |
+
|
3 |
+
## 项目简介
|
4 |
+
|
5 |
+
本项目是一个可以让两个AI大模型(ZhipuAI/GLM-4.5和deepseek-ai/DeepSeek-V3.1)根据给定话题进行实时辩论的系统。用户将能够实时观看两个模型之间的对话内容,并可以控制辩论的进行。
|
6 |
+
|
7 |
+
## 功能特点
|
8 |
+
|
9 |
+
- 支持两个AI大模型之间的实时辩论
|
10 |
+
- 可自定义辩论话题和轮数
|
11 |
+
- 实时显示辩论内容
|
12 |
+
- 支持暂停、恢复和停止辩论
|
13 |
+
- 保存完整的辩论记录
|
14 |
+
- 提供多种显示模式(简单控制台、控制台实时显示、文件保存、复合显示)
|
15 |
+
- 可查看历史辩论记录
|
16 |
+
- 支持流式输出,实时显示模型生成内容
|
17 |
+
- 提供图形用户界面(GUI),便于使用和分享
|
18 |
+
|
19 |
+
## 系统要求
|
20 |
+
|
21 |
+
- Python 3.7+
|
22 |
+
- requests库
|
23 |
+
- tkinter库(用于GUI界面)
|
24 |
+
|
25 |
+
## 安装说明
|
26 |
+
|
27 |
+
1. 克隆或下载本项目
|
28 |
+
2. 安装依赖包:
|
29 |
+
```
|
30 |
+
pip install -r requirements.txt
|
31 |
+
```
|
32 |
+
|
33 |
+
## 使用方法
|
34 |
+
|
35 |
+
### 命令行版本
|
36 |
+
|
37 |
+
在项目根目录下运行主程序:
|
38 |
+
|
39 |
+
```
|
40 |
+
python src/1_辩论系统.py
|
41 |
+
```
|
42 |
+
|
43 |
+
### 命令行参数
|
44 |
+
|
45 |
+
```
|
46 |
+
python src/1_辩论系统.py [--api-key API_KEY] [--display-type DISPLAY_TYPE]
|
47 |
+
```
|
48 |
+
|
49 |
+
参数说明:
|
50 |
+
- `--api-key`: API密钥(默认:ms-b4690538-3224-493a-8f5b-4073d527f788)
|
51 |
+
- `--display-type`: 显示类型,可选值:simple, console, file, composite(默认:composite)
|
52 |
+
|
53 |
+
### GUI版本
|
54 |
+
|
55 |
+
在项目根目录下运行GUI测试程序:
|
56 |
+
|
57 |
+
```
|
58 |
+
python src/5_GUI测试.py
|
59 |
+
```
|
60 |
+
|
61 |
+
GUI界面功能:
|
62 |
+
- 选择模型(GLM-4.5或DeepSeek-V3.1)
|
63 |
+
- 输入提示文本
|
64 |
+
- 实时流式显示模型输出
|
65 |
+
- 开始/停止生成按钮
|
66 |
+
- 清空输出和保存输出功能
|
67 |
+
|
68 |
+
### 操作流程
|
69 |
+
|
70 |
+
1. 启动系统后,会显示主菜单:
|
71 |
+
- 1. 开始新辩论
|
72 |
+
- 2. 查看历史辩论记录
|
73 |
+
- 3. 系统设置
|
74 |
+
- 4. 退出系统
|
75 |
+
|
76 |
+
2. 选择"开始新辩论"后,按提示输入:
|
77 |
+
- 辩论话题
|
78 |
+
- 辩论轮数(默认5轮)
|
79 |
+
- 首发模型(默认GLM-4.5)
|
80 |
+
|
81 |
+
3. 确认参数后,辩论将自动开始,您可以实时观看两个模型的辩论内容。
|
82 |
+
|
83 |
+
4. 在辩论过程中,您可以输入以下命令控制辩论:
|
84 |
+
- `stop`: 停止辩论
|
85 |
+
- `pause`: 暂停辩论
|
86 |
+
- `resume`: 恢复辩论
|
87 |
+
|
88 |
+
5. 辩论结束后,系统会自动保存辩论记录。
|
89 |
+
|
90 |
+
## 项目结构
|
91 |
+
|
92 |
+
```
|
93 |
+
20250907_大模型辩论/
|
94 |
+
├── src/ # 源代码目录
|
95 |
+
│ ├── 1_辩论系统.py # 主程序文件
|
96 |
+
│ ├── 2_模型接口.py # 模型API接口封装
|
97 |
+
│ ├── 3_辩论控制器.py # 辩论流程控制
|
98 |
+
│ ├── 4_显示界面.py # 实时显示界面
|
99 |
+
│ └── 5_GUI测试.py # GUI测试程序
|
100 |
+
├── data/ # 数据目录
|
101 |
+
│ └── 辩论话题.txt # 存储辩论话题
|
102 |
+
├── output/ # 输出目录
|
103 |
+
│ ├── 辩论记录/ # 每次辩论的完整记录
|
104 |
+
│ └── 辩论统计/ # 辩论统计数据
|
105 |
+
├── logs/ # 日志目录
|
106 |
+
│ └── 系统运行日志.log # 系统运行日志
|
107 |
+
├── docs/ # 文档目录
|
108 |
+
│ ├── 项目规划.md # 项目规划文档
|
109 |
+
│ ├── 修改记录.md # 项目修改记录
|
110 |
+
│ └── 更新计划.md # 更新计划文档
|
111 |
+
├── models/ # 模型目录(预留)
|
112 |
+
├── sql/ # SQL脚本目录(预留)
|
113 |
+
├── requirements.txt # 依赖包列表
|
114 |
+
└── README.md # 本文档
|
115 |
+
```
|
116 |
+
|
117 |
+
## 模块说明
|
118 |
+
|
119 |
+
### 1_辩论系统.py
|
120 |
+
|
121 |
+
主程序模块,整合所有模块,提供用户交互界面。包含:
|
122 |
+
- 系统主菜单
|
123 |
+
- 辩论参数设置
|
124 |
+
- 历史记录查看
|
125 |
+
- 系统设置
|
126 |
+
|
127 |
+
### 2_模型接口.py
|
128 |
+
|
129 |
+
模型接口模块,封装两个大模型的API调用。包含:
|
130 |
+
- ModelInterface基类
|
131 |
+
- GLM45Interface类(GLM-4.5模型接口)
|
132 |
+
- DeepSeekV31Interface类(DeepSeek-V3.1模型接口)
|
133 |
+
- ModelManager类(模型管理器)
|
134 |
+
|
135 |
+
### 3_辩论控制器.py
|
136 |
+
|
137 |
+
辩论控制器模块,管理辩论流程和控制发言顺序。包含:
|
138 |
+
- DebateMessage类(辩论消息)
|
139 |
+
- DebateSession类(辩论会话)
|
140 |
+
- DebateController类(辩论控制器)
|
141 |
+
|
142 |
+
### 4_显示界面.py
|
143 |
+
|
144 |
+
显示界面模块,用于实时显示辩论内容。包含:
|
145 |
+
- ConsoleDisplay类(控制台显示界面)
|
146 |
+
- SimpleDisplay类(简单显示界面)
|
147 |
+
- FileDisplay类(文件显示界面)
|
148 |
+
- CompositeDisplay类(复合显示界面)
|
149 |
+
- DisplayManager类(显示管理器)
|
150 |
+
|
151 |
+
### 5_GUI测试.py
|
152 |
+
|
153 |
+
GUI测试模块,提供图形用户界面。包含:
|
154 |
+
- StreamOutputGUI类(流式输出GUI界面)
|
155 |
+
- 支持模型选择和流式输出显示
|
156 |
+
- 提供完整的用户交互功能
|
157 |
+
|
158 |
+
## API信息
|
159 |
+
|
160 |
+
- API基础地址:https://api-inference.modelscope.cn/v1/
|
161 |
+
- 支持的模型:
|
162 |
+
- ZhipuAI/GLM-4.5
|
163 |
+
- deepseek-ai/DeepSeek-V3.1
|
164 |
+
|
165 |
+
## 注意事项
|
166 |
+
|
167 |
+
1. 确保网络连接正常,能够访问API服务器。
|
168 |
+
2. 辩论过程中请耐心等待,模型响��可能需要一定时间。
|
169 |
+
3. 辩论记录会自动保存在`output/辩论记录/`目录下。
|
170 |
+
4. 系统日志会记录在`logs/`目录下,便于排查问题。
|
171 |
+
|
172 |
+
## 常见问题
|
173 |
+
|
174 |
+
### Q: 如何更换API密钥?
|
175 |
+
A: 可以通过命令行参数`--api-key`指定,或者在代码中修改默认值。
|
176 |
+
|
177 |
+
### Q: 如何更改显示模式?
|
178 |
+
A: 可以通过命令行参数`--display-type`指定,或者在系统设置中更改。
|
179 |
+
|
180 |
+
### Q: 辩论过程中出现错误怎么办?
|
181 |
+
A: 系统会自动记录错误信息到日志文件,可以查看`logs/`目录下的日志文件排查问题。
|
182 |
+
|
183 |
+
### Q: 如何查看历史辩论记录?
|
184 |
+
A: 在主菜单选择"查看历史辩论记录",然后选择要查看的记录即可。
|
185 |
+
|
186 |
+
## 开发计划
|
187 |
+
|
188 |
+
1. 增加更多模型支持
|
189 |
+
2. 优化辩论逻辑和提示词
|
190 |
+
3. 添加辩论评分功能
|
191 |
+
4. 开发图形用户界面
|
192 |
+
5. 支持更多输出格式(如HTML、PDF等)
|
193 |
+
|
194 |
+
## 许可证
|
195 |
+
|
196 |
+
本项目仅供学习和研究使用。
|
197 |
+
|
198 |
+
## 联系方式
|
199 |
+
|
200 |
+
如有问题或建议,请通过以下方式联系:
|
201 |
+
- 邮箱:[您的邮箱]
|
202 |
+
- GitHub:[您的GitHub地址]
|
docs/修改记录.md
ADDED
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# 项目修改记录
|
2 |
+
|
3 |
+
## 第1次修改
|
4 |
+
- 创建项目目录结构
|
5 |
+
- 实现模型接口模块 (2_模型接口.py)
|
6 |
+
- 封装ZhipuAI/GLM-4.5和moonshotai/Kimi-K2-Instruct-0905模型API
|
7 |
+
- 实现ModelManager类统一管理模型接口
|
8 |
+
- 添加错误处理和日志记录
|
9 |
+
- 实现辩论控制器模块 (3_辩论控制器.py)
|
10 |
+
- 设计DebateMessage类管理辩论消息
|
11 |
+
- 实现DebateSession类管理辩论会话
|
12 |
+
- 创建DebateController类控制辩论流程
|
13 |
+
- 支持多线程异步处理辩论
|
14 |
+
- 实现显示界面模块 (4_显示界面.py)
|
15 |
+
- 实现SimpleDisplay类简单显示
|
16 |
+
- 实现ConsoleDisplay类控制台实时显示
|
17 |
+
- 实现FileDisplay类文件保存显示
|
18 |
+
- 实现CompositeDisplay类复合显示
|
19 |
+
- 创建DisplayManager类统一管理显示界面
|
20 |
+
- 实现主程序模块 (1_辩论系统.py)
|
21 |
+
- 整合所有模块
|
22 |
+
- 提供用户交互界面
|
23 |
+
- 实现辩论参数设置
|
24 |
+
- 实现历史记录查看
|
25 |
+
- 实现系统设置
|
26 |
+
- 创建项目规划文档 (docs/项目规划.md)
|
27 |
+
- 创建requirements.txt文件
|
28 |
+
- 创建README.md文档
|
29 |
+
- 创建修改记录文档 (docs/修改记录.md)
|
30 |
+
|
31 |
+
## 第2次修改
|
32 |
+
- 创建更新计划文档 (docs/更新计划.md)
|
33 |
+
- 制定流式输出和GUI界面的实现方案
|
34 |
+
- 规划实施步骤和时间安排
|
35 |
+
- 分析技术风险并制定应对措施
|
36 |
+
|
37 |
+
## 第3次修改
|
38 |
+
- 修改模型接口模块 (2_模型接口.py)
|
39 |
+
- 添加流式输出支持,实现send_stream_request方法
|
40 |
+
- 为GLM45Interface和DeepSeekV31Interface类添加chat_stream方法
|
41 |
+
- 添加回调函数机制支持逐字输出
|
42 |
+
- 更新测试代码以验证流式输出功能
|
43 |
+
- 修复模型名称引用错误(kimi_k2改为deepseek_v31)
|
44 |
+
|
45 |
+
## 第4次修改
|
46 |
+
- 修改requirements.txt文件
|
47 |
+
- 添加tkinter依赖(用于GUI界面开发)
|
48 |
+
- 创建GUI测试程序 (src/5_GUI测试.py)
|
49 |
+
- 实现基于tkinter的图形用户界面
|
50 |
+
- 支持模型选择(GLM-4.5和DeepSeek-V3.1)
|
51 |
+
- 支持流式输出显示
|
52 |
+
- 提供开始/停止/清空/保存功能
|
53 |
+
- 添加状态栏和错误处理
|
docs/更新计划.md
ADDED
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI大模型辩论系统更新计划
|
2 |
+
|
3 |
+
## 目标
|
4 |
+
|
5 |
+
1. 实现大模型回复的流式输出,提升用户体验
|
6 |
+
2. 开发图形用户界面(GUI),使系统更易于使用和分享
|
7 |
+
|
8 |
+
## 当前系统分析
|
9 |
+
|
10 |
+
### 现有架构
|
11 |
+
- **模型接口模块**:使用requests库发送HTTP请求,一次性获取完整响应
|
12 |
+
- **辩论控制器模块**:管理辩论流程和模型交互
|
13 |
+
- **显示界面模块**:提供多种显示方式(简单控制台、控制台实时显示、文件保存、复合显示)
|
14 |
+
- **主程序模块**:整合所有模块,提供命令行界面
|
15 |
+
|
16 |
+
### 存在的问题
|
17 |
+
1. 当前模型接口使用非流式请求,需要等待模型完全生成回复后才能显示
|
18 |
+
2. 只有命令行界面,不利于分享和用户体验
|
19 |
+
|
20 |
+
## 实现方案
|
21 |
+
|
22 |
+
### 1. 流式输出实现
|
23 |
+
|
24 |
+
#### 1.1 修改模型接口模块 (2_模型接口.py)
|
25 |
+
- 添加支持流式输出的chat_stream方法
|
26 |
+
- 使用requests的stream=True参数
|
27 |
+
- 实现SSE(Server-Sent Events)解析
|
28 |
+
- 提供逐字输出的回调函数
|
29 |
+
|
30 |
+
#### 1.2 修改辩论控制器模块 (3_辩论控制器.py)
|
31 |
+
- 添加支持流式输出的辩论方法
|
32 |
+
- 修改_debate_loop方法以支持流式输出
|
33 |
+
- 更新DebateController类以处理流式回调
|
34 |
+
|
35 |
+
#### 1.3 修改显示界面模块 (4_显示界面.py)
|
36 |
+
- 更新显示界面以支持流式内容显示
|
37 |
+
- 添加流式显示的视觉效果(如打字机效果)
|
38 |
+
|
39 |
+
### 2. GUI界面实现
|
40 |
+
|
41 |
+
#### 2.1 技术选型
|
42 |
+
- 使用Python的tkinter库创建桌面GUI应用
|
43 |
+
- 或使用Flask/FastAPI创建Web应用
|
44 |
+
|
45 |
+
#### 2.2 功能设计
|
46 |
+
- 主界面:包含辩论话题输入、模型选择、轮数设置等
|
47 |
+
- 实时显示区域:显示辩论内容,支持流式输出效果
|
48 |
+
- 控制按钮:开始、暂停、停止、保存等
|
49 |
+
- 历史记录查看界面
|
50 |
+
- 设置界面:API密钥、显示设置等
|
51 |
+
|
52 |
+
#### 2.3 实现步骤
|
53 |
+
1. 创建GUI主窗口和布局
|
54 |
+
2. 实现辩论参数设置界面
|
55 |
+
3. 实现辩论内容显示区域
|
56 |
+
4. 实现控制按钮功能
|
57 |
+
5. 实现历史记录查看功能
|
58 |
+
6. 实现设置功能
|
59 |
+
|
60 |
+
## 实施计划
|
61 |
+
|
62 |
+
### 第一阶段:流式输出实现(预计2天)
|
63 |
+
1. 修改模型接口模块,添加流式输出支持
|
64 |
+
2. 修改辩论控制器模块,支持流式输出
|
65 |
+
3. 更新显示界面模块,支持流式显示
|
66 |
+
4. 测试流式输出功能
|
67 |
+
|
68 |
+
### 第二阶段:GUI界面开发(预计3天)
|
69 |
+
1. 设计GUI界面布局和功能
|
70 |
+
2. 实现主界面和参数设置
|
71 |
+
3. 实现辩论内容显示区域
|
72 |
+
4. 实现控制功能
|
73 |
+
5. 实现历史记录和设置功能
|
74 |
+
6. 测试GUI界面
|
75 |
+
|
76 |
+
### 第三阶段:集成测试和优化(预计1天)
|
77 |
+
1. 集成流式输出和GUI界面
|
78 |
+
2. 进行完整功能测试
|
79 |
+
3. 优化用户体验
|
80 |
+
4. 编写使用文档
|
81 |
+
|
82 |
+
## 预期效果
|
83 |
+
|
84 |
+
1. **流式输出**:用户可以实时看到模型生成的内容,提升交互体验
|
85 |
+
2. **GUI界面**:提供直观易用的图形界面,便于分享和使用
|
86 |
+
3. **兼容性**:保持原有命令行界面功能,同时提供GUI选项
|
87 |
+
4. **稳定性**:确保系统稳定运行,错误处理完善
|
88 |
+
|
89 |
+
## 风险评估和应对
|
90 |
+
|
91 |
+
### 技术风险
|
92 |
+
- **流式输出兼容性**:某些模型可能不支持流式输出
|
93 |
+
- 应对:提供降级方案,不支持流式时使用现有方式
|
94 |
+
|
95 |
+
- **GUI跨平台兼容性**:不同操作系统可能显示效果不同
|
96 |
+
- 应对:使用标准组件,进行多平台测试
|
97 |
+
|
98 |
+
### 时间风险
|
99 |
+
- **开发时间超期**:功能实现可能比预期复杂
|
100 |
+
- 应对:分阶段实现,优先核心功能
|
101 |
+
|
102 |
+
## 结论
|
103 |
+
|
104 |
+
通过实现流式输出和GUI界面,可以显著提升AI大模型辩论系统的用户体验和可用性。流式输出让用户能够实时看到模型生成的内容,增强交互感;GUI界面使系统更易于使用和分享。该更新计划分阶段实施,风险可控,预期效果良好。
|
docs/项目规划.md
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# AI大模型辩论系统项目规划
|
2 |
+
|
3 |
+
## 项目概述
|
4 |
+
|
5 |
+
本项目旨在创建一个可以让两个AI大模型(ZhipuAI/GLM-4.5和moonshotai/Kimi-K2-Instruct-0905)根据给定话题进行实时辩论的系统。用户将能够实时观看两个模型之间的对话内容,并可以控制辩论的进行。
|
6 |
+
|
7 |
+
## 系统架构
|
8 |
+
|
9 |
+
### 1. 核心组件
|
10 |
+
|
11 |
+
- **辩论控制器**:管理辩论流程,控制发言顺序,处理用户输入
|
12 |
+
- **模型接口**:与两个大模型API进行交互的封装
|
13 |
+
- **实时显示界面**:展示辩论内容的用户界面
|
14 |
+
- **日志记录系统**:记录辩论过程和结果
|
15 |
+
|
16 |
+
### 2. 数据流
|
17 |
+
|
18 |
+
1. 用户输入辩论话题和初始参数
|
19 |
+
2. 辩论控制器初始化辩论
|
20 |
+
3. 模型A生成初始论点
|
21 |
+
4. 模型B针对模型A的论点进行反驳
|
22 |
+
5. 模型A针对模型B的反驳进行回应
|
23 |
+
6. 循环进行,直到达到设定的轮数或用户手动停止
|
24 |
+
7. 实时显示每轮辩论内容
|
25 |
+
8. 记录完整辩论过程
|
26 |
+
|
27 |
+
## 技术实现方案
|
28 |
+
|
29 |
+
### 1. 目录结构
|
30 |
+
|
31 |
+
```
|
32 |
+
20250907_大模型辩论/
|
33 |
+
├── src/ # 源代码目录
|
34 |
+
│ ├── 1_辩论系统.py # 主程序文件
|
35 |
+
│ ├── 2_模型接口.py # 模型API接口封装
|
36 |
+
│ ├── 3_辩论控制器.py # 辩论流程控制
|
37 |
+
│ └── 4_显示界面.py # 实时显示界面
|
38 |
+
├── data/ # 数据目录
|
39 |
+
│ └── 辩论话题.txt # 存储辩论话题
|
40 |
+
├── output/ # 输出目录
|
41 |
+
│ ├── 辩论记录/ # 每次辩论的完整记录
|
42 |
+
│ └── 辩论统计/ # 辩论统计数据
|
43 |
+
├── logs/ # 日志目录
|
44 |
+
│ └── 系统运行日志.log # 系统运行日志
|
45 |
+
├── docs/ # 文档目录
|
46 |
+
│ └── 项目规划.md # 本文档
|
47 |
+
├── models/ # 模型目录(预留)
|
48 |
+
├── sql/ # SQL脚本目录(预留)
|
49 |
+
├── requirements.txt # 依赖包列表
|
50 |
+
└── README.md # 项目说明文档
|
51 |
+
```
|
52 |
+
|
53 |
+
### 2. 核心功能模块
|
54 |
+
|
55 |
+
#### 2.1 模型接口模块 (2_模型接口.py)
|
56 |
+
|
57 |
+
- 封装两个大模型的API调用
|
58 |
+
- 处理API请求和响应
|
59 |
+
- 错误处理和重试机制
|
60 |
+
- 支持自定义模型参数
|
61 |
+
|
62 |
+
#### 2.2 辩论控制器模块 (3_辩论控制器.py)
|
63 |
+
|
64 |
+
- 管理辩论流程和状态
|
65 |
+
- 控制发言顺序和轮次
|
66 |
+
- 处理辩论规则和逻辑
|
67 |
+
- 提供辩论开始、暂停、继续、结束等控制功能
|
68 |
+
|
69 |
+
#### 2.3 显示界面模块 (4_显示界面.py)
|
70 |
+
|
71 |
+
- 实时显示辩论内容
|
72 |
+
- 区分不同模型的发言
|
73 |
+
- 提供用户控制界面
|
74 |
+
- 支持滚动查看历史内容
|
75 |
+
|
76 |
+
#### 2.4 主程序模块 (1_辩论系统.py)
|
77 |
+
|
78 |
+
- 整合所有模块
|
79 |
+
- 处理用户输入
|
80 |
+
- 协调各模块工作
|
81 |
+
- 提供命令行界面
|
82 |
+
|
83 |
+
### 3. 辩论流程设计
|
84 |
+
|
85 |
+
1. **初始化阶段**
|
86 |
+
- 用户输入辩论话题
|
87 |
+
- 设置辩论轮数(默认为5轮)
|
88 |
+
- 设置每个模型的发言角色(正方/反方)
|
89 |
+
- 选择首发模型
|
90 |
+
|
91 |
+
2. **辩论阶段**
|
92 |
+
- 首发模型根据话题生成初始论点
|
93 |
+
- 对方模型针对论点进行反驳
|
94 |
+
- 首发模型再针对反驳进行回应
|
95 |
+
- 交替进行,直到达到设定轮数
|
96 |
+
|
97 |
+
3. **结束阶段**
|
98 |
+
- 显示辩论总结
|
99 |
+
- 保存辩论记录
|
100 |
+
- 生成辩论统计信息
|
101 |
+
|
102 |
+
### 4. 技术细节
|
103 |
+
|
104 |
+
#### 4.1 API调用
|
105 |
+
|
106 |
+
- 使用requests库进行HTTP请求
|
107 |
+
- API基础地址:https://api-inference.modelscope.cn/v1/
|
108 |
+
- API Key:ms-b4690538-3224-493a-8f5b-4073d527f788
|
109 |
+
- 模型标识:
|
110 |
+
- ZhipuAI/GLM-4.5
|
111 |
+
- moonshotai/Kimi-K2-Instruct-0905
|
112 |
+
|
113 |
+
#### 4.2 实时显示
|
114 |
+
|
115 |
+
- 使用多线程实现异步处理
|
116 |
+
- 主线程处理用户交互
|
117 |
+
- 子线程处理API调用和辩论逻辑
|
118 |
+
- 使用队列传递辩论内容
|
119 |
+
|
120 |
+
#### 4.3 日志记录
|
121 |
+
|
122 |
+
- 记录系统运行状态
|
123 |
+
- 记录API调用详情
|
124 |
+
- 记录每轮辩论内容
|
125 |
+
- 记录错误和异常信息
|
126 |
+
|
127 |
+
## 实施计划
|
128 |
+
|
129 |
+
1. **第一阶段**:创建项目结构,实现模型接口模块
|
130 |
+
2. **第二阶段**:实现辩论控制器模块
|
131 |
+
3. **第三阶段**:实现显示界面模块
|
132 |
+
4. **第四阶段**:整合所有模块,完善主程序
|
133 |
+
5. **第五阶段**:测试和优化
|
134 |
+
|
135 |
+
## 预期效果
|
136 |
+
|
137 |
+
- 用户可以输入任意话题,观看两个AI模型的辩论
|
138 |
+
- 实时显示辩论内容,无需等待全部完成
|
139 |
+
- 可以控制辩论的进行(暂停、继续、结束)
|
140 |
+
- 完整记录辩论过程,便于后续分析
|
141 |
+
- 系统稳定运行,错误处理完善
|
requirements.txt
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
fastapi
|
2 |
+
uvicorn[standard]
|
3 |
+
requests
|
4 |
+
jinja2
|
src/app.py
ADDED
@@ -0,0 +1,473 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
#!/usr/bin/env python3
|
2 |
+
# -*- coding: utf-8 -*-
|
3 |
+
"""
|
4 |
+
AI大模型辩论系统Web版本
|
5 |
+
基于FastAPI的Web应用,提供图形用户界面的辩论系统
|
6 |
+
"""
|
7 |
+
|
8 |
+
import os
|
9 |
+
import sys
|
10 |
+
import json
|
11 |
+
import logging
|
12 |
+
from datetime import datetime
|
13 |
+
from typing import Optional
|
14 |
+
import importlib.util
|
15 |
+
import asyncio
|
16 |
+
|
17 |
+
# 在代码开头强制设置终端编码为UTF-8
|
18 |
+
os.system('chcp 6001 > nul')
|
19 |
+
|
20 |
+
# 获取当前脚本文件所在目录的绝对路径
|
21 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
22 |
+
|
23 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
24 |
+
project_root = os.path.dirname(current_script_dir)
|
25 |
+
|
26 |
+
# 定义数据输入、输出和日志目录
|
27 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
28 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
29 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
30 |
+
|
31 |
+
# 确保目录存在
|
32 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
33 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
34 |
+
|
35 |
+
# 配置日志
|
36 |
+
logging.basicConfig(
|
37 |
+
level=logging.INFO,
|
38 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
39 |
+
handlers=[
|
40 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '辩论系统Web日志.log'), encoding='utf-8'),
|
41 |
+
logging.StreamHandler(sys.stdout)
|
42 |
+
]
|
43 |
+
)
|
44 |
+
logger = logging.getLogger(__name__)
|
45 |
+
|
46 |
+
# 捕获警告并记录到日志
|
47 |
+
logging.captureWarnings(True)
|
48 |
+
|
49 |
+
# 导入FastAPI相关模块
|
50 |
+
try:
|
51 |
+
from fastapi import FastAPI, Request, WebSocket, WebSocketDisconnect
|
52 |
+
from fastapi.responses import HTMLResponse
|
53 |
+
from fastapi.staticfiles import StaticFiles
|
54 |
+
from fastapi.templating import Jinja2Templates
|
55 |
+
import uvicorn
|
56 |
+
logger.info("FastAPI模块导入成功")
|
57 |
+
except ImportError as e:
|
58 |
+
logger.error(f"导入FastAPI模块失败: {str(e)}")
|
59 |
+
print("请安装FastAPI: pip install fastapi uvicorn")
|
60 |
+
sys.exit(1)
|
61 |
+
|
62 |
+
# 导入自定义模块
|
63 |
+
try:
|
64 |
+
# 动态导入模型接口模块
|
65 |
+
model_interface_path = os.path.join(current_script_dir, "model_interface.py")
|
66 |
+
model_interface_spec = importlib.util.spec_from_file_location("model_interface", model_interface_path)
|
67 |
+
model_interface = importlib.util.module_from_spec(model_interface_spec)
|
68 |
+
model_interface_spec.loader.exec_module(model_interface)
|
69 |
+
|
70 |
+
# 动态导入辩论控制器模块
|
71 |
+
debate_controller_path = os.path.join(current_script_dir, "debate_controller.py")
|
72 |
+
debate_controller_spec = importlib.util.spec_from_file_location("debate_controller", debate_controller_path)
|
73 |
+
debate_controller = importlib.util.module_from_spec(debate_controller_spec)
|
74 |
+
debate_controller_spec.loader.exec_module(debate_controller)
|
75 |
+
|
76 |
+
# 从模块中获取需要的类
|
77 |
+
ModelManager = model_interface.ModelManager
|
78 |
+
DebateMessage = debate_controller.DebateMessage
|
79 |
+
DebateSession = debate_controller.DebateSession
|
80 |
+
|
81 |
+
except Exception as e:
|
82 |
+
logger.error(f"导入模块失败: {str(e)}")
|
83 |
+
sys.exit(1)
|
84 |
+
|
85 |
+
# 创建FastAPI应用
|
86 |
+
app = FastAPI(title="AI大模型辩论系统", description="基于FastAPI的AI大模型辩论系统Web版本")
|
87 |
+
|
88 |
+
# 设置静态文件目录
|
89 |
+
static_dir = os.path.join(project_root, "static")
|
90 |
+
os.makedirs(static_dir, exist_ok=True)
|
91 |
+
app.mount("/static", StaticFiles(directory=static_dir), name="static")
|
92 |
+
|
93 |
+
# 设置模板目录
|
94 |
+
templates_dir = os.path.join(project_root, "templates")
|
95 |
+
os.makedirs(templates_dir, exist_ok=True)
|
96 |
+
templates = Jinja2Templates(directory=templates_dir)
|
97 |
+
|
98 |
+
# 全局变量
|
99 |
+
api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
100 |
+
model_manager = ModelManager(api_key)
|
101 |
+
active_debate_session = None
|
102 |
+
active_websocket = None
|
103 |
+
|
104 |
+
@app.get("/", response_class=HTMLResponse)
|
105 |
+
async def read_root(request: Request):
|
106 |
+
"""主页路由,返回Web界面"""
|
107 |
+
return templates.TemplateResponse("index.html", {
|
108 |
+
"request": request,
|
109 |
+
"title": "AI大模型辩论系统"
|
110 |
+
})
|
111 |
+
|
112 |
+
@app.get("/api/status")
|
113 |
+
async def get_status():
|
114 |
+
"""获取系统状态"""
|
115 |
+
return {
|
116 |
+
"status": "running",
|
117 |
+
"timestamp": datetime.now().isoformat(),
|
118 |
+
"models": ["glm45", "deepseek_v31"]
|
119 |
+
}
|
120 |
+
|
121 |
+
@app.websocket("/ws")
|
122 |
+
async def websocket_endpoint(websocket: WebSocket):
|
123 |
+
"""WebSocket端点,用于实时通信"""
|
124 |
+
global active_websocket
|
125 |
+
await websocket.accept()
|
126 |
+
active_websocket = websocket
|
127 |
+
|
128 |
+
try:
|
129 |
+
while True:
|
130 |
+
data = await websocket.receive_text()
|
131 |
+
# 处理接收到的消息
|
132 |
+
await handle_websocket_message(websocket, data)
|
133 |
+
except WebSocketDisconnect:
|
134 |
+
logger.info("WebSocket连接已断开")
|
135 |
+
active_websocket = None
|
136 |
+
except Exception as e:
|
137 |
+
logger.error(f"WebSocket处理错误: {str(e)}")
|
138 |
+
active_websocket = None
|
139 |
+
|
140 |
+
async def handle_websocket_message(websocket: WebSocket, message: str):
|
141 |
+
"""处理WebSocket消息"""
|
142 |
+
try:
|
143 |
+
data = json.loads(message)
|
144 |
+
action = data.get("action")
|
145 |
+
|
146 |
+
if action == "start_debate":
|
147 |
+
await start_debate(websocket, data)
|
148 |
+
elif action == "stop_debate":
|
149 |
+
await stop_debate(websocket)
|
150 |
+
elif action == "pause_debate":
|
151 |
+
await pause_debate(websocket)
|
152 |
+
elif action == "resume_debate":
|
153 |
+
await resume_debate(websocket)
|
154 |
+
else:
|
155 |
+
await websocket.send_text(json.dumps({
|
156 |
+
"type": "error",
|
157 |
+
"message": f"未知操作: {action}"
|
158 |
+
}))
|
159 |
+
except json.JSONDecodeError:
|
160 |
+
await websocket.send_text(json.dumps({
|
161 |
+
"type": "error",
|
162 |
+
"message": "无效的JSON格式"
|
163 |
+
}))
|
164 |
+
except Exception as e:
|
165 |
+
logger.error(f"处理WebSocket消息时出错: {str(e)}")
|
166 |
+
await websocket.send_text(json.dumps({
|
167 |
+
"type": "error",
|
168 |
+
"message": f"处理消息时出错: {str(e)}"
|
169 |
+
}))
|
170 |
+
|
171 |
+
async def start_debate(websocket: WebSocket, data: dict):
|
172 |
+
"""开始辩论"""
|
173 |
+
global active_debate_session
|
174 |
+
loop = asyncio.get_event_loop()
|
175 |
+
|
176 |
+
try:
|
177 |
+
topic = data.get("topic", "人工智能是否会取代人类的工作")
|
178 |
+
rounds = int(data.get("rounds", 5))
|
179 |
+
first_model = data.get("first_model", "glm45")
|
180 |
+
initial_prompt = data.get("initial_prompt", "").strip() # 新增:获取自定义初始提示
|
181 |
+
|
182 |
+
await websocket.send_text(json.dumps({ "type": "debate_started", "message": "辩论已开始", "topic": topic, "rounds": rounds, "first_model": first_model }))
|
183 |
+
|
184 |
+
active_debate_session = DebateSession(topic, rounds, first_model)
|
185 |
+
|
186 |
+
# 新增:如果存在自定义初始提示,则添加到会话历史中
|
187 |
+
if initial_prompt:
|
188 |
+
active_debate_session.add_message(DebateMessage("user", initial_prompt, "system"))
|
189 |
+
|
190 |
+
model_a_name = first_model
|
191 |
+
model_b_name = 'deepseek_v31' if model_a_name == 'glm45' else 'glm45'
|
192 |
+
model_a = model_manager.get_model(model_a_name)
|
193 |
+
model_b = model_manager.get_model(model_b_name)
|
194 |
+
|
195 |
+
active_debate_session.start_time = datetime.now()
|
196 |
+
|
197 |
+
speakers = [(model_a_name, model_a), (model_b_name, model_b)]
|
198 |
+
|
199 |
+
# Main debate loop
|
200 |
+
for i in range(rounds * 2):
|
201 |
+
if not active_debate_session.is_active:
|
202 |
+
break
|
203 |
+
|
204 |
+
round_num = (i // 2) + 1
|
205 |
+
if i % 2 == 0:
|
206 |
+
active_debate_session.current_round = round_num
|
207 |
+
await websocket.send_text(json.dumps({ "type": "round_info", "message": f"--- 第{round_num}轮 ---" }))
|
208 |
+
|
209 |
+
speaker_name, speaker_model = speakers[i % 2]
|
210 |
+
role = "正方" if speaker_name == first_model else "反方"
|
211 |
+
|
212 |
+
await websocket.send_text(json.dumps({ "type": "model_speaking", "model": speaker_name, "role": role }))
|
213 |
+
|
214 |
+
prompt = active_debate_session.generate_prompt(speaker_name, first_model)
|
215 |
+
|
216 |
+
response_content = ""
|
217 |
+
def stream_callback(content):
|
218 |
+
nonlocal response_content
|
219 |
+
response_content += content
|
220 |
+
asyncio.run_coroutine_threadsafe(
|
221 |
+
websocket.send_text(json.dumps({"type": "stream_content", "content": content})), loop
|
222 |
+
)
|
223 |
+
|
224 |
+
await loop.run_in_executor(None, speaker_model.chat_stream, active_debate_session.get_messages_for_model(speaker_name) + [{"role": "user", "content": prompt}], stream_callback)
|
225 |
+
await websocket.send_text(json.dumps({"type": "stream_end", "model": speaker_name}))
|
226 |
+
|
227 |
+
active_debate_session.add_message(DebateMessage("user", prompt, "system"))
|
228 |
+
active_debate_session.add_message(DebateMessage("assistant", response_content, speaker_name))
|
229 |
+
save_debate_record() # Incremental save
|
230 |
+
|
231 |
+
if active_debate_session.is_active:
|
232 |
+
active_debate_session.end_time = datetime.now()
|
233 |
+
active_debate_session.is_active = False
|
234 |
+
await websocket.send_text(json.dumps({ "type": "debate_ended", "message": "=== 辩论结束 ===" }))
|
235 |
+
save_debate_record()
|
236 |
+
logger.info("辩论结束")
|
237 |
+
|
238 |
+
except Exception as e:
|
239 |
+
logger.error(f"辩论过程中出错: {str(e)}")
|
240 |
+
await websocket.send_text(json.dumps({ "type": "error", "message": f"辩论过程中出错: {str(e)}" }))
|
241 |
+
|
242 |
+
async def stop_debate(websocket: WebSocket):
|
243 |
+
"""停止辩论"""
|
244 |
+
global active_debate_session
|
245 |
+
if active_debate_session:
|
246 |
+
active_debate_session.is_active = False
|
247 |
+
active_debate_session.end_time = datetime.now()
|
248 |
+
await websocket.send_text(json.dumps({
|
249 |
+
"type": "debate_stopped",
|
250 |
+
"message": "辩论已停止"
|
251 |
+
}))
|
252 |
+
save_debate_record()
|
253 |
+
active_debate_session = None
|
254 |
+
|
255 |
+
async def pause_debate(websocket: WebSocket):
|
256 |
+
"""暂停辩论"""
|
257 |
+
await websocket.send_text(json.dumps({
|
258 |
+
"type": "debate_paused",
|
259 |
+
"message": "辩论已暂停"
|
260 |
+
}))
|
261 |
+
|
262 |
+
async def resume_debate(websocket: WebSocket):
|
263 |
+
"""继续辩论"""
|
264 |
+
await websocket.send_text(json.dumps({
|
265 |
+
"type": "debate_resumed",
|
266 |
+
"message": "辩论已继续"
|
267 |
+
}))
|
268 |
+
|
269 |
+
def save_debate_record():
|
270 |
+
"""保存辩论记录"""
|
271 |
+
global active_debate_session
|
272 |
+
if active_debate_session:
|
273 |
+
try:
|
274 |
+
output_dir = os.path.join(OUTPUT_DIR, "辩论记录")
|
275 |
+
os.makedirs(output_dir, exist_ok=True)
|
276 |
+
file_path = os.path.join(output_dir, f"{active_debate_session.debate_id}.json")
|
277 |
+
active_debate_session.save_to_file(file_path)
|
278 |
+
logger.info(f"辩论记录已保存: {file_path}")
|
279 |
+
except Exception as e:
|
280 |
+
logger.error(f"保存辩论记录时出错: {str(e)}")
|
281 |
+
|
282 |
+
def create_templates():
|
283 |
+
"""创建HTML模板文件"""
|
284 |
+
index_html = """
|
285 |
+
<!DOCTYPE html>
|
286 |
+
<html lang="zh-CN">
|
287 |
+
<head>
|
288 |
+
<meta charset="UTF-8">
|
289 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
290 |
+
<title>AI大模型辩论系统</title>
|
291 |
+
<style>
|
292 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #333; height: 100vh; display: flex; flex-direction: column; }
|
293 |
+
.container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 95%; max-width: 1400px; margin: 20px auto; flex-grow: 1; display: flex; flex-direction: column; }
|
294 |
+
.main-layout { display: flex; gap: 20px; flex-grow: 1; min-height: 0; }
|
295 |
+
.sidebar { flex: 0 0 320px; display: flex; flex-direction: column; gap: 15px; }
|
296 |
+
.chat-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
297 |
+
.header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
|
298 |
+
.header h1 { color: #1a73e8; }
|
299 |
+
.controls, .control-group { margin-bottom: 20px; }
|
300 |
+
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
301 |
+
.control-group { flex: 1; min-width: 200px; }
|
302 |
+
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
|
303 |
+
input, select, button, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px; }
|
304 |
+
textarea { resize: vertical; }
|
305 |
+
button { background-color: #1a73e8; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
|
306 |
+
button:hover { background-color: #1558b8; }
|
307 |
+
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
308 |
+
.output-container { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; height: 500px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
|
309 |
+
.status { padding: 10px; border-radius: 6px; margin-bottom: 15px; border: 1px solid; }
|
310 |
+
.status.connected { background-color: #e6f4ea; color: #155724; border-color: #c3e6cb; }
|
311 |
+
.status.disconnected { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
312 |
+
|
313 |
+
/* 聊天气泡样式 */
|
314 |
+
.message { display: flex; align-items: flex-start; gap: 10px; max-width: 80%; }
|
315 |
+
.message .avatar { width: 40px; height: 40px; border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; font-size: 18px; }
|
316 |
+
.message .content { background-color: #ffffff; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
317 |
+
.message .sender { font-weight: bold; margin-bottom: 5px; color: #333; }
|
318 |
+
.message.glm45 { align-self: flex-start; }
|
319 |
+
.message.glm45 .avatar { background-color: #34a853; } /* Google Green */
|
320 |
+
.message.glm45 .content { border-top-left-radius: 4px; }
|
321 |
+
.message.deepseek_v31 { align-self: flex-end; flex-direction: row-reverse; }
|
322 |
+
.message.deepseek_v31 .avatar { background-color: #4285f4; } /* Google Blue */
|
323 |
+
.message.deepseek_v31 .content { background-color: #e7f3ff; border-top-right-radius: 4px; }
|
324 |
+
.message .text { white-space: pre-wrap; word-wrap: break-word; }
|
325 |
+
.message .text p { margin: 0 0 10px; }
|
326 |
+
.message .text h1, .message .text h2, .message .text h3 { margin: 15px 0 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
327 |
+
.message .text ul, .message .text ol { padding-left: 20px; }
|
328 |
+
.message .text code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
|
329 |
+
.message .text pre { background-color: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; }
|
330 |
+
.message .text pre code { background-color: transparent; padding: 0; }
|
331 |
+
.round-separator { text-align: center; color: #888; font-size: 0.9em; margin: 20px 0; font-weight: 600; }
|
332 |
+
|
333 |
+
/* 响应式设计 - 针对手机等小屏幕设备 */
|
334 |
+
@media (max-width: 768px) {
|
335 |
+
body { padding: 0; }
|
336 |
+
.container { width: 100%; margin: 0; border-radius: 0; padding: 10px; height: 100%; }
|
337 |
+
.main-layout { flex-direction: column; }
|
338 |
+
.sidebar { flex: 0 0 auto; }
|
339 |
+
.header h1 { font-size: 1.5em; }
|
340 |
+
.controls { flex-direction: column; }
|
341 |
+
.control-group { min-width: unset; }
|
342 |
+
}
|
343 |
+
</style>
|
344 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
345 |
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
346 |
+
</head>
|
347 |
+
<body>
|
348 |
+
<div class="container">
|
349 |
+
<div class="header"><h1>AI大模型辩论系统</h1><p>观看两个AI大模型实时辩论</p></div>
|
350 |
+
|
351 |
+
<div class="main-layout">
|
352 |
+
<div class="sidebar">
|
353 |
+
<div class="control-group"><label for="topic">辩论话题:</label><input type="text" id="topic" value="人工智能是否会取代人类的工作"></div>
|
354 |
+
<div class="control-group">
|
355 |
+
<label for="initialPrompt">自定义初始提示 (可选):</label>
|
356 |
+
<textarea id="initialPrompt" rows="6" placeholder="例如:请作为正方,用强势的语气开启辩论..."></textarea>
|
357 |
+
</div>
|
358 |
+
<div class="control-group"><label for="rounds">轮数:</label><input type="number" id="rounds" min="1" max="10" value="5"></div>
|
359 |
+
<div class="control-group"><label for="firstModel">首发模型:</label><select id="firstModel"><option value="glm45">ZhipuAI/GLM-4.5</option><option value="deepseek_v31">deepseek-ai/DeepSeek-V3.1</option></select></div>
|
360 |
+
<div class="controls">
|
361 |
+
<button id="startBtn" disabled>开始辩论</button>
|
362 |
+
<button id="stopBtn" disabled>停止辩论</button>
|
363 |
+
</div>
|
364 |
+
</div>
|
365 |
+
<div class="chat-area">
|
366 |
+
<label for="output">辩论实况:</label>
|
367 |
+
<div id="output" class="output-container"></div>
|
368 |
+
</div>
|
369 |
+
</div>
|
370 |
+
</div>
|
371 |
+
<script>
|
372 |
+
let websocket = null, isConnected = false, currentMessageElement = null, currentMessageContent = '';
|
373 |
+
const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn');
|
374 |
+
const outputDiv = document.getElementById('output');
|
375 |
+
|
376 |
+
function handleWebSocketMessage(data) {
|
377 |
+
let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10;
|
378 |
+
|
379 |
+
switch (data.type) {
|
380 |
+
case 'debate_started':
|
381 |
+
outputDiv.innerHTML = '';
|
382 |
+
const topicDiv = document.createElement('div');
|
383 |
+
topicDiv.className = 'round-separator';
|
384 |
+
topicDiv.innerHTML = `<strong>话题:</strong> ${data.topic}`;
|
385 |
+
outputDiv.appendChild(topicDiv);
|
386 |
+
break;
|
387 |
+
case 'round_info':
|
388 |
+
const separator = document.createElement('div');
|
389 |
+
separator.className = 'round-separator';
|
390 |
+
separator.textContent = data.message.trim();
|
391 |
+
outputDiv.appendChild(separator);
|
392 |
+
break;
|
393 |
+
case 'model_speaking':
|
394 |
+
currentMessageContent = ''; // 重置当前消息内容
|
395 |
+
const messageDiv = document.createElement('div');
|
396 |
+
messageDiv.className = `message ${data.model}`;
|
397 |
+
messageDiv.innerHTML = `
|
398 |
+
<div class="avatar">${data.model.substring(0, 1).toUpperCase()}</div>
|
399 |
+
<div class="content">
|
400 |
+
<div class="sender">${data.model} (${data.role})</div>
|
401 |
+
<div class="text"><i>正在思考...</i></div>
|
402 |
+
</div>`;
|
403 |
+
outputDiv.appendChild(messageDiv);
|
404 |
+
currentMessageElement = messageDiv.querySelector('.text');
|
405 |
+
break;
|
406 |
+
case 'stream_content':
|
407 |
+
if (currentMessageElement) {
|
408 |
+
currentMessageContent += data.content;
|
409 |
+
currentMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(currentMessageContent));
|
410 |
+
}
|
411 |
+
break;
|
412 |
+
case 'stream_end':
|
413 |
+
currentMessageElement = null;
|
414 |
+
break;
|
415 |
+
case 'debate_ended':
|
416 |
+
case 'debate_stopped':
|
417 |
+
const endMsg = document.createElement('div');
|
418 |
+
endMsg.className = 'round-separator';
|
419 |
+
endMsg.textContent = data.message;
|
420 |
+
outputDiv.appendChild(endMsg);
|
421 |
+
startBtn.disabled = false; stopBtn.disabled = true;
|
422 |
+
break;
|
423 |
+
case 'error':
|
424 |
+
outputDiv.innerHTML += `<div class="round-separator" style="color: red;">错误: ${data.message}</div>`;
|
425 |
+
break;
|
426 |
+
}
|
427 |
+
if(shouldScroll) {
|
428 |
+
outputDiv.scrollTop = outputDiv.scrollHeight;
|
429 |
+
}
|
430 |
+
}
|
431 |
+
|
432 |
+
function connect() {
|
433 |
+
const wsUrl = `ws://${window.location.host}/ws`;
|
434 |
+
websocket = new WebSocket(wsUrl);
|
435 |
+
websocket.onopen = () => { isConnected = true; startBtn.disabled = false; };
|
436 |
+
websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
|
437 |
+
websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; };
|
438 |
+
websocket.onerror = (error) => { console.error('WebSocket Error:', error); };
|
439 |
+
}
|
440 |
+
|
441 |
+
window.addEventListener('load', connect);
|
442 |
+
|
443 |
+
startBtn.addEventListener('click', () => {
|
444 |
+
if (!websocket) return;
|
445 |
+
const message = {
|
446 |
+
action: "start_debate",
|
447 |
+
topic: document.getElementById('topic').value,
|
448 |
+
rounds: parseInt(document.getElementById('rounds').value),
|
449 |
+
first_model: document.getElementById('firstModel').value,
|
450 |
+
initial_prompt: document.getElementById('initialPrompt').value
|
451 |
+
};
|
452 |
+
websocket.send(JSON.stringify(message));
|
453 |
+
startBtn.disabled = true; stopBtn.disabled = false;
|
454 |
+
});
|
455 |
+
stopBtn.addEventListener('click', () => {
|
456 |
+
if (!websocket) return;
|
457 |
+
websocket.send(JSON.stringify({ action: "stop_debate" }));
|
458 |
+
});
|
459 |
+
</script>
|
460 |
+
</body>
|
461 |
+
</html>"""
|
462 |
+
with open(os.path.join(templates_dir, "index.html"), "w", encoding="utf-8") as f:
|
463 |
+
f.write(index_html)
|
464 |
+
logger.info("Web应用模板文件已创建")
|
465 |
+
|
466 |
+
if __name__ == "__main__":
|
467 |
+
os.makedirs(static_dir, exist_ok=True)
|
468 |
+
os.makedirs(templates_dir, exist_ok=True)
|
469 |
+
create_templates()
|
470 |
+
logger.info("启动Web服务器...")
|
471 |
+
# 智能端口切换:优先使用环境变量PORT,否则默认为8000
|
472 |
+
port = int(os.environ.get("PORT", 8000))
|
473 |
+
uvicorn.run(app, host="0.0.0.0", port=port)
|
src/debate_controller.py
ADDED
@@ -0,0 +1,454 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
辩论控制器模块
|
4 |
+
管理辩论流程和控制发言顺序
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import json
|
10 |
+
import time
|
11 |
+
import logging
|
12 |
+
import threading
|
13 |
+
from typing import Dict, List, Any, Optional, Callable
|
14 |
+
from datetime import datetime
|
15 |
+
from queue import Queue
|
16 |
+
|
17 |
+
# 在代码开头强制设置终端编码为UTF-8
|
18 |
+
os.system('chcp 65001 > nul')
|
19 |
+
|
20 |
+
# 获取当前脚本文件所在目录的绝对路径
|
21 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
22 |
+
|
23 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
24 |
+
project_root = os.path.dirname(current_script_dir)
|
25 |
+
|
26 |
+
# 定义数据输入、输出和日志目录
|
27 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
28 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
29 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
30 |
+
|
31 |
+
# 确保目录存在
|
32 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
33 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
34 |
+
|
35 |
+
# 配置日志
|
36 |
+
logging.basicConfig(
|
37 |
+
level=logging.INFO,
|
38 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
39 |
+
handlers=[
|
40 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '辩论控制器日志.log'), encoding='utf-8'),
|
41 |
+
logging.StreamHandler(sys.stdout)
|
42 |
+
]
|
43 |
+
)
|
44 |
+
logger = logging.getLogger(__name__)
|
45 |
+
|
46 |
+
# 捕获警告并记录到日志
|
47 |
+
logging.captureWarnings(True)
|
48 |
+
|
49 |
+
class DebateMessage:
|
50 |
+
"""辩论消息类"""
|
51 |
+
|
52 |
+
def __init__(self, role: str, content: str, model_name: str, timestamp: datetime = None):
|
53 |
+
"""
|
54 |
+
初始化辩论消息
|
55 |
+
|
56 |
+
Args:
|
57 |
+
role: 消息角色 ('user' 或 'assistant')
|
58 |
+
content: 消息内容
|
59 |
+
model_name: 模型名称
|
60 |
+
timestamp: 时间戳
|
61 |
+
"""
|
62 |
+
self.role = role
|
63 |
+
self.content = content
|
64 |
+
self.model_name = model_name
|
65 |
+
self.timestamp = timestamp or datetime.now()
|
66 |
+
|
67 |
+
def to_dict(self) -> Dict[str, Any]:
|
68 |
+
"""转换为字典"""
|
69 |
+
return {
|
70 |
+
'role': self.role,
|
71 |
+
'content': self.content,
|
72 |
+
'model_name': self.model_name,
|
73 |
+
'timestamp': self.timestamp.isoformat()
|
74 |
+
}
|
75 |
+
|
76 |
+
@classmethod
|
77 |
+
def from_dict(cls, data: Dict[str, Any]) -> 'DebateMessage':
|
78 |
+
"""从字典创建实例"""
|
79 |
+
return cls(
|
80 |
+
role=data['role'],
|
81 |
+
content=data['content'],
|
82 |
+
model_name=data['model_name'],
|
83 |
+
timestamp=datetime.fromisoformat(data['timestamp'])
|
84 |
+
)
|
85 |
+
|
86 |
+
class DebateSession:
|
87 |
+
"""辩论会话类"""
|
88 |
+
|
89 |
+
def __init__(self, topic: str, max_rounds: int = 5, first_model: str = 'glm45'):
|
90 |
+
"""
|
91 |
+
初始化辩论会话
|
92 |
+
|
93 |
+
Args:
|
94 |
+
topic: 辩论话题
|
95 |
+
max_rounds: 最大轮数
|
96 |
+
first_model: 首发模型 ('glm45' 或 'kimi_k2')
|
97 |
+
"""
|
98 |
+
self.topic = topic
|
99 |
+
self.max_rounds = max_rounds
|
100 |
+
self.first_model = first_model
|
101 |
+
self.messages: List[DebateMessage] = []
|
102 |
+
self.current_round = 0
|
103 |
+
self.is_active = True
|
104 |
+
self.is_paused = False
|
105 |
+
self.start_time = None
|
106 |
+
self.end_time = None
|
107 |
+
self.debate_id = f"debate_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
108 |
+
|
109 |
+
logger.info(f"辩论会话初始化完成,话题: {topic}, 最大轮数: {max_rounds}, 首发模型: {first_model}")
|
110 |
+
|
111 |
+
def add_message(self, message: DebateMessage):
|
112 |
+
"""添加消息到辩论记录"""
|
113 |
+
self.messages.append(message)
|
114 |
+
logger.info(f"添加消息到辩论记录,模型: {message.model_name}, 内容长度: {len(message.content)}")
|
115 |
+
|
116 |
+
def get_messages_for_model(self, model_name: str) -> List[Dict[str, str]]:
|
117 |
+
"""
|
118 |
+
获取指定模型的消息历史
|
119 |
+
|
120 |
+
Args:
|
121 |
+
model_name: 模型名称
|
122 |
+
|
123 |
+
Returns:
|
124 |
+
消息历史列表
|
125 |
+
"""
|
126 |
+
# 转换为API需要的格式
|
127 |
+
result = []
|
128 |
+
for msg in self.messages:
|
129 |
+
result.append({
|
130 |
+
'role': msg.role,
|
131 |
+
'content': msg.content
|
132 |
+
})
|
133 |
+
return result
|
134 |
+
|
135 |
+
def get_debate_summary(self) -> Dict[str, Any]:
|
136 |
+
"""获取辩论摘要"""
|
137 |
+
return {
|
138 |
+
'debate_id': self.debate_id,
|
139 |
+
'topic': self.topic,
|
140 |
+
'max_rounds': self.max_rounds,
|
141 |
+
'current_round': self.current_round,
|
142 |
+
'total_messages': len(self.messages),
|
143 |
+
'is_active': self.is_active,
|
144 |
+
'is_paused': self.is_paused,
|
145 |
+
'start_time': self.start_time.isoformat() if self.start_time else None,
|
146 |
+
'end_time': self.end_time.isoformat() if self.end_time else None,
|
147 |
+
'duration': (self.end_time - self.start_time).total_seconds() if self.start_time and self.end_time else None
|
148 |
+
}
|
149 |
+
|
150 |
+
def save_to_file(self, file_path: str):
|
151 |
+
"""保存辩论记录到文件"""
|
152 |
+
data = {
|
153 |
+
'debate_info': self.get_debate_summary(),
|
154 |
+
'messages': [msg.to_dict() for msg in self.messages]
|
155 |
+
}
|
156 |
+
|
157 |
+
with open(file_path, 'w', encoding='utf-8') as f:
|
158 |
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
159 |
+
|
160 |
+
logger.info(f"辩论记录已保存到: {file_path}")
|
161 |
+
|
162 |
+
@classmethod
|
163 |
+
def load_from_file(cls, file_path: str) -> 'DebateSession':
|
164 |
+
"""从文件加载辩论记录"""
|
165 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
166 |
+
data = json.load(f)
|
167 |
+
|
168 |
+
debate_info = data['debate_info']
|
169 |
+
session = cls(
|
170 |
+
topic=debate_info['topic'],
|
171 |
+
max_rounds=debate_info['max_rounds'],
|
172 |
+
first_model='glm45' # 默认值,实际值应该从消息中推断
|
173 |
+
)
|
174 |
+
|
175 |
+
session.debate_id = debate_info['debate_id']
|
176 |
+
session.current_round = debate_info['current_round']
|
177 |
+
session.is_active = debate_info['is_active']
|
178 |
+
session.is_paused = debate_info['is_paused']
|
179 |
+
session.start_time = datetime.fromisoformat(debate_info['start_time']) if debate_info['start_time'] else None
|
180 |
+
session.end_time = datetime.fromisoformat(debate_info['end_time']) if debate_info['end_time'] else None
|
181 |
+
|
182 |
+
session.messages = [DebateMessage.from_dict(msg_data) for msg_data in data['messages']]
|
183 |
+
|
184 |
+
logger.info(f"从文件加载辩论记录: {file_path}")
|
185 |
+
return session
|
186 |
+
|
187 |
+
def generate_prompt(self, speaker_name: str, first_model_name: str) -> str:
|
188 |
+
"""
|
189 |
+
根据辩论历史和当前发言者生成提示
|
190 |
+
|
191 |
+
Args:
|
192 |
+
speaker_name: 当前发言者名称
|
193 |
+
first_model_name: 首发模型名称
|
194 |
+
|
195 |
+
Returns:
|
196 |
+
生成的提示字符串
|
197 |
+
"""
|
198 |
+
is_positive_side = (speaker_name == first_model_name)
|
199 |
+
role = "正方" if is_positive_side else "反方"
|
200 |
+
|
201 |
+
if not self.messages:
|
202 |
+
# 辩论开始,第一个发言者
|
203 |
+
return f"你将作为{role},就以下话题进行辩论:{self.topic}。请提出你的主要论点,陈述你的核心立场和关键论据。"
|
204 |
+
else:
|
205 |
+
last_message = self.messages[-1]
|
206 |
+
opponent_statement = last_message.content
|
207 |
+
|
208 |
+
if len(self.messages) == 1:
|
209 |
+
# 第二个发言者,对开场陈述进行反驳
|
210 |
+
return f"你将作为{role},就以下话题进行辩论:{self.topic}。你的对手({role})的开场陈述是:\n\n“{opponent_statement}”\n\n请直接反驳对方的观点,并提出你自己的论点。"
|
211 |
+
else:
|
212 |
+
# 后续发言,进行反驳和回应
|
213 |
+
return f"现在轮到你({role})发言。你的对手刚刚的发言是:\n\n“{opponent_statement}”\n\n请针对他的观点进行反驳,并进一步阐述和强化你自己的立场。"
|
214 |
+
|
215 |
+
class DebateController:
|
216 |
+
"""辩论控制器类"""
|
217 |
+
|
218 |
+
def __init__(self, model_manager, output_callback: Optional[Callable] = None):
|
219 |
+
"""
|
220 |
+
初始化辩论控制器
|
221 |
+
|
222 |
+
Args:
|
223 |
+
model_manager: 模型管理器实例
|
224 |
+
output_callback: 输出回调函数,用于实时显示辩论内容
|
225 |
+
"""
|
226 |
+
self.model_manager = model_manager
|
227 |
+
self.output_callback = output_callback
|
228 |
+
self.current_session: Optional[DebateSession] = None
|
229 |
+
self.debate_thread: Optional[threading.Thread] = None
|
230 |
+
self.stop_event = threading.Event()
|
231 |
+
self.pause_event = threading.Event()
|
232 |
+
|
233 |
+
logger.info("辩论控制器初始化完成")
|
234 |
+
|
235 |
+
def create_debate(self, topic: str, max_rounds: int = 5, first_model: str = 'glm45') -> DebateSession:
|
236 |
+
"""
|
237 |
+
创建新的辩论会话
|
238 |
+
|
239 |
+
Args:
|
240 |
+
topic: 辩论话题
|
241 |
+
max_rounds: 最大轮数
|
242 |
+
first_model: 首发模型
|
243 |
+
|
244 |
+
Returns:
|
245 |
+
辩论会话实例
|
246 |
+
"""
|
247 |
+
self.current_session = DebateSession(topic, max_rounds, first_model)
|
248 |
+
logger.info(f"创建新辩论会话,话题: {topic}")
|
249 |
+
return self.current_session
|
250 |
+
|
251 |
+
def start_debate(self):
|
252 |
+
"""开始辩论"""
|
253 |
+
if not self.current_session:
|
254 |
+
logger.error("没有活动的辩论会话")
|
255 |
+
return
|
256 |
+
|
257 |
+
if self.current_session.is_active:
|
258 |
+
logger.warning("辩论已经在进行中")
|
259 |
+
return
|
260 |
+
|
261 |
+
self.current_session.is_active = True
|
262 |
+
self.current_session.is_paused = False
|
263 |
+
self.current_session.start_time = datetime.now()
|
264 |
+
self.stop_event.clear()
|
265 |
+
self.pause_event.clear()
|
266 |
+
|
267 |
+
# 启动辩论线程
|
268 |
+
self.debate_thread = threading.Thread(target=self._debate_loop)
|
269 |
+
self.debate_thread.daemon = True
|
270 |
+
self.debate_thread.start()
|
271 |
+
|
272 |
+
logger.info("辩论开始")
|
273 |
+
|
274 |
+
def pause_debate(self):
|
275 |
+
"""暂停辩论"""
|
276 |
+
if not self.current_session or not self.current_session.is_active:
|
277 |
+
logger.warning("没有活动的辩论会话")
|
278 |
+
return
|
279 |
+
|
280 |
+
self.current_session.is_paused = True
|
281 |
+
self.pause_event.set()
|
282 |
+
logger.info("辩论已暂停")
|
283 |
+
|
284 |
+
def resume_debate(self):
|
285 |
+
"""恢复辩论"""
|
286 |
+
if not self.current_session or not self.current_session.is_active:
|
287 |
+
logger.warning("没有活动的辩论会话")
|
288 |
+
return
|
289 |
+
|
290 |
+
self.current_session.is_paused = False
|
291 |
+
self.pause_event.clear()
|
292 |
+
logger.info("辩论已恢复")
|
293 |
+
|
294 |
+
def stop_debate(self):
|
295 |
+
"""停止辩论"""
|
296 |
+
if not self.current_session or not self.current_session.is_active:
|
297 |
+
logger.warning("没有活动的辩论会话")
|
298 |
+
return
|
299 |
+
|
300 |
+
self.stop_event.set()
|
301 |
+
self.current_session.is_active = False
|
302 |
+
self.current_session.is_paused = False
|
303 |
+
self.current_session.end_time = datetime.now()
|
304 |
+
|
305 |
+
if self.debate_thread and self.debate_thread.is_alive():
|
306 |
+
self.debate_thread.join(timeout=5)
|
307 |
+
|
308 |
+
# 保存辩论记录
|
309 |
+
self._save_debate_record()
|
310 |
+
|
311 |
+
logger.info("辩论已停止")
|
312 |
+
|
313 |
+
def _debate_loop(self):
|
314 |
+
"""辩论主循环"""
|
315 |
+
session = self.current_session
|
316 |
+
if not session:
|
317 |
+
return
|
318 |
+
|
319 |
+
# 确定模型顺序
|
320 |
+
model_a = session.first_model
|
321 |
+
model_b = 'deepseek_v31' if model_a == 'glm45' else 'glm45'
|
322 |
+
|
323 |
+
# 获取模型接口
|
324 |
+
model_a_interface = self.model_manager.get_model(model_a)
|
325 |
+
model_b_interface = self.model_manager.get_model(model_b)
|
326 |
+
|
327 |
+
# 构建初始提示
|
328 |
+
initial_prompt_a = f"你将作为正方,就以下话题进行辩论:{session.topic}。请提出你的主要论点。"
|
329 |
+
initial_prompt_b = f"你将作为反方,就以下话题进行辩论:{session.topic}。请针对对方的论点进行反驳。"
|
330 |
+
|
331 |
+
# 第一轮:模型A提出论点
|
332 |
+
if not self._should_continue_debate(session):
|
333 |
+
return
|
334 |
+
|
335 |
+
self._output_message(f"=== 辩论开始 ===\n话题: {session.topic}\n")
|
336 |
+
self._output_message(f"--- 第1轮 ---\n")
|
337 |
+
|
338 |
+
# 模型A发言
|
339 |
+
self._output_message(f"{model_a} (正方): ")
|
340 |
+
response_a = model_a_interface.chat([{"role": "user", "content": initial_prompt_a}])
|
341 |
+
self._output_message(f"{response_a}\n\n")
|
342 |
+
|
343 |
+
# 记录消息
|
344 |
+
session.add_message(DebateMessage("user", initial_prompt_a, "system"))
|
345 |
+
session.add_message(DebateMessage("assistant", response_a, model_a))
|
346 |
+
|
347 |
+
session.current_round = 1
|
348 |
+
|
349 |
+
# 后续轮次
|
350 |
+
while self._should_continue_debate(session):
|
351 |
+
# 检查暂停状态
|
352 |
+
if self.pause_event.is_set():
|
353 |
+
self._output_message("辩论已暂停,等待恢复...\n")
|
354 |
+
self.pause_event.wait()
|
355 |
+
self._output_message("辩论已恢复\n")
|
356 |
+
|
357 |
+
session.current_round += 1
|
358 |
+
self._output_message(f"--- 第{session.current_round}轮 ---\n")
|
359 |
+
|
360 |
+
# 模型B反驳
|
361 |
+
self._output_message(f"{model_b} (反方): ")
|
362 |
+
prompt_b = f"{initial_prompt_b}\n\n对方的论点: {response_a}\n\n请进行反驳。"
|
363 |
+
response_b = model_b_interface.chat(session.get_messages_for_model(model_b) + [{"role": "user", "content": prompt_b}])
|
364 |
+
self._output_message(f"{response_b}\n\n")
|
365 |
+
|
366 |
+
# 记录消息
|
367 |
+
session.add_message(DebateMessage("user", prompt_b, "system"))
|
368 |
+
session.add_message(DebateMessage("assistant", response_b, model_b))
|
369 |
+
|
370 |
+
# 检查是否应该继续
|
371 |
+
if not self._should_continue_debate(session):
|
372 |
+
break
|
373 |
+
|
374 |
+
# 模型A回应
|
375 |
+
self._output_message(f"{model_a} (正方): ")
|
376 |
+
prompt_a = f"请针对对方的反驳进行回应:{response_b}"
|
377 |
+
response_a = model_a_interface.chat(session.get_messages_for_model(model_a) + [{"role": "user", "content": prompt_a}])
|
378 |
+
self._output_message(f"{response_a}\n\n")
|
379 |
+
|
380 |
+
# 记录消息
|
381 |
+
session.add_message(DebateMessage("user", prompt_a, "system"))
|
382 |
+
session.add_message(DebateMessage("assistant", response_a, model_a))
|
383 |
+
|
384 |
+
# 辩论结束
|
385 |
+
session.is_active = False
|
386 |
+
session.end_time = datetime.now()
|
387 |
+
self._output_message("=== 辩论结束 ===\n")
|
388 |
+
|
389 |
+
# 保存辩论记录
|
390 |
+
self._save_debate_record()
|
391 |
+
|
392 |
+
def _should_continue_debate(self, session: DebateSession) -> bool:
|
393 |
+
"""检查是否应该继续辩论"""
|
394 |
+
if self.stop_event.is_set():
|
395 |
+
return False
|
396 |
+
|
397 |
+
if session.current_round >= session.max_rounds:
|
398 |
+
return False
|
399 |
+
|
400 |
+
return True
|
401 |
+
|
402 |
+
def _output_message(self, message: str):
|
403 |
+
"""输出消息"""
|
404 |
+
if self.output_callback:
|
405 |
+
self.output_callback(message)
|
406 |
+
else:
|
407 |
+
print(message, end='', flush=True)
|
408 |
+
|
409 |
+
def _save_debate_record(self):
|
410 |
+
"""保存辩论记录"""
|
411 |
+
if not self.current_session:
|
412 |
+
return
|
413 |
+
|
414 |
+
# 创建输出文件路径
|
415 |
+
output_dir = os.path.join(OUTPUT_DIR, "辩论记录")
|
416 |
+
os.makedirs(output_dir, exist_ok=True)
|
417 |
+
|
418 |
+
file_path = os.path.join(output_dir, f"{self.current_session.debate_id}.json")
|
419 |
+
self.current_session.save_to_file(file_path)
|
420 |
+
|
421 |
+
logger.info(f"辩论记录已保存: {file_path}")
|
422 |
+
|
423 |
+
# 测试代码
|
424 |
+
if __name__ == "__main__":
|
425 |
+
# 简单测试
|
426 |
+
import sys
|
427 |
+
import os
|
428 |
+
import importlib.util
|
429 |
+
|
430 |
+
# 动态导入模块
|
431 |
+
module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "2_模型接口.py")
|
432 |
+
spec = importlib.util.spec_from_file_location("model_interface", module_path)
|
433 |
+
model_interface = importlib.util.module_from_spec(spec)
|
434 |
+
spec.loader.exec_module(model_interface)
|
435 |
+
|
436 |
+
# 创建模型管理器
|
437 |
+
api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
438 |
+
model_manager = model_interface.ModelManager(api_key)
|
439 |
+
|
440 |
+
# 创建辩论控制器
|
441 |
+
def print_callback(message):
|
442 |
+
print(message, end='', flush=True)
|
443 |
+
|
444 |
+
controller = DebateController(model_manager, print_callback)
|
445 |
+
|
446 |
+
# 创建辩论会话
|
447 |
+
session = controller.create_debate("人工智能是否会取代人类的工作", max_rounds=3, first_model='glm45')
|
448 |
+
|
449 |
+
# 开始辩论
|
450 |
+
controller.start_debate()
|
451 |
+
|
452 |
+
# 等待辩论结束
|
453 |
+
if controller.debate_thread:
|
454 |
+
controller.debate_thread.join()
|
src/model_interface.py
ADDED
@@ -0,0 +1,376 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
模型接口模块
|
4 |
+
用于与两个大模型API进行交互的封装
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import requests
|
10 |
+
import json
|
11 |
+
import time
|
12 |
+
import logging
|
13 |
+
from typing import Dict, Any, Optional, Callable
|
14 |
+
from urllib3.exceptions import InsecureRequestWarning
|
15 |
+
|
16 |
+
# 在代码开头强制设置终端编码为UTF-8
|
17 |
+
os.system('chcp 65001 > nul')
|
18 |
+
|
19 |
+
# 获取当前脚本文件所在目录的绝对路径
|
20 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
21 |
+
|
22 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
23 |
+
project_root = os.path.dirname(current_script_dir)
|
24 |
+
|
25 |
+
# 定义数据输入、输出和日志目录
|
26 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
27 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
28 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
29 |
+
|
30 |
+
# 确保目录存在
|
31 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
32 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
33 |
+
|
34 |
+
# 配置日志
|
35 |
+
logging.basicConfig(
|
36 |
+
level=logging.INFO,
|
37 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
38 |
+
handlers=[
|
39 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '模型接口日志.log'), encoding='utf-8'),
|
40 |
+
logging.StreamHandler(sys.stdout)
|
41 |
+
]
|
42 |
+
)
|
43 |
+
logger = logging.getLogger(__name__)
|
44 |
+
|
45 |
+
# 捕获警告并记录到日志
|
46 |
+
logging.captureWarnings(True)
|
47 |
+
|
48 |
+
class ModelInterface:
|
49 |
+
"""模型接口基类"""
|
50 |
+
|
51 |
+
def __init__(self, api_key: str, base_url: str):
|
52 |
+
"""
|
53 |
+
初始化模型接口
|
54 |
+
|
55 |
+
Args:
|
56 |
+
api_key: API密钥
|
57 |
+
base_url: API基础URL
|
58 |
+
"""
|
59 |
+
self.api_key = api_key
|
60 |
+
self.base_url = base_url
|
61 |
+
self.headers = {
|
62 |
+
'Authorization': f'Bearer {api_key}',
|
63 |
+
'Content-Type': 'application/json'
|
64 |
+
}
|
65 |
+
logger.info(f"模型接口初始化完成,基础URL: {base_url}")
|
66 |
+
|
67 |
+
def send_request(self, model: str, messages: list, **kwargs) -> Dict[str, Any]:
|
68 |
+
"""
|
69 |
+
发送请求到模型API
|
70 |
+
|
71 |
+
Args:
|
72 |
+
model: 模型名称
|
73 |
+
messages: 消息列表
|
74 |
+
**kwargs: 其他参数
|
75 |
+
|
76 |
+
Returns:
|
77 |
+
API响应结果
|
78 |
+
"""
|
79 |
+
payload = {
|
80 |
+
'model': model,
|
81 |
+
'messages': messages,
|
82 |
+
**kwargs
|
83 |
+
}
|
84 |
+
|
85 |
+
try:
|
86 |
+
logger.info(f"向模型 {model} 发送请求")
|
87 |
+
response = requests.post(
|
88 |
+
f"{self.base_url}/chat/completions",
|
89 |
+
headers=self.headers,
|
90 |
+
json=payload,
|
91 |
+
timeout=60
|
92 |
+
)
|
93 |
+
response.raise_for_status()
|
94 |
+
result = response.json()
|
95 |
+
logger.info(f"模型 {model} 响应成功")
|
96 |
+
return result
|
97 |
+
except requests.exceptions.RequestException as e:
|
98 |
+
logger.error(f"请求模型 {model} 失败: {str(e)}")
|
99 |
+
raise
|
100 |
+
except json.JSONDecodeError as e:
|
101 |
+
logger.error(f"解析模型 {model} 响应失败: {str(e)}")
|
102 |
+
raise
|
103 |
+
|
104 |
+
def send_stream_request(self, model: str, messages: list, callback: Callable[[str], None], **kwargs) -> str:
|
105 |
+
"""
|
106 |
+
发送流式请求到模型API
|
107 |
+
|
108 |
+
Args:
|
109 |
+
model: 模型名称
|
110 |
+
messages: 消息列表
|
111 |
+
callback: 回调函数,用于处理流式输出的每个字符
|
112 |
+
**kwargs: 其他参数
|
113 |
+
|
114 |
+
Returns:
|
115 |
+
完整的响应文本
|
116 |
+
"""
|
117 |
+
payload = {
|
118 |
+
'model': model,
|
119 |
+
'messages': messages,
|
120 |
+
'stream': True,
|
121 |
+
**kwargs
|
122 |
+
}
|
123 |
+
|
124 |
+
full_response = ""
|
125 |
+
try:
|
126 |
+
logger.info(f"向模型 {model} 发送流式请求")
|
127 |
+
response = requests.post(
|
128 |
+
f"{self.base_url}/chat/completions",
|
129 |
+
headers=self.headers,
|
130 |
+
json=payload,
|
131 |
+
timeout=60,
|
132 |
+
stream=True
|
133 |
+
)
|
134 |
+
response.raise_for_status()
|
135 |
+
|
136 |
+
# 处理流式响应
|
137 |
+
for line in response.iter_lines():
|
138 |
+
if line:
|
139 |
+
decoded_line = line.decode('utf-8')
|
140 |
+
if decoded_line.startswith("data: "):
|
141 |
+
data = decoded_line[6:] # 去掉 "data: " 前缀
|
142 |
+
if data != "[DONE]":
|
143 |
+
try:
|
144 |
+
json_data = json.loads(data)
|
145 |
+
content = json_data["choices"][0]["delta"].get("content", "")
|
146 |
+
if content:
|
147 |
+
full_response += content
|
148 |
+
# 调用回调函数处理每个字符
|
149 |
+
callback(content)
|
150 |
+
except json.JSONDecodeError:
|
151 |
+
pass # 忽略无法解析的行
|
152 |
+
|
153 |
+
logger.info(f"模型 {model} 流式响应完成")
|
154 |
+
return full_response
|
155 |
+
except requests.exceptions.RequestException as e:
|
156 |
+
logger.error(f"流式请求模型 {model} 失败: {str(e)}")
|
157 |
+
raise
|
158 |
+
except Exception as e:
|
159 |
+
logger.error(f"处理流式响应时出错: {str(e)}")
|
160 |
+
raise
|
161 |
+
|
162 |
+
def get_response_text(self, response: Dict[str, Any]) -> str:
|
163 |
+
"""
|
164 |
+
从API响应中提取回复文本
|
165 |
+
|
166 |
+
Args:
|
167 |
+
response: API响应
|
168 |
+
|
169 |
+
Returns:
|
170 |
+
回复文本
|
171 |
+
"""
|
172 |
+
try:
|
173 |
+
return response['choices'][0]['message']['content']
|
174 |
+
except (KeyError, IndexError) as e:
|
175 |
+
logger.error(f"提取回复文本失败: {str(e)}")
|
176 |
+
raise
|
177 |
+
|
178 |
+
class GLM45Interface(ModelInterface):
|
179 |
+
"""GLM-4.5模型接口"""
|
180 |
+
|
181 |
+
def __init__(self, api_key: str):
|
182 |
+
"""
|
183 |
+
初始化GLM-4.5模型接口
|
184 |
+
|
185 |
+
Args:
|
186 |
+
api_key: API密钥
|
187 |
+
"""
|
188 |
+
super().__init__(api_key, "https://api-inference.modelscope.cn/v1")
|
189 |
+
self.model_name = "ZhipuAI/GLM-4.5"
|
190 |
+
logger.info(f"GLM-4.5模型接口初始化完成")
|
191 |
+
|
192 |
+
def chat(self, messages: list, temperature: float = 0.7, max_tokens: int = 1000) -> str:
|
193 |
+
"""
|
194 |
+
与GLM-4.5模型对话
|
195 |
+
|
196 |
+
Args:
|
197 |
+
messages: 消息列表
|
198 |
+
temperature: 温度参数,控制随机性
|
199 |
+
max_tokens: 最大令牌数
|
200 |
+
|
201 |
+
Returns:
|
202 |
+
模型回复文本
|
203 |
+
"""
|
204 |
+
try:
|
205 |
+
response = self.send_request(
|
206 |
+
model=self.model_name,
|
207 |
+
messages=messages,
|
208 |
+
temperature=temperature,
|
209 |
+
max_tokens=max_tokens
|
210 |
+
)
|
211 |
+
return self.get_response_text(response)
|
212 |
+
except Exception as e:
|
213 |
+
logger.error(f"GLM-4.5对话失败: {str(e)}")
|
214 |
+
return f"GLM-4.5对话失败: {str(e)}"
|
215 |
+
|
216 |
+
def chat_stream(self, messages: list, callback: Callable[[str], None], temperature: float = 0.7, max_tokens: int = 1000) -> str:
|
217 |
+
"""
|
218 |
+
与GLM-4.5模型流式对话
|
219 |
+
|
220 |
+
Args:
|
221 |
+
messages: 消息列表
|
222 |
+
callback: 回调函数,用于处理流式输出的每个字符
|
223 |
+
temperature: 温度参数,控制随机性
|
224 |
+
max_tokens: 最大令牌数
|
225 |
+
|
226 |
+
Returns:
|
227 |
+
完整的模型回复文本
|
228 |
+
"""
|
229 |
+
try:
|
230 |
+
return self.send_stream_request(
|
231 |
+
model=self.model_name,
|
232 |
+
messages=messages,
|
233 |
+
callback=callback,
|
234 |
+
temperature=temperature
|
235 |
+
# 移除max_tokens参数,避免截断
|
236 |
+
# max_tokens=max_tokens
|
237 |
+
)
|
238 |
+
except Exception as e:
|
239 |
+
logger.error(f"GLM-4.5流式对话失败: {str(e)}")
|
240 |
+
error_msg = f"GLM-4.5流式对话失败: {str(e)}"
|
241 |
+
# 通过回调函数发送错误信息
|
242 |
+
callback(f"\n{error_msg}\n")
|
243 |
+
return error_msg
|
244 |
+
|
245 |
+
class DeepSeekV31Interface(ModelInterface):
|
246 |
+
"""DeepSeek-V3.1模型接口"""
|
247 |
+
|
248 |
+
def __init__(self, api_key: str):
|
249 |
+
"""
|
250 |
+
初始化DeepSeek-V3.1模型接口
|
251 |
+
|
252 |
+
Args:
|
253 |
+
api_key: API密钥
|
254 |
+
"""
|
255 |
+
super().__init__(api_key, "https://api-inference.modelscope.cn/v1")
|
256 |
+
self.model_name = "deepseek-ai/DeepSeek-V3.1"
|
257 |
+
logger.info(f"DeepSeek-V3.1模型接口初始化完成")
|
258 |
+
|
259 |
+
def chat(self, messages: list, temperature: float = 0.7, max_tokens: int = 1000) -> str:
|
260 |
+
"""
|
261 |
+
与DeepSeek-V3.1模型对话
|
262 |
+
|
263 |
+
Args:
|
264 |
+
messages: 消息列表
|
265 |
+
temperature: 温度参数,控制随机性
|
266 |
+
max_tokens: 最大令牌数
|
267 |
+
|
268 |
+
Returns:
|
269 |
+
模型回复文本
|
270 |
+
"""
|
271 |
+
try:
|
272 |
+
response = self.send_request(
|
273 |
+
model=self.model_name,
|
274 |
+
messages=messages,
|
275 |
+
temperature=temperature,
|
276 |
+
max_tokens=max_tokens
|
277 |
+
)
|
278 |
+
return self.get_response_text(response)
|
279 |
+
except Exception as e:
|
280 |
+
logger.error(f"DeepSeek-V3.1对话失败: {str(e)}")
|
281 |
+
return f"DeepSeek-V3.1对话失败: {str(e)}"
|
282 |
+
|
283 |
+
def chat_stream(self, messages: list, callback: Callable[[str], None], temperature: float = 0.7, max_tokens: int = 1000) -> str:
|
284 |
+
"""
|
285 |
+
与DeepSeek-V3.1模型流式对话
|
286 |
+
|
287 |
+
Args:
|
288 |
+
messages: 消息列表
|
289 |
+
callback: 回调函数,用于处理流式输出的每个字符
|
290 |
+
temperature: 温度参数,控制随机性
|
291 |
+
max_tokens: 最大令牌数
|
292 |
+
|
293 |
+
Returns:
|
294 |
+
完整的模型回复文本
|
295 |
+
"""
|
296 |
+
try:
|
297 |
+
return self.send_stream_request(
|
298 |
+
model=self.model_name,
|
299 |
+
messages=messages,
|
300 |
+
callback=callback,
|
301 |
+
temperature=temperature
|
302 |
+
# 暂时移除max_tokens参数,排查兼容性问题
|
303 |
+
# max_tokens=max_tokens
|
304 |
+
)
|
305 |
+
except Exception as e:
|
306 |
+
logger.error(f"DeepSeek-V3.1流式对话失败: {str(e)}")
|
307 |
+
error_msg = f"DeepSeek-V3.1流式对话失败: {str(e)}"
|
308 |
+
# 通过回调函数发送错误信息
|
309 |
+
callback(f"\n{error_msg}\n")
|
310 |
+
return error_msg
|
311 |
+
|
312 |
+
class ModelManager:
|
313 |
+
"""模型管理器,统一管理两个模型接口"""
|
314 |
+
|
315 |
+
def __init__(self, api_key: str):
|
316 |
+
"""
|
317 |
+
初始化模型管理器
|
318 |
+
|
319 |
+
Args:
|
320 |
+
api_key: API密钥
|
321 |
+
"""
|
322 |
+
self.api_key = api_key
|
323 |
+
self.glm45 = GLM45Interface(api_key)
|
324 |
+
self.deepseek_v31 = DeepSeekV31Interface(api_key)
|
325 |
+
logger.info("模型管理器初始化完成")
|
326 |
+
|
327 |
+
def get_model(self, model_name: str) -> ModelInterface:
|
328 |
+
"""
|
329 |
+
获取指定模型接口
|
330 |
+
|
331 |
+
Args:
|
332 |
+
model_name: 模型名称 ('glm45' 或 'deepseek_v31')
|
333 |
+
|
334 |
+
Returns:
|
335 |
+
模型接口实例
|
336 |
+
"""
|
337 |
+
if model_name.lower() == 'glm45':
|
338 |
+
return self.glm45
|
339 |
+
elif model_name.lower() == 'deepseek_v31':
|
340 |
+
return self.deepseek_v31
|
341 |
+
else:
|
342 |
+
logger.error(f"不支持的模型名称: {model_name}")
|
343 |
+
raise ValueError(f"不支持的模型名称: {model_name}")
|
344 |
+
|
345 |
+
# 测试代码
|
346 |
+
if __name__ == "__main__":
|
347 |
+
# 测试模型接口
|
348 |
+
api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
349 |
+
manager = ModelManager(api_key)
|
350 |
+
|
351 |
+
# 测试GLM-4.5
|
352 |
+
glm45 = manager.get_model('glm45')
|
353 |
+
messages = [{"role": "user", "content": "你好,请简单介绍一下自己"}]
|
354 |
+
|
355 |
+
print("=== 测试GLM-4.5普通对话 ===")
|
356 |
+
response = glm45.chat(messages)
|
357 |
+
print(f"GLM-4.5回复: {response}")
|
358 |
+
|
359 |
+
print("\n=== 测试GLM-4.5流式对话 ===")
|
360 |
+
def stream_callback(content):
|
361 |
+
print(content, end='', flush=True)
|
362 |
+
|
363 |
+
response = glm45.chat_stream(messages, stream_callback)
|
364 |
+
print(f"\n完整回复: {response}")
|
365 |
+
|
366 |
+
# 测试DeepSeek-V3.1
|
367 |
+
deepseek_v31 = manager.get_model('deepseek_v31')
|
368 |
+
messages = [{"role": "user", "content": "你好,请简单介绍一下自己"}]
|
369 |
+
|
370 |
+
print("\n=== 测试DeepSeek-V3.1普通对话 ===")
|
371 |
+
response = deepseek_v31.chat(messages)
|
372 |
+
print(f"DeepSeek-V3.1回复: {response}")
|
373 |
+
|
374 |
+
print("\n=== 测试DeepSeek-V3.1流式对话 ===")
|
375 |
+
response = deepseek_v31.chat_stream(messages, stream_callback)
|
376 |
+
print(f"\n完整回复: {response}")
|
src/test.py
ADDED
File without changes
|
src/旧文件/1_辩论系统.py
ADDED
@@ -0,0 +1,414 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
AI大模型辩论系统主程序
|
4 |
+
整合所有模块,提供用户交互界面
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import time
|
10 |
+
import logging
|
11 |
+
import argparse
|
12 |
+
from datetime import datetime
|
13 |
+
|
14 |
+
# 在代码开头强制设置终端编码为UTF-8
|
15 |
+
os.system('chcp 65001 > nul')
|
16 |
+
|
17 |
+
# 获取当前脚本文件所在目录的绝对路径
|
18 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
19 |
+
|
20 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
21 |
+
project_root = os.path.dirname(current_script_dir)
|
22 |
+
|
23 |
+
# 定义数据输入、输出和日志目录
|
24 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
25 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
26 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
27 |
+
|
28 |
+
# 确保目录存在
|
29 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
30 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
31 |
+
|
32 |
+
# 配置日志
|
33 |
+
logging.basicConfig(
|
34 |
+
level=logging.INFO,
|
35 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
36 |
+
handlers=[
|
37 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '系统运行日志.log'), encoding='utf-8'),
|
38 |
+
logging.StreamHandler(sys.stdout)
|
39 |
+
]
|
40 |
+
)
|
41 |
+
logger = logging.getLogger(__name__)
|
42 |
+
|
43 |
+
# 捕获警告并记录到日志
|
44 |
+
logging.captureWarnings(True)
|
45 |
+
|
46 |
+
# 导入自定义模块
|
47 |
+
try:
|
48 |
+
import importlib.util
|
49 |
+
import os
|
50 |
+
|
51 |
+
# 动态导入模型接口模块
|
52 |
+
model_interface_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "2_模型接口.py")
|
53 |
+
model_interface_spec = importlib.util.spec_from_file_location("model_interface", model_interface_path)
|
54 |
+
model_interface = importlib.util.module_from_spec(model_interface_spec)
|
55 |
+
model_interface_spec.loader.exec_module(model_interface)
|
56 |
+
|
57 |
+
# 动态导入辩论控制器模块
|
58 |
+
debate_controller_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "3_辩论控制器.py")
|
59 |
+
debate_controller_spec = importlib.util.spec_from_file_location("debate_controller", debate_controller_path)
|
60 |
+
debate_controller = importlib.util.module_from_spec(debate_controller_spec)
|
61 |
+
debate_controller_spec.loader.exec_module(debate_controller)
|
62 |
+
|
63 |
+
# 动态导入显示界面模块
|
64 |
+
display_interface_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "4_显示界面.py")
|
65 |
+
display_interface_spec = importlib.util.spec_from_file_location("display_interface", display_interface_path)
|
66 |
+
display_interface = importlib.util.module_from_spec(display_interface_spec)
|
67 |
+
display_interface_spec.loader.exec_module(display_interface)
|
68 |
+
|
69 |
+
# 从模块中获取需要的类
|
70 |
+
ModelManager = model_interface.ModelManager
|
71 |
+
DebateController = debate_controller.DebateController
|
72 |
+
DisplayManager = display_interface.DisplayManager
|
73 |
+
|
74 |
+
except ImportError as e:
|
75 |
+
logger.error(f"导入模块失败: {str(e)}")
|
76 |
+
print("请确保所有模块文件都在同一目录下")
|
77 |
+
sys.exit(1)
|
78 |
+
|
79 |
+
class DebateSystem:
|
80 |
+
"""AI大模型辩论系统"""
|
81 |
+
|
82 |
+
def __init__(self, api_key: str, display_type: str = "composite"):
|
83 |
+
"""
|
84 |
+
初始化辩论系统
|
85 |
+
|
86 |
+
Args:
|
87 |
+
api_key: API密钥
|
88 |
+
display_type: 显示类型 ('simple', 'console', 'file', 'composite')
|
89 |
+
"""
|
90 |
+
self.api_key = api_key
|
91 |
+
self.display_type = display_type
|
92 |
+
|
93 |
+
# 初始化模型管理器
|
94 |
+
self.model_manager = ModelManager(api_key)
|
95 |
+
|
96 |
+
# 初始化显示管理器
|
97 |
+
self.display_manager = DisplayManager(display_type)
|
98 |
+
|
99 |
+
# 初始化辩论控制器
|
100 |
+
self.debate_controller = DebateController(
|
101 |
+
self.model_manager,
|
102 |
+
output_callback=self.display_manager.add_message
|
103 |
+
)
|
104 |
+
|
105 |
+
logger.info("AI大模型辩论系统初始化完成")
|
106 |
+
|
107 |
+
def start(self):
|
108 |
+
"""启动辩论系统"""
|
109 |
+
logger.info("启动AI大模型辩论系统")
|
110 |
+
|
111 |
+
# 启动显示界面
|
112 |
+
self.display_manager.start()
|
113 |
+
|
114 |
+
# 显示欢迎信息
|
115 |
+
self.display_manager.add_message("=" * 50 + "\n")
|
116 |
+
self.display_manager.add_message("欢迎使用AI大模型辩论系统\n")
|
117 |
+
self.display_manager.add_message("=" * 50 + "\n\n")
|
118 |
+
|
119 |
+
# 主循环
|
120 |
+
while True:
|
121 |
+
try:
|
122 |
+
# 显示菜单
|
123 |
+
self.display_manager.add_message("请选择操作:\n")
|
124 |
+
self.display_manager.add_message("1. 开始新辩论\n")
|
125 |
+
self.display_manager.add_message("2. 查看历史辩论记录\n")
|
126 |
+
self.display_manager.add_message("3. 系统设置\n")
|
127 |
+
self.display_manager.add_message("4. 退出系统\n")
|
128 |
+
self.display_manager.add_message("请输入选项 (1-4): ")
|
129 |
+
|
130 |
+
# 获取用户输入
|
131 |
+
choice = input().strip()
|
132 |
+
|
133 |
+
if choice == "1":
|
134 |
+
self.start_new_debate()
|
135 |
+
elif choice == "2":
|
136 |
+
self.view_history()
|
137 |
+
elif choice == "3":
|
138 |
+
self.system_settings()
|
139 |
+
elif choice == "4":
|
140 |
+
self.display_manager.add_message("感谢使用AI大模型辩论系统,再见!\n")
|
141 |
+
break
|
142 |
+
else:
|
143 |
+
self.display_manager.add_message("无效选项,请重新输入\n\n")
|
144 |
+
|
145 |
+
except KeyboardInterrupt:
|
146 |
+
self.display_manager.add_message("\n\n检测到中断信号,正在退出系统...\n")
|
147 |
+
break
|
148 |
+
except Exception as e:
|
149 |
+
logger.error(f"系统运行出错: {str(e)}")
|
150 |
+
self.display_manager.add_message(f"系统运行出错: {str(e)}\n\n")
|
151 |
+
|
152 |
+
# 停止显示界面
|
153 |
+
self.display_manager.stop()
|
154 |
+
logger.info("AI大模型辩论系统已关闭")
|
155 |
+
|
156 |
+
def start_new_debate(self):
|
157 |
+
"""开始新辩论"""
|
158 |
+
self.display_manager.add_message("\n--- 开始新辩论 ---\n")
|
159 |
+
|
160 |
+
# 获取辩论话题
|
161 |
+
self.display_manager.add_message("请输入辩论话题: ")
|
162 |
+
topic = input().strip()
|
163 |
+
|
164 |
+
if not topic:
|
165 |
+
self.display_manager.add_message("话题不能为空\n\n")
|
166 |
+
return
|
167 |
+
|
168 |
+
# 获取辩论轮数
|
169 |
+
self.display_manager.add_message("请输入辩论轮数 (默认5轮): ")
|
170 |
+
max_rounds_input = input().strip()
|
171 |
+
|
172 |
+
try:
|
173 |
+
max_rounds = int(max_rounds_input) if max_rounds_input else 5
|
174 |
+
if max_rounds < 1 or max_rounds > 10:
|
175 |
+
self.display_manager.add_message("轮数必须在1-10之间,使用默认值5\n")
|
176 |
+
max_rounds = 5
|
177 |
+
except ValueError:
|
178 |
+
self.display_manager.add_message("无效输入,使用默认值5轮\n")
|
179 |
+
max_rounds = 5
|
180 |
+
|
181 |
+
# 获取首发模型
|
182 |
+
self.display_manager.add_message("请选择首发模型 (1. GLM-4.5, 2. DeepSeek-V3.1, 默认1): ")
|
183 |
+
first_model_input = input().strip()
|
184 |
+
|
185 |
+
if first_model_input == "2":
|
186 |
+
first_model = "deepseek_v31"
|
187 |
+
else:
|
188 |
+
first_model = "glm45"
|
189 |
+
|
190 |
+
# 显示参数设置
|
191 |
+
self.display_manager.add_message(f"\n辩论参数设置:\n")
|
192 |
+
self.display_manager.add_message(f"话题: {topic}\n")
|
193 |
+
self.display_manager.add_message(f"轮数: {max_rounds}\n")
|
194 |
+
self.display_manager.add_message(f"首发模型: {first_model}\n")
|
195 |
+
|
196 |
+
# 确认开始
|
197 |
+
self.display_manager.add_message("\n确认开始辩论? (y/n): ")
|
198 |
+
confirm = input().strip().lower()
|
199 |
+
|
200 |
+
if confirm != "y":
|
201 |
+
self.display_manager.add_message("已取消辩论\n\n")
|
202 |
+
return
|
203 |
+
|
204 |
+
# 创建辩论会话
|
205 |
+
debate_session = self.debate_controller.create_debate(topic, max_rounds, first_model)
|
206 |
+
|
207 |
+
# 开始辩论
|
208 |
+
self.display_manager.add_message("\n辩论即将开始...\n\n")
|
209 |
+
time.sleep(1)
|
210 |
+
|
211 |
+
self.debate_controller.start_debate()
|
212 |
+
|
213 |
+
# 等待辩论结束
|
214 |
+
try:
|
215 |
+
while self.debate_controller.current_session and self.debate_controller.current_session.is_active:
|
216 |
+
time.sleep(1)
|
217 |
+
|
218 |
+
# 检查用户输入
|
219 |
+
if sys.stdin in select.select([sys.stdin], [], [], 0)[0]:
|
220 |
+
user_input = sys.stdin.readline().strip()
|
221 |
+
if user_input.lower() == "stop":
|
222 |
+
self.debate_controller.stop_debate()
|
223 |
+
self.display_manager.add_message("\n用户手动停止辩论\n")
|
224 |
+
elif user_input.lower() == "pause":
|
225 |
+
self.debate_controller.pause_debate()
|
226 |
+
self.display_manager.add_message("\n辩论已暂停\n")
|
227 |
+
elif user_input.lower() == "resume":
|
228 |
+
self.debate_controller.resume_debate()
|
229 |
+
self.display_manager.add_message("\n辩论已恢复\n")
|
230 |
+
|
231 |
+
except KeyboardInterrupt:
|
232 |
+
self.display_manager.add_message("\n\n检测到中断信号,正在停止辩论...\n")
|
233 |
+
self.debate_controller.stop_debate()
|
234 |
+
|
235 |
+
self.display_manager.add_message("\n辩论已结束\n\n")
|
236 |
+
|
237 |
+
def view_history(self):
|
238 |
+
"""查看历史辩论记录"""
|
239 |
+
self.display_manager.add_message("\n--- 历史辩论记录 ---\n")
|
240 |
+
|
241 |
+
# 获取辩论记录目录
|
242 |
+
debate_records_dir = os.path.join(OUTPUT_DIR, "辩论记录")
|
243 |
+
|
244 |
+
if not os.path.exists(debate_records_dir):
|
245 |
+
self.display_manager.add_message("暂无辩论记录\n\n")
|
246 |
+
return
|
247 |
+
|
248 |
+
# 获取所有辩论记录文件
|
249 |
+
record_files = [f for f in os.listdir(debate_records_dir) if f.endswith('.json')]
|
250 |
+
|
251 |
+
if not record_files:
|
252 |
+
self.display_manager.add_message("暂无辩论记录\n\n")
|
253 |
+
return
|
254 |
+
|
255 |
+
# 显示辩论记录列表
|
256 |
+
self.display_manager.add_message("可用的辩论记录:\n")
|
257 |
+
for i, filename in enumerate(record_files, 1):
|
258 |
+
self.display_manager.add_message(f"{i}. {filename}\n")
|
259 |
+
|
260 |
+
# 选择查看记录
|
261 |
+
self.display_manager.add_message("\n请选择要查看的记录 (输入序号,0返回): ")
|
262 |
+
choice = input().strip()
|
263 |
+
|
264 |
+
try:
|
265 |
+
choice_num = int(choice)
|
266 |
+
if choice_num == 0:
|
267 |
+
self.display_manager.add_message("\n")
|
268 |
+
return
|
269 |
+
|
270 |
+
if 1 <= choice_num <= len(record_files):
|
271 |
+
selected_file = record_files[choice_num - 1]
|
272 |
+
self.display_debate_record(os.path.join(debate_records_dir, selected_file))
|
273 |
+
else:
|
274 |
+
self.display_manager.add_message("无效选择\n\n")
|
275 |
+
except ValueError:
|
276 |
+
self.display_manager.add_message("无效输入\n\n")
|
277 |
+
|
278 |
+
def display_debate_record(self, file_path: str):
|
279 |
+
"""显示辩论记录"""
|
280 |
+
try:
|
281 |
+
import json
|
282 |
+
|
283 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
284 |
+
data = json.load(f)
|
285 |
+
|
286 |
+
debate_info = data['debate_info']
|
287 |
+
messages = data['messages']
|
288 |
+
|
289 |
+
self.display_manager.add_message(f"\n--- 辩论记录详情 ---\n")
|
290 |
+
self.display_manager.add_message(f"辩论ID: {debate_info['debate_id']}\n")
|
291 |
+
self.display_manager.add_message(f"话题: {debate_info['topic']}\n")
|
292 |
+
self.display_manager.add_message(f"最大轮数: {debate_info['max_rounds']}\n")
|
293 |
+
self.display_manager.add_message(f"实际轮数: {debate_info['current_round']}\n")
|
294 |
+
self.display_manager.add_message(f"总消息数: {debate_info['total_messages']}\n")
|
295 |
+
|
296 |
+
if debate_info['start_time']:
|
297 |
+
start_time = datetime.fromisoformat(debate_info['start_time'])
|
298 |
+
self.display_manager.add_message(f"开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
299 |
+
|
300 |
+
if debate_info['end_time']:
|
301 |
+
end_time = datetime.fromisoformat(debate_info['end_time'])
|
302 |
+
self.display_manager.add_message(f"结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}\n")
|
303 |
+
|
304 |
+
if debate_info['duration']:
|
305 |
+
duration = int(debate_info['duration'])
|
306 |
+
minutes, seconds = divmod(duration, 60)
|
307 |
+
self.display_manager.add_message(f"持续时间: {minutes}分{seconds}秒\n")
|
308 |
+
|
309 |
+
self.display_manager.add_message("\n--- 辩论内容 ---\n")
|
310 |
+
|
311 |
+
for msg in messages:
|
312 |
+
timestamp = datetime.fromisoformat(msg['timestamp'])
|
313 |
+
self.display_manager.add_message(f"[{timestamp.strftime('%H:%M:%S')}] {msg['model_name']}: {msg['content']}\n\n")
|
314 |
+
|
315 |
+
except Exception as e:
|
316 |
+
logger.error(f"显示辩论记录时出错: {str(e)}")
|
317 |
+
self.display_manager.add_message(f"显示辩论记录时出错: {str(e)}\n\n")
|
318 |
+
|
319 |
+
def system_settings(self):
|
320 |
+
"""系统设置"""
|
321 |
+
self.display_manager.add_message("\n--- 系统设置 ---\n")
|
322 |
+
|
323 |
+
while True:
|
324 |
+
self.display_manager.add_message("请选择设置项:\n")
|
325 |
+
self.display_manager.add_message("1. 更改显示类型\n")
|
326 |
+
self.display_manager.add_message("2. 查看系统信息\n")
|
327 |
+
self.display_manager.add_message("3. 返回主菜单\n")
|
328 |
+
self.display_manager.add_message("请输入选项 (1-3): ")
|
329 |
+
|
330 |
+
choice = input().strip()
|
331 |
+
|
332 |
+
if choice == "1":
|
333 |
+
self.change_display_type()
|
334 |
+
elif choice == "2":
|
335 |
+
self.show_system_info()
|
336 |
+
elif choice == "3":
|
337 |
+
self.display_manager.add_message("\n")
|
338 |
+
break
|
339 |
+
else:
|
340 |
+
self.display_manager.add_message("无效选项,请重新输入\n\n")
|
341 |
+
|
342 |
+
def change_display_type(self):
|
343 |
+
"""更改显示类型"""
|
344 |
+
self.display_manager.add_message("\n当前显示类型: " + self.display_type + "\n")
|
345 |
+
self.display_manager.add_message("可选显示类型:\n")
|
346 |
+
self.display_manager.add_message("1. simple - 简单控制台输出\n")
|
347 |
+
self.display_manager.add_message("2. console - 控制台实时显示\n")
|
348 |
+
self.display_manager.add_message("3. file - 仅保存到文件\n")
|
349 |
+
self.display_manager.add_message("4. composite - 复合显示 (默认)\n")
|
350 |
+
self.display_manager.add_message("请输入选项 (1-4): ")
|
351 |
+
|
352 |
+
choice = input().strip()
|
353 |
+
|
354 |
+
display_types = {
|
355 |
+
"1": "simple",
|
356 |
+
"2": "console",
|
357 |
+
"3": "file",
|
358 |
+
"4": "composite"
|
359 |
+
}
|
360 |
+
|
361 |
+
if choice in display_types:
|
362 |
+
# 停止当前显示
|
363 |
+
self.display_manager.stop()
|
364 |
+
|
365 |
+
# 更改显示类型
|
366 |
+
self.display_type = display_types[choice]
|
367 |
+
|
368 |
+
# 重新初始化显示管理器
|
369 |
+
self.display_manager = DisplayManager(self.display_type)
|
370 |
+
|
371 |
+
# 更新辩论控制器的回调
|
372 |
+
self.debate_controller.output_callback = self.display_manager.add_message
|
373 |
+
|
374 |
+
# 启动新显示
|
375 |
+
self.display_manager.start()
|
376 |
+
|
377 |
+
self.display_manager.add_message(f"\n显示类型已更改为: {self.display_type}\n\n")
|
378 |
+
else:
|
379 |
+
self.display_manager.add_message("无效选项\n\n")
|
380 |
+
|
381 |
+
def show_system_info(self):
|
382 |
+
"""显示系统信息"""
|
383 |
+
self.display_manager.add_message("\n--- 系统信息 ---\n")
|
384 |
+
self.display_manager.add_message(f"API基础URL: https://api-inference.modelscope.cn/v1/\n")
|
385 |
+
self.display_manager.add_message(f"支持的模型:\n")
|
386 |
+
self.display_manager.add_message(f" - ZhipuAI/GLM-4.5\n")
|
387 |
+
self.display_manager.add_message(f" - deepseek-ai/DeepSeek-V3.1\n")
|
388 |
+
self.display_manager.add_message(f"当前显示类型: {self.display_type}\n")
|
389 |
+
self.display_manager.add_message(f"项目根目录: {project_root}\n")
|
390 |
+
self.display_manager.add_message(f"数据目录: {DATA_DIR}\n")
|
391 |
+
self.display_manager.add_message(f"输出目录: {OUTPUT_DIR}\n")
|
392 |
+
self.display_manager.add_message(f"日志目录: {LOGS_DIR}\n\n")
|
393 |
+
|
394 |
+
def main():
|
395 |
+
"""主函数"""
|
396 |
+
# 解析命令行参数
|
397 |
+
parser = argparse.ArgumentParser(description='AI大模型辩论系统')
|
398 |
+
parser.add_argument('--api-key', type=str, default='ms-b4690538-3224-493a-8f5b-4073d527f788',
|
399 |
+
help='API密钥')
|
400 |
+
parser.add_argument('--display-type', type=str, default='composite',
|
401 |
+
choices=['simple', 'console', 'file', 'composite'],
|
402 |
+
help='显示类型')
|
403 |
+
|
404 |
+
args = parser.parse_args()
|
405 |
+
|
406 |
+
# 创建并启动辩论系统
|
407 |
+
debate_system = DebateSystem(args.api_key, args.display_type)
|
408 |
+
debate_system.start()
|
409 |
+
|
410 |
+
if __name__ == "__main__":
|
411 |
+
# 导入select模块(用于检测用户输入)
|
412 |
+
import select
|
413 |
+
|
414 |
+
main()
|
src/旧文件/4_显示界面.py
ADDED
@@ -0,0 +1,347 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
显示界面模块
|
4 |
+
用于实时显示辩论内容
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import time
|
10 |
+
import threading
|
11 |
+
import logging
|
12 |
+
from typing import Optional, Dict, Any
|
13 |
+
from datetime import datetime
|
14 |
+
import queue
|
15 |
+
|
16 |
+
# 在代码开头强制设置终端编码为UTF-8
|
17 |
+
os.system('chcp 65001 > nul')
|
18 |
+
|
19 |
+
# 获取当前脚本文件所在目录的绝对路径
|
20 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
21 |
+
|
22 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
23 |
+
project_root = os.path.dirname(current_script_dir)
|
24 |
+
|
25 |
+
# 定义数据输入、输出和日志目录
|
26 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
27 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
28 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
29 |
+
|
30 |
+
# 确保目录存在
|
31 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
32 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
33 |
+
|
34 |
+
# 配置日志
|
35 |
+
logging.basicConfig(
|
36 |
+
level=logging.INFO,
|
37 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
38 |
+
handlers=[
|
39 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '显示界面日志.log'), encoding='utf-8'),
|
40 |
+
logging.StreamHandler(sys.stdout)
|
41 |
+
]
|
42 |
+
)
|
43 |
+
logger = logging.getLogger(__name__)
|
44 |
+
|
45 |
+
# 捕获警告并记录到日志
|
46 |
+
logging.captureWarnings(True)
|
47 |
+
|
48 |
+
class ConsoleDisplay:
|
49 |
+
"""控制台显示界面"""
|
50 |
+
|
51 |
+
def __init__(self):
|
52 |
+
"""初始化控制台显示界面"""
|
53 |
+
self.message_queue = queue.Queue()
|
54 |
+
self.display_thread = None
|
55 |
+
self.stop_event = threading.Event()
|
56 |
+
self.is_running = False
|
57 |
+
|
58 |
+
logger.info("控制台显示界面初始化完成")
|
59 |
+
|
60 |
+
def start(self):
|
61 |
+
"""启动显示界面"""
|
62 |
+
if self.is_running:
|
63 |
+
logger.warning("显示界面已经在运行中")
|
64 |
+
return
|
65 |
+
|
66 |
+
self.is_running = True
|
67 |
+
self.stop_event.clear()
|
68 |
+
|
69 |
+
# 启动显示线程
|
70 |
+
self.display_thread = threading.Thread(target=self._display_loop)
|
71 |
+
self.display_thread.daemon = True
|
72 |
+
self.display_thread.start()
|
73 |
+
|
74 |
+
logger.info("显示界面已启动")
|
75 |
+
|
76 |
+
def stop(self):
|
77 |
+
"""停止显示界面"""
|
78 |
+
if not self.is_running:
|
79 |
+
logger.warning("显示界面未在运行")
|
80 |
+
return
|
81 |
+
|
82 |
+
self.is_running = False
|
83 |
+
self.stop_event.set()
|
84 |
+
|
85 |
+
if self.display_thread and self.display_thread.is_alive():
|
86 |
+
self.display_thread.join(timeout=1)
|
87 |
+
|
88 |
+
logger.info("显示界面已停止")
|
89 |
+
|
90 |
+
def add_message(self, message: str):
|
91 |
+
"""添加消息到显示队列"""
|
92 |
+
if not self.is_running:
|
93 |
+
logger.warning("显示界面未在运行,无法添加消息")
|
94 |
+
return
|
95 |
+
|
96 |
+
self.message_queue.put(message)
|
97 |
+
|
98 |
+
def clear_screen(self):
|
99 |
+
"""清空屏幕"""
|
100 |
+
os.system('cls' if os.name == 'nt' else 'clear')
|
101 |
+
|
102 |
+
def _display_loop(self):
|
103 |
+
"""显示主循环"""
|
104 |
+
buffer = ""
|
105 |
+
|
106 |
+
while not self.stop_event.is_set():
|
107 |
+
try:
|
108 |
+
# 从队列获取消息,设置超时以便检查停止事件
|
109 |
+
message = self.message_queue.get(timeout=0.1)
|
110 |
+
|
111 |
+
# 将消息添加到缓冲区
|
112 |
+
buffer += message
|
113 |
+
|
114 |
+
# 清空屏幕并显示缓冲区内容
|
115 |
+
self.clear_screen()
|
116 |
+
print(buffer, end='', flush=True)
|
117 |
+
|
118 |
+
# 标记消息已处理
|
119 |
+
self.message_queue.task_done()
|
120 |
+
|
121 |
+
except queue.Empty:
|
122 |
+
# 队列为空,继续循环
|
123 |
+
continue
|
124 |
+
except Exception as e:
|
125 |
+
logger.error(f"显示消息时出错: {str(e)}")
|
126 |
+
continue
|
127 |
+
|
128 |
+
class SimpleDisplay:
|
129 |
+
"""简单显示界面,直接输出到控制台"""
|
130 |
+
|
131 |
+
def __init__(self):
|
132 |
+
"""初始化简单显示界面"""
|
133 |
+
logger.info("简单显示界面初始化完成")
|
134 |
+
|
135 |
+
def start(self):
|
136 |
+
"""启动显示界面"""
|
137 |
+
logger.info("简单显示界面已启动")
|
138 |
+
|
139 |
+
def stop(self):
|
140 |
+
"""停止显示界面"""
|
141 |
+
logger.info("简单显示界面已停止")
|
142 |
+
|
143 |
+
def add_message(self, message: str):
|
144 |
+
"""添加消息到显示"""
|
145 |
+
print(message, end='', flush=True)
|
146 |
+
|
147 |
+
class FileDisplay:
|
148 |
+
"""文件显示界面,将辩论内容保存到文件"""
|
149 |
+
|
150 |
+
def __init__(self, output_dir: str = None):
|
151 |
+
"""
|
152 |
+
初始化文件显示界面
|
153 |
+
|
154 |
+
Args:
|
155 |
+
output_dir: 输出目录
|
156 |
+
"""
|
157 |
+
self.output_dir = output_dir or os.path.join(OUTPUT_DIR, "辩论记录")
|
158 |
+
os.makedirs(self.output_dir, exist_ok=True)
|
159 |
+
|
160 |
+
self.current_file = None
|
161 |
+
self.file_handle = None
|
162 |
+
|
163 |
+
logger.info("文件显示界面初始化完成")
|
164 |
+
|
165 |
+
def start(self, debate_id: str = None):
|
166 |
+
"""
|
167 |
+
启动文件显示���面
|
168 |
+
|
169 |
+
Args:
|
170 |
+
debate_id: 辩论ID,用于生成文件名
|
171 |
+
"""
|
172 |
+
if self.file_handle:
|
173 |
+
logger.warning("文件显示界面已经在运行中")
|
174 |
+
return
|
175 |
+
|
176 |
+
# 生成文件名
|
177 |
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
178 |
+
if debate_id:
|
179 |
+
filename = f"{debate_id}_实时记录.txt"
|
180 |
+
else:
|
181 |
+
filename = f"辩论记录_{timestamp}.txt"
|
182 |
+
|
183 |
+
self.current_file = os.path.join(self.output_dir, filename)
|
184 |
+
|
185 |
+
# 打开文件
|
186 |
+
self.file_handle = open(self.current_file, 'w', encoding='utf-8')
|
187 |
+
|
188 |
+
logger.info(f"文件显示界面已启动,输出文件: {self.current_file}")
|
189 |
+
|
190 |
+
def stop(self):
|
191 |
+
"""停止文件显示界面"""
|
192 |
+
if not self.file_handle:
|
193 |
+
logger.warning("文件显示界面未在运行")
|
194 |
+
return
|
195 |
+
|
196 |
+
# 关闭文件
|
197 |
+
self.file_handle.close()
|
198 |
+
self.file_handle = None
|
199 |
+
|
200 |
+
logger.info(f"文件显示界面已停止,输出文件: {self.current_file}")
|
201 |
+
self.current_file = None
|
202 |
+
|
203 |
+
def add_message(self, message: str):
|
204 |
+
"""添加消息到文件"""
|
205 |
+
if not self.file_handle:
|
206 |
+
logger.warning("文件显示界面未在运行,无法添加消息")
|
207 |
+
return
|
208 |
+
|
209 |
+
try:
|
210 |
+
self.file_handle.write(message)
|
211 |
+
self.file_handle.flush()
|
212 |
+
except Exception as e:
|
213 |
+
logger.error(f"写入文件时出错: {str(e)}")
|
214 |
+
|
215 |
+
class CompositeDisplay:
|
216 |
+
"""复合显示界面,可以同时使用多种显示方式"""
|
217 |
+
|
218 |
+
def __init__(self, displays: list = None):
|
219 |
+
"""
|
220 |
+
初始化复合显示界面
|
221 |
+
|
222 |
+
Args:
|
223 |
+
displays: 显示界面列表
|
224 |
+
"""
|
225 |
+
self.displays = displays or []
|
226 |
+
|
227 |
+
logger.info("复合显示界面初始化完成")
|
228 |
+
|
229 |
+
def add_display(self, display):
|
230 |
+
"""添加显示界面"""
|
231 |
+
self.displays.append(display)
|
232 |
+
logger.info("已添加显示界面")
|
233 |
+
|
234 |
+
def start(self, **kwargs):
|
235 |
+
"""启动所有显示界面"""
|
236 |
+
for display in self.displays:
|
237 |
+
try:
|
238 |
+
if hasattr(display, 'start'):
|
239 |
+
if isinstance(display, FileDisplay):
|
240 |
+
display.start(**kwargs)
|
241 |
+
else:
|
242 |
+
display.start()
|
243 |
+
except Exception as e:
|
244 |
+
logger.error(f"启动显示界面时出错: {str(e)}")
|
245 |
+
|
246 |
+
logger.info("所有显示界面已启动")
|
247 |
+
|
248 |
+
def stop(self):
|
249 |
+
"""停止所有显示界面"""
|
250 |
+
for display in self.displays:
|
251 |
+
try:
|
252 |
+
if hasattr(display, 'stop'):
|
253 |
+
display.stop()
|
254 |
+
except Exception as e:
|
255 |
+
logger.error(f"停止显示界面时出错: {str(e)}")
|
256 |
+
|
257 |
+
logger.info("所有显示界面已停止")
|
258 |
+
|
259 |
+
def add_message(self, message: str):
|
260 |
+
"""添加消息到所有显示界面"""
|
261 |
+
for display in self.displays:
|
262 |
+
try:
|
263 |
+
if hasattr(display, 'add_message'):
|
264 |
+
display.add_message(message)
|
265 |
+
except Exception as e:
|
266 |
+
logger.error(f"添加消息到显示界面时出错: {str(e)}")
|
267 |
+
|
268 |
+
class DisplayManager:
|
269 |
+
"""显示管理器,统一管理所有显示界面"""
|
270 |
+
|
271 |
+
def __init__(self, display_type: str = "simple"):
|
272 |
+
"""
|
273 |
+
初始化显示管理器
|
274 |
+
|
275 |
+
Args:
|
276 |
+
display_type: 显示类型 ('simple', 'console', 'file', 'composite')
|
277 |
+
"""
|
278 |
+
self.display_type = display_type
|
279 |
+
self.display = None
|
280 |
+
|
281 |
+
# 根据类型创建显示界面
|
282 |
+
if display_type == "simple":
|
283 |
+
self.display = SimpleDisplay()
|
284 |
+
elif display_type == "console":
|
285 |
+
self.display = ConsoleDisplay()
|
286 |
+
elif display_type == "file":
|
287 |
+
self.display = FileDisplay()
|
288 |
+
elif display_type == "composite":
|
289 |
+
# 复合显示,包含简单显示和文件显示
|
290 |
+
simple_display = SimpleDisplay()
|
291 |
+
file_display = FileDisplay()
|
292 |
+
self.display = CompositeDisplay([simple_display, file_display])
|
293 |
+
else:
|
294 |
+
logger.warning(f"不支持的显示类型: {display_type},使用简单显示")
|
295 |
+
self.display = SimpleDisplay()
|
296 |
+
|
297 |
+
logger.info(f"显示管理器初始化完成,显示类型: {display_type}")
|
298 |
+
|
299 |
+
def start(self, **kwargs):
|
300 |
+
"""启动显示界面"""
|
301 |
+
if self.display:
|
302 |
+
self.display.start(**kwargs)
|
303 |
+
|
304 |
+
def stop(self):
|
305 |
+
"""停止显示界面"""
|
306 |
+
if self.display:
|
307 |
+
self.display.stop()
|
308 |
+
|
309 |
+
def add_message(self, message: str):
|
310 |
+
"""添加消息到显示界面"""
|
311 |
+
if self.display:
|
312 |
+
self.display.add_message(message)
|
313 |
+
|
314 |
+
# 测试代码
|
315 |
+
if __name__ == "__main__":
|
316 |
+
# 测试简单��示
|
317 |
+
print("=== 测试简单显示 ===")
|
318 |
+
simple_display = DisplayManager("simple")
|
319 |
+
simple_display.start()
|
320 |
+
|
321 |
+
for i in range(5):
|
322 |
+
simple_display.add_message(f"简单显示消息 {i}\n")
|
323 |
+
time.sleep(0.5)
|
324 |
+
|
325 |
+
simple_display.stop()
|
326 |
+
|
327 |
+
# 测试文件显示
|
328 |
+
print("\n=== 测试文件显示 ===")
|
329 |
+
file_display = DisplayManager("file")
|
330 |
+
file_display.start(debate_id="test_debate")
|
331 |
+
|
332 |
+
for i in range(5):
|
333 |
+
file_display.add_message(f"文件显示消息 {i}\n")
|
334 |
+
time.sleep(0.5)
|
335 |
+
|
336 |
+
file_display.stop()
|
337 |
+
|
338 |
+
# 测试复合显示
|
339 |
+
print("\n=== 测试复合显示 ===")
|
340 |
+
composite_display = DisplayManager("composite")
|
341 |
+
composite_display.start(debate_id="test_composite")
|
342 |
+
|
343 |
+
for i in range(5):
|
344 |
+
composite_display.add_message(f"复合显示消息 {i}\n")
|
345 |
+
time.sleep(0.5)
|
346 |
+
|
347 |
+
composite_display.stop()
|
src/旧文件/5_GUI测试.py
ADDED
@@ -0,0 +1,289 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
GUI测试程序
|
4 |
+
用于验证流式输出功能
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import threading
|
10 |
+
import logging
|
11 |
+
from tkinter import *
|
12 |
+
from tkinter import ttk, scrolledtext, messagebox
|
13 |
+
from tkinter.filedialog import asksaveasfilename
|
14 |
+
import importlib.util
|
15 |
+
|
16 |
+
# 在代码开头强制设置终端编码为UTF-8
|
17 |
+
os.system('chcp 65001 > nul')
|
18 |
+
|
19 |
+
# 获取当前脚本文件所在目录的绝对路径
|
20 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
21 |
+
|
22 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
23 |
+
project_root = os.path.dirname(current_script_dir)
|
24 |
+
|
25 |
+
# 定义数据输入、输出和日志目录
|
26 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
27 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
28 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
29 |
+
|
30 |
+
# 确保目录存在
|
31 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
32 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
33 |
+
|
34 |
+
# 配置日志
|
35 |
+
logging.basicConfig(
|
36 |
+
level=logging.INFO,
|
37 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
38 |
+
handlers=[
|
39 |
+
logging.FileHandler(os.path.join(LOGS_DIR, 'GUI测试日志.log'), encoding='utf-8'),
|
40 |
+
logging.StreamHandler(sys.stdout)
|
41 |
+
]
|
42 |
+
)
|
43 |
+
logger = logging.getLogger(__name__)
|
44 |
+
|
45 |
+
# 捕获警告并记录到日志
|
46 |
+
logging.captureWarnings(True)
|
47 |
+
|
48 |
+
# 导入自定义模块
|
49 |
+
try:
|
50 |
+
# 动态导入模型接口模块
|
51 |
+
model_interface_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "2_模型接口.py")
|
52 |
+
model_interface_spec = importlib.util.spec_from_file_location("model_interface", model_interface_path)
|
53 |
+
model_interface = importlib.util.module_from_spec(model_interface_spec)
|
54 |
+
model_interface_spec.loader.exec_module(model_interface)
|
55 |
+
|
56 |
+
# 从模块中获取需要的类
|
57 |
+
ModelManager = model_interface.ModelManager
|
58 |
+
|
59 |
+
except ImportError as e:
|
60 |
+
logger.error(f"导入模块失败: {str(e)}")
|
61 |
+
print("请确保所有模块文件都在同一目录下")
|
62 |
+
sys.exit(1)
|
63 |
+
|
64 |
+
class StreamOutputGUI:
|
65 |
+
"""流式输出GUI界面"""
|
66 |
+
|
67 |
+
def __init__(self, root):
|
68 |
+
self.root = root
|
69 |
+
self.root.title("AI大模型流式输出测试")
|
70 |
+
self.root.geometry("800x600")
|
71 |
+
|
72 |
+
# API密钥
|
73 |
+
self.api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
74 |
+
|
75 |
+
# 初始化模型管理器
|
76 |
+
self.model_manager = ModelManager(self.api_key)
|
77 |
+
|
78 |
+
# 当前使用的模型
|
79 |
+
self.current_model = "glm45"
|
80 |
+
|
81 |
+
# 是否正在生成
|
82 |
+
self.is_generating = False
|
83 |
+
|
84 |
+
# 创建界面
|
85 |
+
self.create_widgets()
|
86 |
+
|
87 |
+
logger.info("GUI界面初始化完成")
|
88 |
+
|
89 |
+
def create_widgets(self):
|
90 |
+
"""创建界面组件"""
|
91 |
+
# 主框架
|
92 |
+
main_frame = ttk.Frame(self.root, padding="10")
|
93 |
+
main_frame.grid(row=0, column=0, sticky=(W, E, N, S))
|
94 |
+
|
95 |
+
# 配置网格权重
|
96 |
+
self.root.columnconfigure(0, weight=1)
|
97 |
+
self.root.rowconfigure(0, weight=1)
|
98 |
+
main_frame.columnconfigure(1, weight=1)
|
99 |
+
main_frame.rowconfigure(2, weight=1)
|
100 |
+
|
101 |
+
# 模型选择
|
102 |
+
ttk.Label(main_frame, text="选择模型:").grid(row=0, column=0, sticky=W, padx=(0, 10))
|
103 |
+
self.model_var = StringVar(value="glm45")
|
104 |
+
model_combo = ttk.Combobox(main_frame, textvariable=self.model_var,
|
105 |
+
values=["glm45", "deepseek_v31"], state="readonly", width=15)
|
106 |
+
model_combo.grid(row=0, column=1, sticky=W, padx=(0, 10))
|
107 |
+
model_combo.bind("<<ComboboxSelected>>", self.on_model_change)
|
108 |
+
|
109 |
+
# 输入框
|
110 |
+
ttk.Label(main_frame, text="输入提示:").grid(row=1, column=0, sticky=W, padx=(0, 10), pady=(10, 0))
|
111 |
+
self.input_text = scrolledtext.ScrolledText(main_frame, height=3, width=60)
|
112 |
+
self.input_text.grid(row=1, column=1, columnspan=2, sticky=(W, E), pady=(10, 0))
|
113 |
+
self.input_text.insert("1.0", "请用100字介绍一下人工智能的发展历程")
|
114 |
+
|
115 |
+
# 控制按钮框架
|
116 |
+
button_frame = ttk.Frame(main_frame)
|
117 |
+
button_frame.grid(row=2, column=0, columnspan=3, sticky=(W, E), pady=(10, 0))
|
118 |
+
|
119 |
+
# 开始按钮
|
120 |
+
self.start_button = ttk.Button(button_frame, text="开始生成", command=self.start_generation)
|
121 |
+
self.start_button.pack(side=LEFT, padx=(0, 10))
|
122 |
+
|
123 |
+
# 停止按钮
|
124 |
+
self.stop_button = ttk.Button(button_frame, text="停止生成", command=self.stop_generation, state=DISABLED)
|
125 |
+
self.stop_button.pack(side=LEFT, padx=(0, 10))
|
126 |
+
|
127 |
+
# 清空按钮
|
128 |
+
ttk.Button(button_frame, text="清空输出", command=self.clear_output).pack(side=LEFT, padx=(0, 10))
|
129 |
+
|
130 |
+
# 保存按钮
|
131 |
+
ttk.Button(button_frame, text="保存输出", command=self.save_output).pack(side=LEFT, padx=(0, 10))
|
132 |
+
|
133 |
+
# 输出框
|
134 |
+
ttk.Label(main_frame, text="输出结果:").grid(row=3, column=0, sticky=W, padx=(0, 10), pady=(10, 0))
|
135 |
+
self.output_text = scrolledtext.ScrolledText(main_frame, height=20, width=80)
|
136 |
+
self.output_text.grid(row=4, column=0, columnspan=3, sticky=(W, E, N, S), pady=(5, 0))
|
137 |
+
|
138 |
+
# 状态栏
|
139 |
+
self.status_var = StringVar(value="就绪")
|
140 |
+
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=SUNKEN, anchor=W)
|
141 |
+
status_bar.grid(row=5, column=0, columnspan=3, sticky=(W, E), pady=(10, 0))
|
142 |
+
|
143 |
+
def on_model_change(self, event=None):
|
144 |
+
"""模型选择改变事件"""
|
145 |
+
self.current_model = self.model_var.get()
|
146 |
+
logger.info(f"切换模型为: {self.current_model}")
|
147 |
+
self.status_var.set(f"已选择模型: {self.current_model}")
|
148 |
+
|
149 |
+
def start_generation(self):
|
150 |
+
"""开始生成"""
|
151 |
+
if self.is_generating:
|
152 |
+
return
|
153 |
+
|
154 |
+
# 获取输入文本
|
155 |
+
prompt = self.input_text.get("1.0", END).strip()
|
156 |
+
if not prompt:
|
157 |
+
messagebox.showwarning("警告", "请输入提示文本")
|
158 |
+
return
|
159 |
+
|
160 |
+
# 更新界面状态
|
161 |
+
self.is_generating = True
|
162 |
+
self.start_button.config(state=DISABLED)
|
163 |
+
self.stop_button.config(state=NORMAL)
|
164 |
+
self.status_var.set("正在生成...")
|
165 |
+
|
166 |
+
# 清空输出框
|
167 |
+
self.output_text.delete("1.0", END)
|
168 |
+
|
169 |
+
# 在新线程中执行生成任务
|
170 |
+
thread = threading.Thread(target=self.generate_content, args=(prompt,))
|
171 |
+
thread.daemon = True
|
172 |
+
thread.start()
|
173 |
+
|
174 |
+
logger.info(f"开始生成内容,模型: {self.current_model},提示: {prompt}")
|
175 |
+
|
176 |
+
def stop_generation(self):
|
177 |
+
"""停止生成"""
|
178 |
+
self.is_generating = False
|
179 |
+
self.start_button.config(state=NORMAL)
|
180 |
+
self.stop_button.config(state=DISABLED)
|
181 |
+
self.status_var.set("已停止生成")
|
182 |
+
logger.info("停止生成")
|
183 |
+
|
184 |
+
def generate_content(self, prompt):
|
185 |
+
"""生成内容"""
|
186 |
+
try:
|
187 |
+
# 获取模型接口
|
188 |
+
model = self.model_manager.get_model(self.current_model)
|
189 |
+
|
190 |
+
# 构造消息
|
191 |
+
messages = [{"role": "user", "content": prompt}]
|
192 |
+
|
193 |
+
# 定义流式输出回调函数
|
194 |
+
def stream_callback(content):
|
195 |
+
if self.is_generating:
|
196 |
+
# 在主线程中更新界面
|
197 |
+
self.root.after(0, self.append_output, content)
|
198 |
+
else:
|
199 |
+
# 如果已停止生成,则中断
|
200 |
+
return False
|
201 |
+
|
202 |
+
# 调用流式输出方法
|
203 |
+
full_response = model.chat_stream(messages, stream_callback)
|
204 |
+
|
205 |
+
# 生成完成后更新界面
|
206 |
+
self.root.after(0, self.generation_completed)
|
207 |
+
|
208 |
+
logger.info(f"生成完成,响应长度: {len(full_response)} 字符")
|
209 |
+
|
210 |
+
except Exception as e:
|
211 |
+
logger.error(f"生成内容时出错: {str(e)}")
|
212 |
+
self.root.after(0, self.generation_error, str(e))
|
213 |
+
|
214 |
+
def append_output(self, content):
|
215 |
+
"""追加输出内容"""
|
216 |
+
if self.is_generating:
|
217 |
+
self.output_text.insert(END, content)
|
218 |
+
self.output_text.see(END) # 滚动到末尾
|
219 |
+
|
220 |
+
def generation_completed(self):
|
221 |
+
"""生成完成"""
|
222 |
+
self.is_generating = False
|
223 |
+
self.start_button.config(state=NORMAL)
|
224 |
+
self.stop_button.config(state=DISABLED)
|
225 |
+
self.status_var.set("生成完成")
|
226 |
+
logger.info("生成完成")
|
227 |
+
|
228 |
+
def generation_error(self, error_msg):
|
229 |
+
"""生成出错"""
|
230 |
+
self.is_generating = False
|
231 |
+
self.start_button.config(state=NORMAL)
|
232 |
+
self.stop_button.config(state=DISABLED)
|
233 |
+
self.status_var.set("生成出错")
|
234 |
+
self.output_text.insert(END, f"\n[错误] {error_msg}\n")
|
235 |
+
logger.error(f"生成出错: {error_msg}")
|
236 |
+
messagebox.showerror("错误", f"生成内容时出错:\n{error_msg}")
|
237 |
+
|
238 |
+
def clear_output(self):
|
239 |
+
"""清空输出"""
|
240 |
+
self.output_text.delete("1.0", END)
|
241 |
+
self.status_var.set("输出已清空")
|
242 |
+
logger.info("清空输出")
|
243 |
+
|
244 |
+
def save_output(self):
|
245 |
+
"""保存输出"""
|
246 |
+
content = self.output_text.get("1.0", END)
|
247 |
+
if not content.strip():
|
248 |
+
messagebox.showinfo("提示", "没有内容可保存")
|
249 |
+
return
|
250 |
+
|
251 |
+
try:
|
252 |
+
filename = asksaveasfilename(
|
253 |
+
defaultextension=".txt",
|
254 |
+
filetypes=[("文本文件", "*.txt"), ("所有文件", "*.*")],
|
255 |
+
title="保存输出内容"
|
256 |
+
)
|
257 |
+
if filename:
|
258 |
+
with open(filename, "w", encoding="utf-8") as f:
|
259 |
+
f.write(content)
|
260 |
+
self.status_var.set(f"内容已保存到: {filename}")
|
261 |
+
logger.info(f"内容已保存到: {filename}")
|
262 |
+
except Exception as e:
|
263 |
+
logger.error(f"保存文件时出错: {str(e)}")
|
264 |
+
messagebox.showerror("错误", f"保存文件时出错:\n{str(e)}")
|
265 |
+
|
266 |
+
def main():
|
267 |
+
"""主函数"""
|
268 |
+
# 创建根窗口
|
269 |
+
root = Tk()
|
270 |
+
|
271 |
+
# 创建GUI应用
|
272 |
+
app = StreamOutputGUI(root)
|
273 |
+
|
274 |
+
# 设置窗口关闭事件
|
275 |
+
def on_closing():
|
276 |
+
if app.is_generating:
|
277 |
+
if messagebox.askokcancel("确认", "正在生成内容,确定要退出吗?"):
|
278 |
+
app.stop_generation()
|
279 |
+
root.destroy()
|
280 |
+
else:
|
281 |
+
root.destroy()
|
282 |
+
|
283 |
+
root.protocol("WM_DELETE_WINDOW", on_closing)
|
284 |
+
|
285 |
+
# 运行主循环
|
286 |
+
root.mainloop()
|
287 |
+
|
288 |
+
if __name__ == "__main__":
|
289 |
+
main()
|
src/旧文件/6_辩论系统GUI.py
ADDED
@@ -0,0 +1,732 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# -*- coding: utf-8 -*-
|
2 |
+
"""
|
3 |
+
AI大模型辩论系统GUI版本
|
4 |
+
提供图形用户界面的辩论系统
|
5 |
+
"""
|
6 |
+
|
7 |
+
import os
|
8 |
+
import sys
|
9 |
+
import threading
|
10 |
+
import time
|
11 |
+
import json
|
12 |
+
import logging
|
13 |
+
from datetime import datetime
|
14 |
+
from tkinter import *
|
15 |
+
from tkinter import ttk, scrolledtext, messagebox, filedialog
|
16 |
+
import importlib.util
|
17 |
+
|
18 |
+
# 在代码开头强制设置终端编码为UTF-8
|
19 |
+
os.system('chcp 65001 > nul')
|
20 |
+
|
21 |
+
# 获取当前脚本文件所在目录的绝对路径
|
22 |
+
current_script_dir = os.path.dirname(os.path.abspath(__file__))
|
23 |
+
|
24 |
+
# 项目根目录 (即 '20250907_大模型辩论' 目录, 是 'src' 的上一级)
|
25 |
+
project_root = os.path.dirname(current_script_dir)
|
26 |
+
|
27 |
+
# 定义数据输入、输出和日志目录
|
28 |
+
DATA_DIR = os.path.join(project_root, 'data')
|
29 |
+
OUTPUT_DIR = os.path.join(project_root, 'output')
|
30 |
+
LOGS_DIR = os.path.join(project_root, 'logs')
|
31 |
+
|
32 |
+
# 确保目录存在
|
33 |
+
os.makedirs(OUTPUT_DIR, exist_ok=True)
|
34 |
+
os.makedirs(LOGS_DIR, exist_ok=True)
|
35 |
+
|
36 |
+
# 配置日志
|
37 |
+
logging.basicConfig(
|
38 |
+
level=logging.INFO,
|
39 |
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
40 |
+
handlers=[
|
41 |
+
logging.FileHandler(os.path.join(LOGS_DIR, '辩论系统GUI日志.log'), encoding='utf-8'),
|
42 |
+
logging.StreamHandler(sys.stdout)
|
43 |
+
]
|
44 |
+
)
|
45 |
+
logger = logging.getLogger(__name__)
|
46 |
+
|
47 |
+
# 捕获警告并记录到日志
|
48 |
+
logging.captureWarnings(True)
|
49 |
+
|
50 |
+
# 导入自定义模块
|
51 |
+
try:
|
52 |
+
# 动态导入模型接口模块
|
53 |
+
model_interface_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "2_模型接口.py")
|
54 |
+
model_interface_spec = importlib.util.spec_from_file_location("model_interface", model_interface_path)
|
55 |
+
model_interface = importlib.util.module_from_spec(model_interface_spec)
|
56 |
+
model_interface_spec.loader.exec_module(model_interface)
|
57 |
+
|
58 |
+
# 动态导入辩论控制器模块
|
59 |
+
debate_controller_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "3_辩论控制器.py")
|
60 |
+
debate_controller_spec = importlib.util.spec_from_file_location("debate_controller", debate_controller_path)
|
61 |
+
debate_controller = importlib.util.module_from_spec(debate_controller_spec)
|
62 |
+
debate_controller_spec.loader.exec_module(debate_controller)
|
63 |
+
|
64 |
+
# 从模块中获取需要的类
|
65 |
+
ModelManager = model_interface.ModelManager
|
66 |
+
DebateMessage = debate_controller.DebateMessage
|
67 |
+
DebateSession = debate_controller.DebateSession
|
68 |
+
|
69 |
+
except ImportError as e:
|
70 |
+
logger.error(f"导入模块失败: {str(e)}")
|
71 |
+
messagebox.showerror("错误", f"导入模块失败: {str(e)}\n请确保所有模块文件都在同一目录下")
|
72 |
+
sys.exit(1)
|
73 |
+
|
74 |
+
class DebateSystemGUI:
|
75 |
+
"""AI大模型辩论系统GUI版本"""
|
76 |
+
|
77 |
+
def __init__(self, root):
|
78 |
+
self.root = root
|
79 |
+
self.root.title("AI大模型辩论系统")
|
80 |
+
self.root.geometry("1000x700")
|
81 |
+
self.root.minsize(800, 600)
|
82 |
+
|
83 |
+
# API密钥
|
84 |
+
self.api_key = "ms-b4690538-3224-493a-8f5b-4073d527f788"
|
85 |
+
|
86 |
+
# 初始化模型管理器
|
87 |
+
self.model_manager = ModelManager(self.api_key)
|
88 |
+
|
89 |
+
# 辩论会话
|
90 |
+
self.debate_session = None
|
91 |
+
|
92 |
+
# 是否正在辩论
|
93 |
+
self.is_debating = False
|
94 |
+
|
95 |
+
# 暂停标志
|
96 |
+
self.is_paused = False
|
97 |
+
|
98 |
+
# 创建界面
|
99 |
+
self.create_widgets()
|
100 |
+
|
101 |
+
logger.info("AI大模型辩论系统GUI初始化完成")
|
102 |
+
|
103 |
+
def create_widgets(self):
|
104 |
+
"""创建界面组件"""
|
105 |
+
# 创建菜单栏
|
106 |
+
self.create_menu()
|
107 |
+
|
108 |
+
# 主框架
|
109 |
+
main_frame = ttk.Frame(self.root, padding="10")
|
110 |
+
main_frame.pack(fill=BOTH, expand=True)
|
111 |
+
|
112 |
+
# 配置网格权重
|
113 |
+
main_frame.columnconfigure(1, weight=1)
|
114 |
+
main_frame.rowconfigure(0, weight=0)
|
115 |
+
main_frame.rowconfigure(1, weight=1)
|
116 |
+
|
117 |
+
# 参数设置框架
|
118 |
+
settings_frame = ttk.LabelFrame(main_frame, text="辩论参数设置", padding="10")
|
119 |
+
settings_frame.grid(row=0, column=0, columnspan=2, sticky=(W, E), pady=(0, 10))
|
120 |
+
settings_frame.columnconfigure(1, weight=1)
|
121 |
+
settings_frame.columnconfigure(3, weight=1)
|
122 |
+
|
123 |
+
# 辩论话题
|
124 |
+
ttk.Label(settings_frame, text="辩论话题:").grid(row=0, column=0, sticky=W, padx=(0, 5))
|
125 |
+
self.topic_var = StringVar(value="人工智能是否会取代人类的工作")
|
126 |
+
self.topic_entry = ttk.Entry(settings_frame, textvariable=self.topic_var, width=50)
|
127 |
+
self.topic_entry.grid(row=0, column=1, columnspan=3, sticky=(W, E), padx=(0, 10))
|
128 |
+
|
129 |
+
# 辩论轮数
|
130 |
+
ttk.Label(settings_frame, text="辩论轮数:").grid(row=1, column=0, sticky=W, padx=(0, 5), pady=(5, 0))
|
131 |
+
self.rounds_var = StringVar(value="5")
|
132 |
+
self.rounds_entry = ttk.Entry(settings_frame, textvariable=self.rounds_var, width=10)
|
133 |
+
self.rounds_entry.grid(row=1, column=1, sticky=W, padx=(0, 10), pady=(5, 0))
|
134 |
+
|
135 |
+
# 首发模型
|
136 |
+
ttk.Label(settings_frame, text="首发模型:").grid(row=1, column=2, sticky=W, padx=(0, 5), pady=(5, 0))
|
137 |
+
self.first_model_var = StringVar(value="glm45")
|
138 |
+
first_model_combo = ttk.Combobox(settings_frame, textvariable=self.first_model_var,
|
139 |
+
values=["glm45", "deepseek_v31"], state="readonly", width=15)
|
140 |
+
first_model_combo.grid(row=1, column=3, sticky=W, pady=(5, 0))
|
141 |
+
|
142 |
+
# 控制按钮框架
|
143 |
+
control_frame = ttk.Frame(settings_frame)
|
144 |
+
control_frame.grid(row=2, column=0, columnspan=4, sticky=(W, E), pady=(10, 0))
|
145 |
+
|
146 |
+
# 开始辩论按钮
|
147 |
+
self.start_button = ttk.Button(control_frame, text="开始辩论", command=self.start_debate)
|
148 |
+
self.start_button.pack(side=LEFT, padx=(0, 10))
|
149 |
+
|
150 |
+
# 暂停/继续按钮
|
151 |
+
self.pause_button = ttk.Button(control_frame, text="暂停辩论", command=self.toggle_pause, state=DISABLED)
|
152 |
+
self.pause_button.pack(side=LEFT, padx=(0, 10))
|
153 |
+
|
154 |
+
# 停止辩论按钮
|
155 |
+
self.stop_button = ttk.Button(control_frame, text="停止辩论", command=self.stop_debate, state=DISABLED)
|
156 |
+
self.stop_button.pack(side=LEFT, padx=(0, 10))
|
157 |
+
|
158 |
+
# 清空输出按钮
|
159 |
+
ttk.Button(control_frame, text="清空输出", command=self.clear_output).pack(side=LEFT, padx=(0, 10))
|
160 |
+
|
161 |
+
# 辩论内容显示框架
|
162 |
+
debate_frame = ttk.LabelFrame(main_frame, text="辩论内容", padding="10")
|
163 |
+
debate_frame.grid(row=1, column=0, columnspan=2, sticky=(W, E, N, S), pady=(0, 10))
|
164 |
+
debate_frame.columnconfigure(0, weight=1)
|
165 |
+
debate_frame.rowconfigure(0, weight=1)
|
166 |
+
|
167 |
+
# 辩论内容显示区域
|
168 |
+
self.debate_text = scrolledtext.ScrolledText(debate_frame, wrap=WORD, state=DISABLED)
|
169 |
+
self.debate_text.grid(row=0, column=0, sticky=(W, E, N, S))
|
170 |
+
|
171 |
+
# 状态栏
|
172 |
+
self.status_var = StringVar(value="就绪")
|
173 |
+
status_bar = ttk.Label(main_frame, textvariable=self.status_var, relief=SUNKEN, anchor=W)
|
174 |
+
status_bar.grid(row=2, column=0, columnspan=2, sticky=(W, E))
|
175 |
+
|
176 |
+
def create_menu(self):
|
177 |
+
"""创建菜单栏"""
|
178 |
+
menubar = Menu(self.root)
|
179 |
+
self.root.config(menu=menubar)
|
180 |
+
|
181 |
+
# 文件菜单
|
182 |
+
file_menu = Menu(menubar, tearoff=0)
|
183 |
+
menubar.add_cascade(label="文件", menu=file_menu)
|
184 |
+
file_menu.add_command(label="保存辩论记录", command=self.save_debate_record)
|
185 |
+
file_menu.add_command(label="查看历史记录", command=self.view_history)
|
186 |
+
file_menu.add_separator()
|
187 |
+
file_menu.add_command(label="退出", command=self.on_closing)
|
188 |
+
|
189 |
+
# 设置菜单
|
190 |
+
settings_menu = Menu(menubar, tearoff=0)
|
191 |
+
menubar.add_cascade(label="设置", menu=settings_menu)
|
192 |
+
settings_menu.add_command(label="API密钥设置", command=self.set_api_key)
|
193 |
+
|
194 |
+
# 帮助菜单
|
195 |
+
help_menu = Menu(menubar, tearoff=0)
|
196 |
+
menubar.add_cascade(label="帮助", menu=help_menu)
|
197 |
+
help_menu.add_command(label="关于", command=self.show_about)
|
198 |
+
|
199 |
+
def start_debate(self):
|
200 |
+
"""开始辩论"""
|
201 |
+
if self.is_debating:
|
202 |
+
return
|
203 |
+
|
204 |
+
# 获取参数
|
205 |
+
topic = self.topic_var.get().strip()
|
206 |
+
if not topic:
|
207 |
+
messagebox.showwarning("警告", "请输入辩论话题")
|
208 |
+
return
|
209 |
+
|
210 |
+
try:
|
211 |
+
rounds = int(self.rounds_var.get())
|
212 |
+
if rounds < 1 or rounds > 10:
|
213 |
+
messagebox.showwarning("警告", "辩论轮数必须在1-10之间")
|
214 |
+
return
|
215 |
+
except ValueError:
|
216 |
+
messagebox.showwarning("警告", "请输入有效的辩论轮数")
|
217 |
+
return
|
218 |
+
|
219 |
+
first_model = self.first_model_var.get()
|
220 |
+
|
221 |
+
# 更新界面状态
|
222 |
+
self.is_debating = True
|
223 |
+
self.is_paused = False
|
224 |
+
self.start_button.config(state=DISABLED)
|
225 |
+
self.pause_button.config(state=NORMAL, text="暂停辩论")
|
226 |
+
self.stop_button.config(state=NORMAL)
|
227 |
+
self.topic_entry.config(state=DISABLED)
|
228 |
+
self.rounds_entry.config(state=DISABLED)
|
229 |
+
self.first_model_var.set(first_model)
|
230 |
+
|
231 |
+
# 清空输出
|
232 |
+
self.clear_output()
|
233 |
+
|
234 |
+
# 显示辩论开始信息
|
235 |
+
self.append_output("=" * 50 + "\n")
|
236 |
+
self.append_output("AI大模型辩论系统\n")
|
237 |
+
self.append_output("=" * 50 + "\n\n")
|
238 |
+
self.append_output(f"辩论话题: {topic}\n")
|
239 |
+
self.append_output(f"辩论轮数: {rounds}\n")
|
240 |
+
self.append_output(f"首发模型: {first_model}\n")
|
241 |
+
self.append_output("\n辩论即将开始...\n\n")
|
242 |
+
|
243 |
+
# 更新状态栏
|
244 |
+
self.status_var.set("正在辩论中...")
|
245 |
+
|
246 |
+
# 在新线程中执行辩论任务
|
247 |
+
thread = threading.Thread(target=self.run_debate, args=(topic, rounds, first_model))
|
248 |
+
thread.daemon = True
|
249 |
+
thread.start()
|
250 |
+
|
251 |
+
logger.info(f"开始辩论,���题: {topic},轮数: {rounds},首发模型: {first_model}")
|
252 |
+
|
253 |
+
def run_debate(self, topic, rounds, first_model):
|
254 |
+
"""运行辩论"""
|
255 |
+
try:
|
256 |
+
# 创建辩论会话
|
257 |
+
self.debate_session = DebateSession(topic, rounds, first_model)
|
258 |
+
|
259 |
+
# 确定模型顺序
|
260 |
+
model_a_name = first_model
|
261 |
+
model_b_name = 'deepseek_v31' if model_a_name == 'glm45' else 'glm45'
|
262 |
+
|
263 |
+
# 获取模型接口
|
264 |
+
model_a = self.model_manager.get_model(model_a_name)
|
265 |
+
model_b = self.model_manager.get_model(model_b_name)
|
266 |
+
|
267 |
+
# 构建初始提示
|
268 |
+
initial_prompt_a = f"你将作为正方,就以下话题进行辩论:{topic}。请提出你的主要论点。"
|
269 |
+
initial_prompt_b = f"你将作为反方,就以下话题进行辩论:{topic}。请针对对方的论点进行反驳。"
|
270 |
+
|
271 |
+
# 更新状态
|
272 |
+
self.debate_session.current_round = 1
|
273 |
+
self.debate_session.start_time = datetime.now()
|
274 |
+
|
275 |
+
# 显示开始信息
|
276 |
+
self.root.after(0, self.append_output, f"=== 辩论开始 ===\n话题: {topic}\n")
|
277 |
+
self.root.after(0, self.append_output, f"--- 第1轮 ---\n")
|
278 |
+
|
279 |
+
# 第一轮:模型A提出论点
|
280 |
+
if not self.is_debating:
|
281 |
+
return
|
282 |
+
|
283 |
+
# 等待暂停
|
284 |
+
while self.is_paused and self.is_debating:
|
285 |
+
time.sleep(0.1)
|
286 |
+
|
287 |
+
if not self.is_debating:
|
288 |
+
return
|
289 |
+
|
290 |
+
# 模型A发言
|
291 |
+
self.root.after(0, self.append_output, f"{model_a_name} (正方): ")
|
292 |
+
response_a = self.get_model_response(model_a, [{"role": "user", "content": initial_prompt_a}])
|
293 |
+
self.root.after(0, self.append_output, f"{response_a}\n\n")
|
294 |
+
|
295 |
+
# 记录消息
|
296 |
+
self.debate_session.add_message(DebateMessage("user", initial_prompt_a, "system"))
|
297 |
+
self.debate_session.add_message(DebateMessage("assistant", response_a, model_a_name))
|
298 |
+
|
299 |
+
# 后续轮次
|
300 |
+
for round_num in range(2, rounds + 1):
|
301 |
+
# 检查是否继续
|
302 |
+
if not self.is_debating:
|
303 |
+
break
|
304 |
+
|
305 |
+
# 等待暂停
|
306 |
+
while self.is_paused and self.is_debating:
|
307 |
+
time.sleep(0.1)
|
308 |
+
|
309 |
+
if not self.is_debating:
|
310 |
+
break
|
311 |
+
|
312 |
+
self.debate_session.current_round = round_num
|
313 |
+
self.root.after(0, self.append_output, f"--- 第{round_num}轮 ---\n")
|
314 |
+
|
315 |
+
# 模型B反驳
|
316 |
+
self.root.after(0, self.append_output, f"{model_b_name} (反方): ")
|
317 |
+
prompt_b = f"{initial_prompt_b}\n\n对方的论点: {response_a}\n\n请进行反驳。"
|
318 |
+
response_b = self.get_model_response(model_b,
|
319 |
+
self.debate_session.get_messages_for_model(model_b_name) +
|
320 |
+
[{"role": "user", "content": prompt_b}])
|
321 |
+
self.root.after(0, self.append_output, f"{response_b}\n\n")
|
322 |
+
|
323 |
+
# 记录消息
|
324 |
+
self.debate_session.add_message(DebateMessage("user", prompt_b, "system"))
|
325 |
+
self.debate_session.add_message(DebateMessage("assistant", response_b, model_b_name))
|
326 |
+
|
327 |
+
# 检查是否继续
|
328 |
+
if not self.is_debating:
|
329 |
+
break
|
330 |
+
|
331 |
+
# 等待暂停
|
332 |
+
while self.is_paused and self.is_debating:
|
333 |
+
time.sleep(0.1)
|
334 |
+
|
335 |
+
if not self.is_debating:
|
336 |
+
break
|
337 |
+
|
338 |
+
# 模型A回应
|
339 |
+
self.root.after(0, self.append_output, f"{model_a_name} (正方): ")
|
340 |
+
prompt_a = f"请针对对方的反驳进行回应:{response_b}"
|
341 |
+
response_a = self.get_model_response(model_a,
|
342 |
+
self.debate_session.get_messages_for_model(model_a_name) +
|
343 |
+
[{"role": "user", "content": prompt_a}])
|
344 |
+
self.root.after(0, self.append_output, f"{response_a}\n\n")
|
345 |
+
|
346 |
+
# 记录消息
|
347 |
+
self.debate_session.add_message(DebateMessage("user", prompt_a, "system"))
|
348 |
+
self.debate_session.add_message(DebateMessage("assistant", response_a, model_a_name))
|
349 |
+
|
350 |
+
# 辩论结束
|
351 |
+
if self.is_debating:
|
352 |
+
self.debate_session.end_time = datetime.now()
|
353 |
+
self.debate_session.is_active = False
|
354 |
+
self.root.after(0, self.append_output, "=== 辩论结束 ===\n")
|
355 |
+
self.root.after(0, self.debate_completed)
|
356 |
+
|
357 |
+
# 保存辩论记录
|
358 |
+
self.save_debate_record_to_file()
|
359 |
+
|
360 |
+
logger.info("辩论结束")
|
361 |
+
|
362 |
+
except Exception as e:
|
363 |
+
logger.error(f"辩论过程中出错: {str(e)}")
|
364 |
+
self.root.after(0, self.debate_error, str(e))
|
365 |
+
|
366 |
+
def get_model_response(self, model, messages):
|
367 |
+
"""获取模型响应"""
|
368 |
+
try:
|
369 |
+
# 使用列表来存储流式输出的内容(避免作用域问题)
|
370 |
+
full_response = []
|
371 |
+
|
372 |
+
def stream_callback(content):
|
373 |
+
if self.is_debating and not self.is_paused:
|
374 |
+
self.root.after(0, self.append_output, content)
|
375 |
+
full_response.append(content)
|
376 |
+
# 注意:这里不需要return False,因为模型接口会处理流式输出的中断
|
377 |
+
|
378 |
+
# 调用流式输出方法
|
379 |
+
model.chat_stream(messages, stream_callback)
|
380 |
+
return "".join(full_response)
|
381 |
+
except Exception as e:
|
382 |
+
logger.error(f"获取模型响应时出错: {str(e)}")
|
383 |
+
return f"[错误] {str(e)}"
|
384 |
+
|
385 |
+
def toggle_pause(self):
|
386 |
+
"""切换暂停状态"""
|
387 |
+
if not self.is_debating:
|
388 |
+
return
|
389 |
+
|
390 |
+
self.is_paused = not self.is_paused
|
391 |
+
if self.is_paused:
|
392 |
+
self.pause_button.config(text="继续辩论")
|
393 |
+
self.status_var.set("辩论已暂停")
|
394 |
+
logger.info("辩论已暂停")
|
395 |
+
else:
|
396 |
+
self.pause_button.config(text="暂停辩论")
|
397 |
+
self.status_var.set("正在辩论中...")
|
398 |
+
logger.info("辩论已继续")
|
399 |
+
|
400 |
+
def stop_debate(self):
|
401 |
+
"""停止辩论"""
|
402 |
+
self.is_debating = False
|
403 |
+
self.is_paused = False
|
404 |
+
self.start_button.config(state=NORMAL)
|
405 |
+
self.pause_button.config(state=DISABLED, text="暂停辩论")
|
406 |
+
self.stop_button.config(state=DISABLED)
|
407 |
+
self.topic_entry.config(state=NORMAL)
|
408 |
+
self.rounds_entry.config(state=NORMAL)
|
409 |
+
self.status_var.set("辩论已停止")
|
410 |
+
self.append_output("\n用户手动停止辩论\n")
|
411 |
+
logger.info("辩论已停止")
|
412 |
+
|
413 |
+
def debate_completed(self):
|
414 |
+
"""辩论完成"""
|
415 |
+
self.is_debating = False
|
416 |
+
self.is_paused = False
|
417 |
+
self.start_button.config(state=NORMAL)
|
418 |
+
self.pause_button.config(state=DISABLED, text="暂停辩论")
|
419 |
+
self.stop_button.config(state=DISABLED)
|
420 |
+
self.topic_entry.config(state=NORMAL)
|
421 |
+
self.rounds_entry.config(state=NORMAL)
|
422 |
+
self.status_var.set("辩论已完成")
|
423 |
+
logger.info("辩论已完成")
|
424 |
+
|
425 |
+
def debate_error(self, error_msg):
|
426 |
+
"""辩论出错"""
|
427 |
+
self.is_debating = False
|
428 |
+
self.is_paused = False
|
429 |
+
self.start_button.config(state=NORMAL)
|
430 |
+
self.pause_button.config(state=DISABLED, text="暂停辩论")
|
431 |
+
self.stop_button.config(state=DISABLED)
|
432 |
+
self.topic_entry.config(state=NORMAL)
|
433 |
+
self.rounds_entry.config(state=NORMAL)
|
434 |
+
self.status_var.set("辩论出错")
|
435 |
+
self.append_output(f"\n[错误] {error_msg}\n")
|
436 |
+
logger.error(f"辩论出错: {error_msg}")
|
437 |
+
messagebox.showerror("错误", f"辩论过程中出错:\n{error_msg}")
|
438 |
+
|
439 |
+
def append_output(self, content):
|
440 |
+
"""追加输出内容"""
|
441 |
+
self.debate_text.config(state=NORMAL)
|
442 |
+
self.debate_text.insert(END, content)
|
443 |
+
self.debate_text.see(END) # 滚动到末尾
|
444 |
+
self.debate_text.config(state=DISABLED)
|
445 |
+
|
446 |
+
def clear_output(self):
|
447 |
+
"""清空输出"""
|
448 |
+
self.debate_text.config(state=NORMAL)
|
449 |
+
self.debate_text.delete("1.0", END)
|
450 |
+
self.debate_text.config(state=DISABLED)
|
451 |
+
logger.info("清空输出")
|
452 |
+
|
453 |
+
def save_debate_record_to_file(self):
|
454 |
+
"""保存辩论记录到文件"""
|
455 |
+
if not self.debate_session:
|
456 |
+
return
|
457 |
+
|
458 |
+
try:
|
459 |
+
# 创建输出文件路径
|
460 |
+
output_dir = os.path.join(OUTPUT_DIR, "辩论记录")
|
461 |
+
os.makedirs(output_dir, exist_ok=True)
|
462 |
+
|
463 |
+
file_path = os.path.join(output_dir, f"{self.debate_session.debate_id}.json")
|
464 |
+
|
465 |
+
# 保存辩论记录
|
466 |
+
self.debate_session.save_to_file(file_path)
|
467 |
+
|
468 |
+
logger.info(f"辩论记录已保存: {file_path}")
|
469 |
+
except Exception as e:
|
470 |
+
logger.error(f"保存辩论记录时出错: {str(e)}")
|
471 |
+
|
472 |
+
def save_debate_record(self):
|
473 |
+
"""保存当前辩论记录"""
|
474 |
+
if not self.debate_session:
|
475 |
+
messagebox.showinfo("提示", "当前没有辩论记录可保存")
|
476 |
+
return
|
477 |
+
|
478 |
+
try:
|
479 |
+
# 选择保存位置
|
480 |
+
filename = filedialog.asksaveasfilename(
|
481 |
+
defaultextension=".json",
|
482 |
+
filetypes=[("JSON文件", "*.json"), ("所有文件", "*.*")],
|
483 |
+
title="��存辩论记录"
|
484 |
+
)
|
485 |
+
|
486 |
+
if filename:
|
487 |
+
# 保存辩论记录
|
488 |
+
self.debate_session.save_to_file(filename)
|
489 |
+
self.status_var.set(f"辩论记录已保存到: {filename}")
|
490 |
+
logger.info(f"辩论记录已保存到: {filename}")
|
491 |
+
messagebox.showinfo("成功", f"辩论记录已保存到:\n{filename}")
|
492 |
+
except Exception as e:
|
493 |
+
logger.error(f"保存辩论记录时出错: {str(e)}")
|
494 |
+
messagebox.showerror("错误", f"保存辩论记录时出错:\n{str(e)}")
|
495 |
+
|
496 |
+
def view_history(self):
|
497 |
+
"""查看历史记录"""
|
498 |
+
try:
|
499 |
+
# 获取辩论记录目录
|
500 |
+
debate_records_dir = os.path.join(OUTPUT_DIR, "辩论记录")
|
501 |
+
|
502 |
+
if not os.path.exists(debate_records_dir):
|
503 |
+
messagebox.showinfo("提示", "暂无辩论记录")
|
504 |
+
return
|
505 |
+
|
506 |
+
# 获取所有辩论记录文件
|
507 |
+
record_files = [f for f in os.listdir(debate_records_dir) if f.endswith('.json')]
|
508 |
+
|
509 |
+
if not record_files:
|
510 |
+
messagebox.showinfo("提示", "暂无辩论记录")
|
511 |
+
return
|
512 |
+
|
513 |
+
# 创建历史记录窗口
|
514 |
+
history_window = Toplevel(self.root)
|
515 |
+
history_window.title("历史辩论记录")
|
516 |
+
history_window.geometry("600x400")
|
517 |
+
history_window.minsize(400, 300)
|
518 |
+
|
519 |
+
# 创建列表框
|
520 |
+
list_frame = ttk.Frame(history_window, padding="10")
|
521 |
+
list_frame.pack(fill=BOTH, expand=True)
|
522 |
+
|
523 |
+
list_frame.columnconfigure(0, weight=1)
|
524 |
+
list_frame.rowconfigure(0, weight=1)
|
525 |
+
|
526 |
+
# 创建列表框和滚动条
|
527 |
+
listbox = Listbox(list_frame)
|
528 |
+
scrollbar = ttk.Scrollbar(list_frame, orient=VERTICAL, command=listbox.yview)
|
529 |
+
listbox.configure(yscrollcommand=scrollbar.set)
|
530 |
+
|
531 |
+
listbox.grid(row=0, column=0, sticky=(W, E, N, S))
|
532 |
+
scrollbar.grid(row=0, column=1, sticky=(N, S))
|
533 |
+
|
534 |
+
# 添加记录到列表框
|
535 |
+
for filename in record_files:
|
536 |
+
listbox.insert(END, filename)
|
537 |
+
|
538 |
+
# 创建按钮框架
|
539 |
+
button_frame = ttk.Frame(history_window, padding="10")
|
540 |
+
button_frame.pack(fill=X)
|
541 |
+
|
542 |
+
# 查看按钮
|
543 |
+
def view_selected():
|
544 |
+
selection = listbox.curselection()
|
545 |
+
if selection:
|
546 |
+
filename = listbox.get(selection[0])
|
547 |
+
file_path = os.path.join(debate_records_dir, filename)
|
548 |
+
self.display_debate_record(file_path)
|
549 |
+
history_window.destroy()
|
550 |
+
else:
|
551 |
+
messagebox.showwarning("警告", "请选择一个记录文件")
|
552 |
+
|
553 |
+
ttk.Button(button_frame, text="查看", command=view_selected).pack(side=LEFT, padx=(0, 10))
|
554 |
+
|
555 |
+
# 删除按钮
|
556 |
+
def delete_selected():
|
557 |
+
selection = listbox.curselection()
|
558 |
+
if selection:
|
559 |
+
filename = listbox.get(selection[0])
|
560 |
+
if messagebox.askyesno("确认", f"确定要删除记录文件 {filename} 吗?"):
|
561 |
+
file_path = os.path.join(debate_records_dir, filename)
|
562 |
+
try:
|
563 |
+
os.remove(file_path)
|
564 |
+
listbox.delete(selection[0])
|
565 |
+
self.status_var.set(f"已删除记录文件: {filename}")
|
566 |
+
logger.info(f"已删除记录文件: {filename}")
|
567 |
+
except Exception as e:
|
568 |
+
logger.error(f"删除记录文件时出错: {str(e)}")
|
569 |
+
messagebox.showerror("错误", f"删除记录文件时出错:\n{str(e)}")
|
570 |
+
else:
|
571 |
+
messagebox.showwarning("警告", "请选择一个记录文件")
|
572 |
+
|
573 |
+
ttk.Button(button_frame, text="删除", command=delete_selected).pack(side=LEFT, padx=(0, 10))
|
574 |
+
|
575 |
+
# 关闭按钮
|
576 |
+
ttk.Button(button_frame, text="关闭", command=history_window.destroy).pack(side=LEFT)
|
577 |
+
|
578 |
+
except Exception as e:
|
579 |
+
logger.error(f"查看历史记录时出错: {str(e)}")
|
580 |
+
messagebox.showerror("错误", f"查看历史记录时出错:\n{str(e)}")
|
581 |
+
|
582 |
+
def display_debate_record(self, file_path):
|
583 |
+
"""显示辩论记录"""
|
584 |
+
try:
|
585 |
+
with open(file_path, 'r', encoding='utf-8') as f:
|
586 |
+
data = json.load(f)
|
587 |
+
|
588 |
+
debate_info = data['debate_info']
|
589 |
+
messages = data['messages']
|
590 |
+
|
591 |
+
# 创建显示窗口
|
592 |
+
record_window = Toplevel(self.root)
|
593 |
+
record_window.title(f"辩论记录 - {debate_info['debate_id']}")
|
594 |
+
record_window.geometry("800x600")
|
595 |
+
record_window.minsize(600, 400)
|
596 |
+
|
597 |
+
# 创建文本框和滚动条
|
598 |
+
text_frame = ttk.Frame(record_window, padding="10")
|
599 |
+
text_frame.pack(fill=BOTH, expand=True)
|
600 |
+
|
601 |
+
text_frame.columnconfigure(0, weight=1)
|
602 |
+
text_frame.rowconfigure(0, weight=1)
|
603 |
+
|
604 |
+
text_area = scrolledtext.ScrolledText(text_frame, wrap=WORD, state=DISABLED)
|
605 |
+
text_area.grid(row=0, column=0, sticky=(W, E, N, S))
|
606 |
+
|
607 |
+
# 构建显示内容
|
608 |
+
content = f"=== 辩论记录详情 ===\n"
|
609 |
+
content += f"辩论ID: {debate_info['debate_id']}\n"
|
610 |
+
content += f"话题: {debate_info['topic']}\n"
|
611 |
+
content += f"最大轮数: {debate_info['max_rounds']}\n"
|
612 |
+
content += f"实际轮数: {debate_info['current_round']}\n"
|
613 |
+
content += f"总消息数: {debate_info['total_messages']}\n"
|
614 |
+
|
615 |
+
if debate_info['start_time']:
|
616 |
+
start_time = datetime.fromisoformat(debate_info['start_time'])
|
617 |
+
content += f"开始时间: {start_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
618 |
+
|
619 |
+
if debate_info['end_time']:
|
620 |
+
end_time = datetime.fromisoformat(debate_info['end_time'])
|
621 |
+
content += f"结束时间: {end_time.strftime('%Y-%m-%d %H:%M:%S')}\n"
|
622 |
+
|
623 |
+
if debate_info['duration']:
|
624 |
+
duration = int(debate_info['duration'])
|
625 |
+
minutes, seconds = divmod(duration, 60)
|
626 |
+
content += f"持续时间: {minutes}分{seconds}秒\n"
|
627 |
+
|
628 |
+
content += "\n=== 辩论内容 ===\n\n"
|
629 |
+
|
630 |
+
for msg in messages:
|
631 |
+
timestamp = datetime.fromisoformat(msg['timestamp'])
|
632 |
+
content += f"[{timestamp.strftime('%H:%M:%S')}] {msg['model_name']}: {msg['content']}\n\n"
|
633 |
+
|
634 |
+
# 显示内容
|
635 |
+
text_area.config(state=NORMAL)
|
636 |
+
text_area.insert("1.0", content)
|
637 |
+
text_area.config(state=DISABLED)
|
638 |
+
|
639 |
+
except Exception as e:
|
640 |
+
logger.error(f"显示辩论记录时出错: {str(e)}")
|
641 |
+
messagebox.showerror("错误", f"显示辩论记录时出错:\n{str(e)}")
|
642 |
+
|
643 |
+
def set_api_key(self):
|
644 |
+
"""设置API密钥"""
|
645 |
+
# 创建设置窗口
|
646 |
+
settings_window = Toplevel(self.root)
|
647 |
+
settings_window.title("API密钥设置")
|
648 |
+
settings_window.geometry("400x150")
|
649 |
+
settings_window.resizable(False, False)
|
650 |
+
|
651 |
+
# 创建输入框
|
652 |
+
frame = ttk.Frame(settings_window, padding="20")
|
653 |
+
frame.pack(fill=BOTH, expand=True)
|
654 |
+
|
655 |
+
ttk.Label(frame, text="API密钥:").grid(row=0, column=0, sticky=W, pady=(0, 10))
|
656 |
+
|
657 |
+
api_key_var = StringVar(value=self.api_key)
|
658 |
+
api_key_entry = ttk.Entry(frame, textvariable=api_key_var, width=40, show="*")
|
659 |
+
api_key_entry.grid(row=1, column=0, columnspan=2, sticky=(W, E), pady=(0, 20))
|
660 |
+
|
661 |
+
# 按钮框架
|
662 |
+
button_frame = ttk.Frame(frame)
|
663 |
+
button_frame.grid(row=2, column=0, columnspan=2, sticky=E)
|
664 |
+
|
665 |
+
def save_api_key():
|
666 |
+
new_api_key = api_key_var.get().strip()
|
667 |
+
if new_api_key:
|
668 |
+
self.api_key = new_api_key
|
669 |
+
# 重新初始化模型管理器
|
670 |
+
self.model_manager = ModelManager(self.api_key)
|
671 |
+
self.status_var.set("API密钥已更新")
|
672 |
+
logger.info("API密钥已更新")
|
673 |
+
settings_window.destroy()
|
674 |
+
else:
|
675 |
+
messagebox.showwarning("警告", "请输入有效的API密钥")
|
676 |
+
|
677 |
+
ttk.Button(button_frame, text="保存", command=save_api_key).pack(side=LEFT, padx=(0, 10))
|
678 |
+
ttk.Button(button_frame, text="取消", command=settings_window.destroy).pack(side=LEFT)
|
679 |
+
|
680 |
+
def show_about(self):
|
681 |
+
"""显示关于信息"""
|
682 |
+
about_text = """AI大模型辩论系统 GUI版本
|
683 |
+
|
684 |
+
版本: 1.0
|
685 |
+
作者: AI开发者
|
686 |
+
日期: 2025年9月
|
687 |
+
|
688 |
+
本系统支持以下功能:
|
689 |
+
- 两个AI大模型实时辩论
|
690 |
+
- 流式输出显示
|
691 |
+
- 图形用户界面
|
692 |
+
- 辩论记录保存和查看
|
693 |
+
- 暂停/继续/停止控制
|
694 |
+
|
695 |
+
支持的模型:
|
696 |
+
- ZhipuAI/GLM-4.5
|
697 |
+
- deepseek-ai/DeepSeek-V3.1
|
698 |
+
"""
|
699 |
+
messagebox.showinfo("关于", about_text)
|
700 |
+
|
701 |
+
def on_closing(self):
|
702 |
+
"""窗口关闭事件"""
|
703 |
+
if self.is_debating:
|
704 |
+
if messagebox.askokcancel("确认", "正在辩论中,确定要退出吗?"):
|
705 |
+
self.is_debating = False
|
706 |
+
self.root.destroy()
|
707 |
+
else:
|
708 |
+
self.root.destroy()
|
709 |
+
|
710 |
+
def main():
|
711 |
+
"""主函数"""
|
712 |
+
# 创建根窗口
|
713 |
+
root = Tk()
|
714 |
+
|
715 |
+
# 设置窗口图标(如果有的话)
|
716 |
+
try:
|
717 |
+
# root.iconbitmap('icon.ico') # 如果有图标文件可以取消注释
|
718 |
+
pass
|
719 |
+
except:
|
720 |
+
pass
|
721 |
+
|
722 |
+
# 创建GUI应用
|
723 |
+
app = DebateSystemGUI(root)
|
724 |
+
|
725 |
+
# 设置窗口关闭事件
|
726 |
+
root.protocol("WM_DELETE_WINDOW", app.on_closing)
|
727 |
+
|
728 |
+
# 运行主循环
|
729 |
+
root.mainloop()
|
730 |
+
|
731 |
+
if __name__ == "__main__":
|
732 |
+
main()
|
templates/index.html
ADDED
@@ -0,0 +1,178 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
|
2 |
+
<!DOCTYPE html>
|
3 |
+
<html lang="zh-CN">
|
4 |
+
<head>
|
5 |
+
<meta charset="UTF-8">
|
6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
7 |
+
<title>AI大模型辩论系统</title>
|
8 |
+
<style>
|
9 |
+
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; margin: 0; background-color: #f0f2f5; color: #333; height: 100vh; display: flex; flex-direction: column; }
|
10 |
+
.container { background-color: white; padding: 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.1); width: 95%; max-width: 1400px; margin: 20px auto; flex-grow: 1; display: flex; flex-direction: column; }
|
11 |
+
.main-layout { display: flex; gap: 20px; flex-grow: 1; min-height: 0; }
|
12 |
+
.sidebar { flex: 0 0 320px; display: flex; flex-direction: column; gap: 15px; }
|
13 |
+
.chat-area { flex: 1; display: flex; flex-direction: column; min-width: 0; }
|
14 |
+
.header { text-align: center; margin-bottom: 20px; flex-shrink: 0; }
|
15 |
+
.header h1 { color: #1a73e8; }
|
16 |
+
.controls, .control-group { margin-bottom: 20px; }
|
17 |
+
.controls { display: flex; gap: 10px; flex-wrap: wrap; align-items: flex-end; }
|
18 |
+
.control-group { flex: 1; min-width: 200px; }
|
19 |
+
label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; }
|
20 |
+
input, select, button, textarea { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 6px; box-sizing: border-box; font-size: 16px; }
|
21 |
+
textarea { resize: vertical; }
|
22 |
+
button { background-color: #1a73e8; color: white; border: none; cursor: pointer; transition: background-color 0.3s; }
|
23 |
+
button:hover { background-color: #1558b8; }
|
24 |
+
button:disabled { background-color: #ccc; cursor: not-allowed; }
|
25 |
+
.output-container { background-color: #f8f9fa; border: 1px solid #ddd; border-radius: 8px; padding: 20px; height: 500px; overflow-y: auto; display: flex; flex-direction: column; gap: 15px; }
|
26 |
+
.status { padding: 10px; border-radius: 6px; margin-bottom: 15px; border: 1px solid; }
|
27 |
+
.status.connected { background-color: #e6f4ea; color: #155724; border-color: #c3e6cb; }
|
28 |
+
.status.disconnected { background-color: #f8d7da; color: #721c24; border-color: #f5c6cb; }
|
29 |
+
|
30 |
+
/* 聊天气泡样式 */
|
31 |
+
.message { display: flex; align-items: flex-start; gap: 10px; max-width: 80%; }
|
32 |
+
.message .avatar { width: 40px; height: 40px; border-radius: 50%; color: white; display: flex; align-items: center; justify-content: center; font-weight: bold; flex-shrink: 0; font-size: 18px; }
|
33 |
+
.message .content { background-color: #ffffff; padding: 10px 15px; border-radius: 18px; box-shadow: 0 1px 2px rgba(0,0,0,0.1); }
|
34 |
+
.message .sender { font-weight: bold; margin-bottom: 5px; color: #333; }
|
35 |
+
.message.glm45 { align-self: flex-start; }
|
36 |
+
.message.glm45 .avatar { background-color: #34a853; } /* Google Green */
|
37 |
+
.message.glm45 .content { border-top-left-radius: 4px; }
|
38 |
+
.message.deepseek_v31 { align-self: flex-end; flex-direction: row-reverse; }
|
39 |
+
.message.deepseek_v31 .avatar { background-color: #4285f4; } /* Google Blue */
|
40 |
+
.message.deepseek_v31 .content { background-color: #e7f3ff; border-top-right-radius: 4px; }
|
41 |
+
.message .text { white-space: pre-wrap; word-wrap: break-word; }
|
42 |
+
.message .text p { margin: 0 0 10px; }
|
43 |
+
.message .text h1, .message .text h2, .message .text h3 { margin: 15px 0 10px; border-bottom: 1px solid #eee; padding-bottom: 5px; }
|
44 |
+
.message .text ul, .message .text ol { padding-left: 20px; }
|
45 |
+
.message .text code { background-color: #eee; padding: 2px 4px; border-radius: 4px; font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace; }
|
46 |
+
.message .text pre { background-color: #2d2d2d; color: #f8f8f2; padding: 15px; border-radius: 6px; overflow-x: auto; }
|
47 |
+
.message .text pre code { background-color: transparent; padding: 0; }
|
48 |
+
.round-separator { text-align: center; color: #888; font-size: 0.9em; margin: 20px 0; font-weight: 600; }
|
49 |
+
|
50 |
+
/* 响应式设计 - 针对手机等小屏幕设备 */
|
51 |
+
@media (max-width: 768px) {
|
52 |
+
body { padding: 0; }
|
53 |
+
.container { width: 100%; margin: 0; border-radius: 0; padding: 10px; height: 100%; }
|
54 |
+
.main-layout { flex-direction: column; }
|
55 |
+
.sidebar { flex: 0 0 auto; }
|
56 |
+
.header h1 { font-size: 1.5em; }
|
57 |
+
.controls { flex-direction: column; }
|
58 |
+
.control-group { min-width: unset; }
|
59 |
+
}
|
60 |
+
</style>
|
61 |
+
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
62 |
+
<script src="https://cdn.jsdelivr.net/npm/dompurify/dist/purify.min.js"></script>
|
63 |
+
</head>
|
64 |
+
<body>
|
65 |
+
<div class="container">
|
66 |
+
<div class="header"><h1>AI大模型辩论系统</h1><p>观看两个AI大模型实时辩论</p></div>
|
67 |
+
|
68 |
+
<div class="main-layout">
|
69 |
+
<div class="sidebar">
|
70 |
+
<div class="control-group"><label for="topic">辩论话题:</label><input type="text" id="topic" value="人工智能是否会取代人类的工作"></div>
|
71 |
+
<div class="control-group">
|
72 |
+
<label for="initialPrompt">自定义初始提示 (可选):</label>
|
73 |
+
<textarea id="initialPrompt" rows="6" placeholder="例如:请作为正方,用强势的语气开启辩论..."></textarea>
|
74 |
+
</div>
|
75 |
+
<div class="control-group"><label for="rounds">轮数:</label><input type="number" id="rounds" min="1" max="10" value="5"></div>
|
76 |
+
<div class="control-group"><label for="firstModel">首发模型:</label><select id="firstModel"><option value="glm45">ZhipuAI/GLM-4.5</option><option value="deepseek_v31">deepseek-ai/DeepSeek-V3.1</option></select></div>
|
77 |
+
<div class="controls">
|
78 |
+
<button id="startBtn" disabled>开始辩论</button>
|
79 |
+
<button id="stopBtn" disabled>停止辩论</button>
|
80 |
+
</div>
|
81 |
+
</div>
|
82 |
+
<div class="chat-area">
|
83 |
+
<label for="output">辩论实况:</label>
|
84 |
+
<div id="output" class="output-container"></div>
|
85 |
+
</div>
|
86 |
+
</div>
|
87 |
+
</div>
|
88 |
+
<script>
|
89 |
+
let websocket = null, isConnected = false, currentMessageElement = null, currentMessageContent = '';
|
90 |
+
const startBtn = document.getElementById('startBtn'), stopBtn = document.getElementById('stopBtn');
|
91 |
+
const outputDiv = document.getElementById('output');
|
92 |
+
|
93 |
+
function handleWebSocketMessage(data) {
|
94 |
+
let shouldScroll = Math.abs(outputDiv.scrollHeight - outputDiv.clientHeight - outputDiv.scrollTop) < 10;
|
95 |
+
|
96 |
+
switch (data.type) {
|
97 |
+
case 'debate_started':
|
98 |
+
outputDiv.innerHTML = '';
|
99 |
+
const topicDiv = document.createElement('div');
|
100 |
+
topicDiv.className = 'round-separator';
|
101 |
+
topicDiv.innerHTML = `<strong>话题:</strong> ${data.topic}`;
|
102 |
+
outputDiv.appendChild(topicDiv);
|
103 |
+
break;
|
104 |
+
case 'round_info':
|
105 |
+
const separator = document.createElement('div');
|
106 |
+
separator.className = 'round-separator';
|
107 |
+
separator.textContent = data.message.trim();
|
108 |
+
outputDiv.appendChild(separator);
|
109 |
+
break;
|
110 |
+
case 'model_speaking':
|
111 |
+
currentMessageContent = ''; // 重置当前消息内容
|
112 |
+
const messageDiv = document.createElement('div');
|
113 |
+
messageDiv.className = `message ${data.model}`;
|
114 |
+
messageDiv.innerHTML = `
|
115 |
+
<div class="avatar">${data.model.substring(0, 1).toUpperCase()}</div>
|
116 |
+
<div class="content">
|
117 |
+
<div class="sender">${data.model} (${data.role})</div>
|
118 |
+
<div class="text"><i>正在思考...</i></div>
|
119 |
+
</div>`;
|
120 |
+
outputDiv.appendChild(messageDiv);
|
121 |
+
currentMessageElement = messageDiv.querySelector('.text');
|
122 |
+
break;
|
123 |
+
case 'stream_content':
|
124 |
+
if (currentMessageElement) {
|
125 |
+
currentMessageContent += data.content;
|
126 |
+
currentMessageElement.innerHTML = DOMPurify.sanitize(marked.parse(currentMessageContent));
|
127 |
+
}
|
128 |
+
break;
|
129 |
+
case 'stream_end':
|
130 |
+
currentMessageElement = null;
|
131 |
+
break;
|
132 |
+
case 'debate_ended':
|
133 |
+
case 'debate_stopped':
|
134 |
+
const endMsg = document.createElement('div');
|
135 |
+
endMsg.className = 'round-separator';
|
136 |
+
endMsg.textContent = data.message;
|
137 |
+
outputDiv.appendChild(endMsg);
|
138 |
+
startBtn.disabled = false; stopBtn.disabled = true;
|
139 |
+
break;
|
140 |
+
case 'error':
|
141 |
+
outputDiv.innerHTML += `<div class="round-separator" style="color: red;">错误: ${data.message}</div>`;
|
142 |
+
break;
|
143 |
+
}
|
144 |
+
if(shouldScroll) {
|
145 |
+
outputDiv.scrollTop = outputDiv.scrollHeight;
|
146 |
+
}
|
147 |
+
}
|
148 |
+
|
149 |
+
function connect() {
|
150 |
+
const wsUrl = `ws://${window.location.host}/ws`;
|
151 |
+
websocket = new WebSocket(wsUrl);
|
152 |
+
websocket.onopen = () => { isConnected = true; startBtn.disabled = false; };
|
153 |
+
websocket.onmessage = (event) => handleWebSocketMessage(JSON.parse(event.data));
|
154 |
+
websocket.onclose = () => { isConnected = false; startBtn.disabled = true; stopBtn.disabled = true; };
|
155 |
+
websocket.onerror = (error) => { console.error('WebSocket Error:', error); };
|
156 |
+
}
|
157 |
+
|
158 |
+
window.addEventListener('load', connect);
|
159 |
+
|
160 |
+
startBtn.addEventListener('click', () => {
|
161 |
+
if (!websocket) return;
|
162 |
+
const message = {
|
163 |
+
action: "start_debate",
|
164 |
+
topic: document.getElementById('topic').value,
|
165 |
+
rounds: parseInt(document.getElementById('rounds').value),
|
166 |
+
first_model: document.getElementById('firstModel').value,
|
167 |
+
initial_prompt: document.getElementById('initialPrompt').value
|
168 |
+
};
|
169 |
+
websocket.send(JSON.stringify(message));
|
170 |
+
startBtn.disabled = true; stopBtn.disabled = false;
|
171 |
+
});
|
172 |
+
stopBtn.addEventListener('click', () => {
|
173 |
+
if (!websocket) return;
|
174 |
+
websocket.send(JSON.stringify({ action: "stop_debate" }));
|
175 |
+
});
|
176 |
+
</script>
|
177 |
+
</body>
|
178 |
+
</html>
|