1. 优化工作坊列表布局,实现标题与按钮垂直对齐 2. 实现查看结果按钮的防抖功能和加载状态 3. 优化时间排序逻辑,默认按时间降序,点击按钮按时间升序 4. 实现多选功能,勾选框与工作坊名称高度对齐 5. 添加AI结果缓存功能,避免重复API调用 6. 优化空状态显示,居中创建第一个工作坊按钮 7. 完善README.md文档,添加新功能描述和使用指南 8. 调整字体样式,优化视觉效果
368 lines
13 KiB
Python
368 lines
13 KiB
Python
"""
|
||
多 Agent 决策工作坊应用
|
||
|
||
这是一个基于 Flask 的多 Agent 决策工作坊应用,允许用户创建工作坊、配置角色、进行多角色辩论,并使用 DeepSeek API 生成决策要点。
|
||
|
||
主要功能:
|
||
1. 创建新工作坊
|
||
2. 配置工作坊角色及其视角
|
||
3. 进行多角色辩论
|
||
4. 使用 AI 生成决策要点
|
||
5. 查看决策结果
|
||
"""
|
||
|
||
from flask import Flask, render_template, request, redirect, url_for, jsonify
|
||
from datetime import datetime
|
||
import requests
|
||
import json
|
||
from dotenv import load_dotenv
|
||
import os
|
||
|
||
# 加载环境变量(从 .env 文件中读取配置)
|
||
load_dotenv()
|
||
|
||
# 初始化 Flask 应用
|
||
app = Flask(__name__)
|
||
|
||
# DeepSeek API 配置
|
||
# 从环境变量中获取 API 密钥,如果没有则使用默认值
|
||
DEEPSEEK_API_KEY = os.getenv('DEEPSEEK_API_KEY', 'sk-137bb3c986b640ae838d2cd2bfbca1dc')
|
||
DEEPSEEK_API_URL = 'https://api.deepseek.com/v1/chat/completions' # DeepSeek API 端点
|
||
|
||
|
||
def generate_decision_points(workshop):
|
||
"""
|
||
使用 DeepSeek API 生成决策要点
|
||
|
||
Args:
|
||
workshop (dict): 工作坊对象,包含名称、目标和辩论内容
|
||
|
||
Returns:
|
||
str: 生成的决策要点文本
|
||
"""
|
||
# 构建辩论内容的摘要,用于发送给 AI
|
||
debate_summary = "辩论内容:\n"
|
||
for item in workshop['debate_content']:
|
||
debate_summary += f"{item['role']}: {item['opinion']}\n"
|
||
|
||
# 构建系统提示,指导 AI 如何生成决策要点
|
||
system_prompt = f"你是一个决策分析专家,需要基于以下辩论内容,为'{workshop['name']}'工作坊生成全面的决策要点。工作坊目标是:{workshop['goal']}。请从多个角度分析,提取关键决策点,并提供具体的建议。"
|
||
|
||
# 构建请求数据,符合 DeepSeek API 的格式要求
|
||
data = {
|
||
"model": "deepseek-chat", # 使用的模型
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": debate_summary}
|
||
],
|
||
"temperature": 0.7, # 控制生成文本的随机性
|
||
"max_tokens": 1000 # 最大生成 token 数
|
||
}
|
||
|
||
# 构建请求头,包含认证信息
|
||
headers = {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': f'Bearer {DEEPSEEK_API_KEY}'
|
||
}
|
||
|
||
try:
|
||
# 发送请求到 DeepSeek API
|
||
response = requests.post(DEEPSEEK_API_URL, headers=headers, data=json.dumps(data))
|
||
response.raise_for_status() # 检查请求是否成功
|
||
result = response.json() # 解析响应数据
|
||
decision_points = result['choices'][0]['message']['content'] # 提取生成的决策要点
|
||
# 移除井字号和米字号,提高阅读质量
|
||
decision_points = decision_points.replace('#', '').replace('*', '')
|
||
return decision_points
|
||
except Exception as e:
|
||
# 错误处理,当 API 请求失败时,返回简单的决策要点
|
||
print(f"DeepSeek API 请求失败: {e}")
|
||
decision_points = []
|
||
for content in workshop['debate_content']:
|
||
decision_points.append(f"{content['role']}: {content['opinion'][:100]}...")
|
||
return "\n".join(decision_points)
|
||
|
||
|
||
# 存储工作坊数据的临时字典(应用重启后数据会丢失)
|
||
# 键: workshop_id (整数), 值: 工作坊对象 (字典)
|
||
workshops = {}
|
||
|
||
|
||
@app.route('/')
|
||
def index():
|
||
"""
|
||
首页路由,显示所有工作坊列表
|
||
|
||
Returns:
|
||
str: 渲染后的首页 HTML
|
||
"""
|
||
# 获取排序参数,默认为按创建时间排序
|
||
sort_by = request.args.get('sort_by', 'time')
|
||
|
||
# 对工作坊进行排序
|
||
sorted_workshops = []
|
||
if sort_by == 'name':
|
||
# 按名称首字母排序
|
||
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1]['name'])
|
||
elif sort_by == 'time_asc':
|
||
# 按创建时间升序排序
|
||
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min))
|
||
else:
|
||
# 按创建时间降序排序(默认)
|
||
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min), reverse=True)
|
||
|
||
# 将排序后的结果转换回字典格式
|
||
sorted_workshops_dict = {workshop_id: workshop for workshop_id, workshop in sorted_workshops}
|
||
|
||
return render_template('index.html', workshops=sorted_workshops_dict, sort_by=sort_by)
|
||
|
||
|
||
@app.route('/create', methods=['GET', 'POST'])
|
||
def create_workshop():
|
||
"""
|
||
创建新工作坊的路由
|
||
|
||
GET 请求: 显示创建工作坊表单
|
||
POST 请求: 处理表单提交,创建新工作坊并跳转到角色配置页面
|
||
|
||
Returns:
|
||
str: 渲染后的创建工作坊表单或重定向响应
|
||
"""
|
||
if request.method == 'POST':
|
||
# 生成唯一的工作坊 ID
|
||
workshop_id = len(workshops) + 1
|
||
# 获取表单数据
|
||
workshop_name = request.form['workshop_name']
|
||
workshop_goal = request.form['workshop_goal']
|
||
|
||
# 创建新工作坊对象
|
||
workshops[workshop_id] = {
|
||
'name': workshop_name, # 工作坊名称
|
||
'goal': workshop_goal, # 工作坊目标
|
||
'roles': [], # 角色列表,初始为空
|
||
'debate_content': [], # 辩论内容,初始为空
|
||
'decision_points': [], # 决策要点,初始为空
|
||
'final_decision': '', # 最终决策,初始为空
|
||
'created_at': datetime.now() # 创建时间
|
||
}
|
||
|
||
# 重定向到角色配置页面
|
||
return redirect(url_for('configure_roles', workshop_id=workshop_id))
|
||
|
||
# GET 请求,显示创建工作坊表单
|
||
return render_template('create_workshop.html')
|
||
|
||
|
||
@app.route('/workshop/<int:workshop_id>/roles', methods=['GET', 'POST'])
|
||
def configure_roles(workshop_id):
|
||
"""
|
||
配置工作坊角色的路由
|
||
|
||
Args:
|
||
workshop_id (int): 工作坊 ID
|
||
|
||
GET 请求: 显示角色配置表单
|
||
POST 请求: 处理表单提交,添加角色并决定是否继续添加
|
||
|
||
Returns:
|
||
str: 渲染后的角色配置表单或重定向响应
|
||
"""
|
||
# 检查工作坊是否存在
|
||
if workshop_id not in workshops:
|
||
return redirect(url_for('index'))
|
||
|
||
if request.method == 'POST':
|
||
# 检查是否要删除此角色
|
||
if 'remove_current' in request.form:
|
||
# 不添加新角色,直接重定向回当前页面
|
||
return redirect(url_for('configure_roles', workshop_id=workshop_id))
|
||
else:
|
||
# 获取表单数据
|
||
role_name = request.form.get('role_name')
|
||
role_perspective = request.form.get('role_perspective')
|
||
|
||
# 只有当角色名称和视角都存在时才添加新角色
|
||
if role_name and role_perspective:
|
||
# 添加新角色到工作坊
|
||
workshops[workshop_id]['roles'].append({
|
||
'name': role_name, # 角色名称
|
||
'perspective': role_perspective # 角色视角
|
||
})
|
||
|
||
# 检查用户是否要继续添加角色
|
||
if 'add_more' in request.form:
|
||
# 继续添加角色,重定向回当前页面
|
||
return redirect(url_for('configure_roles', workshop_id=workshop_id))
|
||
else:
|
||
# 角色配置完成,跳转到辩论页面
|
||
return redirect(url_for('start_debate', workshop_id=workshop_id))
|
||
|
||
# GET 请求,显示角色配置表单
|
||
return render_template('configure_roles.html', workshop=workshops[workshop_id], workshop_id=workshop_id)
|
||
|
||
|
||
@app.route('/workshop/<int:workshop_id>/debate', methods=['GET', 'POST'])
|
||
def start_debate(workshop_id):
|
||
"""
|
||
开始辩论的路由
|
||
|
||
Args:
|
||
workshop_id (int): 工作坊 ID
|
||
|
||
GET 请求: 显示辩论表单
|
||
POST 请求: 处理表单提交,添加角色观点
|
||
|
||
Returns:
|
||
str: 渲染后的辩论表单或重定向响应
|
||
"""
|
||
# 检查工作坊是否存在
|
||
if workshop_id not in workshops:
|
||
return redirect(url_for('index'))
|
||
|
||
if request.method == 'POST':
|
||
# 获取表单数据
|
||
role_index = int(request.form['role_index'])
|
||
role_opinion = request.form['role_opinion']
|
||
|
||
# 添加角色观点到辩论内容
|
||
workshops[workshop_id]['debate_content'].append({
|
||
'role': workshops[workshop_id]['roles'][role_index]['name'], # 角色名称
|
||
'opinion': role_opinion # 角色观点
|
||
})
|
||
|
||
# 继续辩论,重定向回当前页面
|
||
return redirect(url_for('start_debate', workshop_id=workshop_id))
|
||
|
||
# GET 请求,显示辩论表单
|
||
return render_template('start_debate.html', workshop=workshops[workshop_id], workshop_id=workshop_id)
|
||
|
||
|
||
@app.route('/workshop/<int:workshop_id>/results')
|
||
def show_results(workshop_id):
|
||
"""
|
||
显示决策结果的路由
|
||
|
||
Args:
|
||
workshop_id (int): 工作坊 ID
|
||
|
||
Returns:
|
||
str: 渲染后的结果页面 HTML
|
||
"""
|
||
# 检查工作坊是否存在
|
||
if workshop_id not in workshops:
|
||
return redirect(url_for('index'))
|
||
|
||
# 只有当工作坊没有决策要点时,才生成新的结果
|
||
workshop = workshops[workshop_id]
|
||
if not workshop.get('decision_points') or workshop['decision_points'] == []:
|
||
decision_points = generate_decision_points(workshop)
|
||
workshop['decision_points'] = decision_points
|
||
|
||
# 显示结果页面
|
||
return render_template('results.html', workshop=workshop, workshop_id=workshop_id)
|
||
|
||
|
||
@app.route('/workshop/<int:workshop_id>/save_final_decision', methods=['POST'])
|
||
def save_final_decision(workshop_id):
|
||
"""
|
||
保存最终决策的路由
|
||
|
||
Args:
|
||
workshop_id (int): 工作坊 ID
|
||
|
||
Returns:
|
||
str: 重定向到结果页面
|
||
"""
|
||
# 检查工作坊是否存在
|
||
if workshop_id not in workshops:
|
||
return redirect(url_for('index'))
|
||
|
||
# 获取最终决策内容
|
||
final_decision = request.form.get('final_decision', '')
|
||
workshops[workshop_id]['final_decision'] = final_decision
|
||
workshops[workshop_id]['final_decision_time'] = datetime.now() # 最终决策保存时间
|
||
|
||
# 重定向到首页
|
||
return redirect(url_for('index'))
|
||
|
||
|
||
@app.route('/api/workshops')
|
||
def api_workshops():
|
||
"""
|
||
API路由,返回排序后的工作坊数据
|
||
|
||
Returns:
|
||
str: JSON格式的工作坊数据
|
||
"""
|
||
# 获取排序参数,默认为按创建时间排序
|
||
sort_by = request.args.get('sort_by', 'time')
|
||
|
||
# 对工作坊进行排序
|
||
sorted_workshops = []
|
||
if sort_by == 'name':
|
||
# 按名称首字母排序
|
||
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1]['name'])
|
||
else:
|
||
# 按创建时间排序
|
||
sorted_workshops = sorted(workshops.items(), key=lambda x: x[1].get('created_at', datetime.min), reverse=True)
|
||
|
||
# 转换为前端需要的格式
|
||
result = []
|
||
for workshop_id, workshop in sorted_workshops:
|
||
# 转换时间对象为字符串
|
||
workshop_data = workshop.copy()
|
||
if 'created_at' in workshop_data and workshop_data['created_at']:
|
||
workshop_data['created_at'] = workshop_data['created_at'].strftime('%Y-%m-%d %H:%M')
|
||
if 'final_decision_time' in workshop_data and workshop_data['final_decision_time']:
|
||
workshop_data['final_decision_time'] = workshop_data['final_decision_time'].strftime('%Y-%m-%d %H:%M')
|
||
workshop_data['id'] = workshop_id
|
||
result.append(workshop_data)
|
||
|
||
return jsonify(result)
|
||
|
||
|
||
@app.route('/api/workshop/<int:workshop_id>', methods=['DELETE'])
|
||
def delete_workshop(workshop_id):
|
||
"""
|
||
API路由,删除单个工作坊
|
||
|
||
Args:
|
||
workshop_id (int): 工作坊 ID
|
||
|
||
Returns:
|
||
str: JSON格式的删除结果
|
||
"""
|
||
if workshop_id in workshops:
|
||
del workshops[workshop_id]
|
||
return jsonify({'success': True, 'message': '工作坊删除成功'})
|
||
else:
|
||
return jsonify({'success': False, 'message': '工作坊不存在'}), 404
|
||
|
||
|
||
@app.route('/api/workshops/batch_delete', methods=['POST'])
|
||
def batch_delete_workshops():
|
||
"""
|
||
API路由,批量删除工作坊
|
||
|
||
Returns:
|
||
str: JSON格式的删除结果
|
||
"""
|
||
data = request.get_json()
|
||
workshop_ids = data.get('workshop_ids', [])
|
||
|
||
deleted_count = 0
|
||
for workshop_id in workshop_ids:
|
||
if workshop_id in workshops:
|
||
del workshops[workshop_id]
|
||
deleted_count += 1
|
||
|
||
return jsonify({'success': True, 'message': f'成功删除{deleted_count}个工作坊', 'deleted_count': deleted_count})
|
||
|
||
|
||
if __name__ == '__main__':
|
||
"""
|
||
应用入口点
|
||
|
||
启动 Flask 应用,开启调试模式
|
||
"""
|
||
app.run(debug=True) |