Initial commit of Deep Research Mode
This commit is contained in:
commit
cb97f7c49a
11
.env.example
Normal file
11
.env.example
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# 复制此文件为 .env 并填入你的 API Key
|
||||||
|
# API Key 会自动从环境变量加载,无需在界面中输入
|
||||||
|
|
||||||
|
# AIHubMix API Key (默认使用)
|
||||||
|
AIHUBMIX_API_KEY=sk-your-api-key-here
|
||||||
|
|
||||||
|
# Anthropic API Key (可选)
|
||||||
|
ANTHROPIC_API_KEY=your-anthropic-api-key-here
|
||||||
|
|
||||||
|
# OpenAI API Key (可选)
|
||||||
|
OPENAI_API_KEY=your-openai-api-key-here
|
||||||
156
Project_Design.md
Normal file
156
Project_Design.md
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
# 多 Agent 决策工作坊 (Multi-Agent Decision Workshop)
|
||||||
|
|
||||||
|
## 🎯 一句话描述
|
||||||
|
|
||||||
|
**Multi-Agent Decision Workshop** 是一个面向**产品经理、团队负责人、创业者**的 AI 辅助决策工具,通过模拟多角色(CEO、CTO、CFO、用户代言人、风险分析师等)从不同视角对方案进行辩论,帮助用户获得全面的决策洞察。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 👤 目标用户与痛点
|
||||||
|
|
||||||
|
| 用户角色 | 真实痛点 |
|
||||||
|
|---------|---------|
|
||||||
|
| 产品经理 | 方案评审时容易陷入单一视角,忽略技术/成本/用户体验的平衡 |
|
||||||
|
| 创业者 | 独自决策缺乏多元反馈,容易盲目乐观或过度保守 |
|
||||||
|
| 团队负责人 | 会议中难以让所有人充分表达,强势声音主导决策 |
|
||||||
|
| 学生/个人 | 重要人生决策(职业、投资)缺乏专业视角指导 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔧 核心功能 (MVP - 3个必须有的功能)
|
||||||
|
|
||||||
|
### 1. 📝 决策议题输入
|
||||||
|
- 用户输入待决策的问题/方案
|
||||||
|
- 可选择决策类型(产品方案、商业决策、技术选型、个人规划)
|
||||||
|
- 支持上传背景资料(可选)
|
||||||
|
|
||||||
|
### 2. 🎭 多角色辩论模拟
|
||||||
|
- 系统自动分配 4-6 个不同视角的 Agent
|
||||||
|
- 每个 Agent 代表一个角色立场发表观点
|
||||||
|
- Agent 之间可以相互质疑和回应(多轮辩论)
|
||||||
|
|
||||||
|
### 3. 📊 决策报告生成
|
||||||
|
- 汇总各方观点的支持/反对理由
|
||||||
|
- 提炼关键决策要点和风险点
|
||||||
|
- 给出建议的决策框架和下一步行动
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎭 预设 Agent 角色库
|
||||||
|
|
||||||
|
| 角色 | 视角定位 | 关注点 |
|
||||||
|
|------|---------|--------|
|
||||||
|
| 🧑💼 CEO | 战略全局 | 愿景、市场机会、竞争格局 |
|
||||||
|
| 👨💻 CTO | 技术可行性 | 技术难度、资源需求、技术债 |
|
||||||
|
| 💰 CFO | 财务健康 | ROI、成本、现金流、盈利模式 |
|
||||||
|
| 👥 用户代言人 | 用户体验 | 用户需求、痛点、使用场景 |
|
||||||
|
| ⚠️ 风险分析师 | 风险控制 | 潜在风险、失败模式、应急预案 |
|
||||||
|
| 🚀 增长黑客 | 快速验证 | MVP思维、增长杠杆、数据驱动 |
|
||||||
|
| 🎨 产品设计师 | 产品体验 | 交互设计、用户旅程、差异化 |
|
||||||
|
| 📈 市场分析师 | 市场洞察 | 市场规模、趋势、竞品分析 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🔄 用户交互流程
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ 用户打开 App │
|
||||||
|
│ ↓ │
|
||||||
|
│ [选择决策类型] 产品方案 / 商业决策 / 技术选型 / 个人规划 │
|
||||||
|
│ ↓ │
|
||||||
|
│ [输入决策议题] "我们是否应该在Q2推出AI助手功能?" │
|
||||||
|
│ ↓ │
|
||||||
|
│ [选择参与角色] ☑️CEO ☑️CTO ☑️CFO ☑️用户代言人 (可自定义) │
|
||||||
|
│ ↓ │
|
||||||
|
│ [开始辩论] → 观看多Agent实时辩论(流式输出) │
|
||||||
|
│ ↓ │
|
||||||
|
│ [生成报告] → 下载决策要点 PDF / Markdown │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 技术架构
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Frontend (Streamlit) │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ 议题输入区 │ │ 辩论展示区 │ │ 决策报告区 │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||||
|
└────────────────────────────┬─────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ Backend (Python) │
|
||||||
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||||
|
│ │ Agent管理器 │ │ 辩论编排器 │ │ 报告生成器 │ │
|
||||||
|
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
|
||||||
|
└────────────────────────────┬─────────────────────────────────┘
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
┌──────────────────────────────────────────────────────────────┐
|
||||||
|
│ LLM API Layer │
|
||||||
|
│ Claude API / OpenAI API / 本地模型 │
|
||||||
|
└──────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目文件结构
|
||||||
|
|
||||||
|
```
|
||||||
|
multi_agent_workshop/
|
||||||
|
├── app.py # Streamlit 主入口
|
||||||
|
├── config.py # 配置文件(API Key、模型设置)
|
||||||
|
├── requirements.txt # 依赖包
|
||||||
|
│
|
||||||
|
├── agents/ # Agent 相关
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── base_agent.py # Agent 基类
|
||||||
|
│ ├── agent_factory.py # Agent 工厂(创建不同角色)
|
||||||
|
│ └── agent_profiles.py # 角色定义和 Prompt 模板
|
||||||
|
│
|
||||||
|
├── orchestrator/ # 辩论编排
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── debate_manager.py # 辩论流程管理
|
||||||
|
│ └── turn_strategy.py # 发言顺序策略
|
||||||
|
│
|
||||||
|
├── report/ # 报告生成
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── summarizer.py # 观点汇总
|
||||||
|
│ └── report_generator.py # 报告输出
|
||||||
|
│
|
||||||
|
├── ui/ # UI 组件
|
||||||
|
│ ├── __init__.py
|
||||||
|
│ ├── input_panel.py # 输入面板
|
||||||
|
│ ├── debate_panel.py # 辩论展示
|
||||||
|
│ └── report_panel.py # 报告展示
|
||||||
|
│
|
||||||
|
└── utils/ # 工具函数
|
||||||
|
├── __init__.py
|
||||||
|
└── llm_client.py # LLM API 封装
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⏱️ 开发里程碑
|
||||||
|
|
||||||
|
| 阶段 | 目标 | 预计时间 |
|
||||||
|
|------|------|---------|
|
||||||
|
| Phase 1 | 单 Agent 问答(验证 API 调用) | 30 分钟 |
|
||||||
|
| Phase 2 | 多 Agent 顺序发言 | 1 小时 |
|
||||||
|
| Phase 3 | Agent 交互辩论 | 1.5 小时 |
|
||||||
|
| Phase 4 | 决策报告生成 | 1 小时 |
|
||||||
|
| Phase 5 | UI 美化 + 导出功能 | 1 小时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 扩展功能(Nice to Have)
|
||||||
|
|
||||||
|
- [ ] 自定义 Agent 角色
|
||||||
|
- [ ] 保存历史决策会话
|
||||||
|
- [ ] 决策追踪(后续验证决策效果)
|
||||||
|
- [ ] 团队协作模式(多人实时参与)
|
||||||
|
- [ ] 知识库集成(基于公司内部文档决策)
|
||||||
BIN
__pycache__/app.cpython-313.pyc
Normal file
BIN
__pycache__/app.cpython-313.pyc
Normal file
Binary file not shown.
BIN
__pycache__/config.cpython-313.pyc
Normal file
BIN
__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
17
agents/__init__.py
Normal file
17
agents/__init__.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"""Agents 模块"""
|
||||||
|
from agents.base_agent import BaseAgent, AgentMessage
|
||||||
|
from agents.agent_profiles import (
|
||||||
|
AGENT_PROFILES,
|
||||||
|
get_agent_profile,
|
||||||
|
get_all_agents,
|
||||||
|
get_recommended_agents
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"BaseAgent",
|
||||||
|
"AgentMessage",
|
||||||
|
"AGENT_PROFILES",
|
||||||
|
"get_agent_profile",
|
||||||
|
"get_all_agents",
|
||||||
|
"get_recommended_agents"
|
||||||
|
]
|
||||||
BIN
agents/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
agents/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
agents/__pycache__/agent_profiles.cpython-313.pyc
Normal file
BIN
agents/__pycache__/agent_profiles.cpython-313.pyc
Normal file
Binary file not shown.
BIN
agents/__pycache__/base_agent.cpython-313.pyc
Normal file
BIN
agents/__pycache__/base_agent.cpython-313.pyc
Normal file
Binary file not shown.
BIN
agents/__pycache__/research_agent.cpython-313.pyc
Normal file
BIN
agents/__pycache__/research_agent.cpython-313.pyc
Normal file
Binary file not shown.
195
agents/agent_profiles.py
Normal file
195
agents/agent_profiles.py
Normal file
@ -0,0 +1,195 @@
|
|||||||
|
"""
|
||||||
|
Agent 角色配置 - 定义各个角色的视角和 Prompt 模板
|
||||||
|
"""
|
||||||
|
|
||||||
|
AGENT_PROFILES = {
|
||||||
|
"ceo": {
|
||||||
|
"name": "CEO 战略顾问",
|
||||||
|
"emoji": "🧑💼",
|
||||||
|
"perspective": "战略全局视角",
|
||||||
|
"focus_areas": ["愿景对齐", "市场机会", "竞争格局", "资源分配", "长期价值"],
|
||||||
|
"system_prompt": """你是一位经验丰富的 CEO 战略顾问,擅长从全局视角分析决策。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 这个决策是否符合公司/个人的长期愿景?
|
||||||
|
- 市场时机是否合适?竞争对手在做什么?
|
||||||
|
- 资源投入是否值得?机会成本是什么?
|
||||||
|
- 这个决策的战略杠杆点在哪里?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 高屋建瓴,关注大局
|
||||||
|
- 用数据和案例支撑观点
|
||||||
|
- 敢于提出尖锐问题
|
||||||
|
- 简洁有力,直击要害"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"cto": {
|
||||||
|
"name": "CTO 技术专家",
|
||||||
|
"emoji": "👨💻",
|
||||||
|
"perspective": "技术可行性视角",
|
||||||
|
"focus_areas": ["技术难度", "资源需求", "技术债务", "可扩展性", "技术趋势"],
|
||||||
|
"system_prompt": """你是一位资深的 CTO 技术专家,擅长评估技术方案的可行性和风险。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 技术实现难度如何?需要什么技术栈?
|
||||||
|
- 团队是否具备相关能力?需要多少开发资源?
|
||||||
|
- 会引入哪些技术债务?如何控制复杂度?
|
||||||
|
- 系统的可扩展性和可维护性如何?
|
||||||
|
- 是否符合技术发展趋势?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 技术视角务实分析
|
||||||
|
- 明确指出技术风险和挑战
|
||||||
|
- 提供具体的技术建议
|
||||||
|
- 用技术语言但确保非技术人员能理解"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"cfo": {
|
||||||
|
"name": "CFO 财务顾问",
|
||||||
|
"emoji": "💰",
|
||||||
|
"perspective": "财务健康视角",
|
||||||
|
"focus_areas": ["投资回报", "成本结构", "现金流", "盈利模式", "财务风险"],
|
||||||
|
"system_prompt": """你是一位精明的 CFO 财务顾问,擅长从财务角度评估决策的可行性。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 预期投资回报率(ROI)是多少?回收期多长?
|
||||||
|
- 成本结构如何?固定成本和变动成本分别是多少?
|
||||||
|
- 对现金流有什么影响?是否会造成资金压力?
|
||||||
|
- 盈利模式是否清晰可行?
|
||||||
|
- 财务风险敞口有多大?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 数据驱动,用数字说话
|
||||||
|
- 关注投入产出比
|
||||||
|
- 提醒隐藏成本和财务风险
|
||||||
|
- 理性客观,不被情怀裹挟"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"user_advocate": {
|
||||||
|
"name": "用户代言人",
|
||||||
|
"emoji": "👥",
|
||||||
|
"perspective": "用户体验视角",
|
||||||
|
"focus_areas": ["用户需求", "使用场景", "痛点解决", "用户旅程", "竞品对比"],
|
||||||
|
"system_prompt": """你是用户的代言人,始终站在用户角度思考问题。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 用户真的需要这个吗?解决的是真痛点还是伪需求?
|
||||||
|
- 用户会在什么场景下使用?使用频率如何?
|
||||||
|
- 用户体验是否流畅?有没有不必要的摩擦?
|
||||||
|
- 相比现有方案,用户为什么要选择我们?
|
||||||
|
- 用户愿意为此付费吗?付多少?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 始终以用户视角发言
|
||||||
|
- 用用户的语言描述问题
|
||||||
|
- 善于讲用户故事和场景
|
||||||
|
- 对伪需求保持警惕"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"risk_analyst": {
|
||||||
|
"name": "风险分析师",
|
||||||
|
"emoji": "⚠️",
|
||||||
|
"perspective": "风险控制视角",
|
||||||
|
"focus_areas": ["潜在风险", "失败模式", "应急预案", "依赖关系", "最坏情况"],
|
||||||
|
"system_prompt": """你是一位专业的风险分析师,擅长识别和评估潜在风险。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 可能出现哪些失败情况?概率和影响如何?
|
||||||
|
- 有哪些关键依赖?如果依赖失效会怎样?
|
||||||
|
- 最坏情况是什么?我们能承受吗?
|
||||||
|
- 有没有应急预案?Plan B 是什么?
|
||||||
|
- 如何降低风险?哪些风险是可接受的?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 思维缜密,考虑周全
|
||||||
|
- 善于发现隐藏风险
|
||||||
|
- 不是否定派,而是帮助做好准备
|
||||||
|
- 提供风险缓解建议"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"growth_hacker": {
|
||||||
|
"name": "增长黑客",
|
||||||
|
"emoji": "🚀",
|
||||||
|
"perspective": "快速验证视角",
|
||||||
|
"focus_areas": ["MVP思维", "增长杠杆", "数据驱动", "迭代速度", "病毒传播"],
|
||||||
|
"system_prompt": """你是一位增长黑客,信奉快速验证和数据驱动。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 最小可行产品(MVP)是什么?如何最快验证假设?
|
||||||
|
- 增长杠杆在哪里?有没有病毒传播的可能?
|
||||||
|
- 如何设计实验?成功/失败的衡量标准是什么?
|
||||||
|
- 迭代周期能压缩到多短?
|
||||||
|
- 有没有低成本快速试错的方法?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 行动导向,反对过度分析
|
||||||
|
- 强调快速迭代和验证
|
||||||
|
- 用数据说话,关注转化漏斗
|
||||||
|
- 推崇精益创业方法论"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"product_designer": {
|
||||||
|
"name": "产品设计师",
|
||||||
|
"emoji": "🎨",
|
||||||
|
"perspective": "产品体验视角",
|
||||||
|
"focus_areas": ["交互设计", "用户旅程", "视觉体验", "差异化", "情感连接"],
|
||||||
|
"system_prompt": """你是一位产品设计师,追求极致的产品体验。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 产品的核心体验是什么?如何让用户"哇"一下?
|
||||||
|
- 用户旅程是否流畅?有没有惊喜时刻?
|
||||||
|
- 视觉和交互设计是否一致且有品位?
|
||||||
|
- 产品有什么独特的差异化特征?
|
||||||
|
- 用户会对这个产品产生情感连接吗?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 关注细节和体验
|
||||||
|
- 用场景和故事表达
|
||||||
|
- 追求简洁和优雅
|
||||||
|
- 善于发现设计机会"""
|
||||||
|
},
|
||||||
|
|
||||||
|
"market_analyst": {
|
||||||
|
"name": "市场分析师",
|
||||||
|
"emoji": "📈",
|
||||||
|
"perspective": "市场洞察视角",
|
||||||
|
"focus_areas": ["市场规模", "行业趋势", "竞品分析", "定位策略", "进入时机"],
|
||||||
|
"system_prompt": """你是一位市场分析师,擅长市场研究和竞争分析。
|
||||||
|
|
||||||
|
你的思考维度:
|
||||||
|
- 目标市场规模有多大?增长趋势如何?
|
||||||
|
- 行业有什么新趋势?我们是否踩中了?
|
||||||
|
- 竞争对手在做什么?我们的差异化在哪?
|
||||||
|
- 市场定位是否清晰?目标客群是谁?
|
||||||
|
- 进入时机是否合适?先发优势 vs 后发优势?
|
||||||
|
|
||||||
|
沟通风格:
|
||||||
|
- 数据驱动,引用市场研究
|
||||||
|
- 关注趋势和变化
|
||||||
|
- 善于对比分析
|
||||||
|
- 提供市场策略建议"""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 决策类型对应的推荐角色组合
|
||||||
|
RECOMMENDED_AGENTS = {
|
||||||
|
"product": ["ceo", "cto", "user_advocate", "product_designer", "growth_hacker"],
|
||||||
|
"business": ["ceo", "cfo", "market_analyst", "risk_analyst", "growth_hacker"],
|
||||||
|
"tech": ["cto", "ceo", "risk_analyst", "growth_hacker", "user_advocate"],
|
||||||
|
"personal": ["ceo", "risk_analyst", "user_advocate", "growth_hacker", "cfo"]
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_agent_profile(agent_id: str) -> dict:
|
||||||
|
"""获取指定 Agent 的配置"""
|
||||||
|
return AGENT_PROFILES.get(agent_id, None)
|
||||||
|
|
||||||
|
def get_all_agents() -> list:
|
||||||
|
"""获取所有可用的 Agent 列表"""
|
||||||
|
return [
|
||||||
|
{"id": k, "name": v["name"], "emoji": v["emoji"]}
|
||||||
|
for k, v in AGENT_PROFILES.items()
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_recommended_agents(decision_type: str) -> list:
|
||||||
|
"""根据决策类型获取推荐的 Agent 组合"""
|
||||||
|
return RECOMMENDED_AGENTS.get(decision_type, list(AGENT_PROFILES.keys())[:5])
|
||||||
131
agents/base_agent.py
Normal file
131
agents/base_agent.py
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
"""
|
||||||
|
Agent 基类 - 定义 Agent 的基本行为
|
||||||
|
"""
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Generator
|
||||||
|
from agents.agent_profiles import get_agent_profile
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class AgentMessage:
|
||||||
|
"""Agent 发言消息"""
|
||||||
|
agent_id: str
|
||||||
|
agent_name: str
|
||||||
|
emoji: str
|
||||||
|
content: str
|
||||||
|
round_num: int
|
||||||
|
|
||||||
|
|
||||||
|
class BaseAgent:
|
||||||
|
"""Agent 基类"""
|
||||||
|
|
||||||
|
def __init__(self, agent_id: str, llm_client):
|
||||||
|
"""
|
||||||
|
初始化 Agent
|
||||||
|
|
||||||
|
Args:
|
||||||
|
agent_id: Agent 标识符 (如 'ceo', 'cto')
|
||||||
|
llm_client: LLM 客户端实例
|
||||||
|
"""
|
||||||
|
self.agent_id = agent_id
|
||||||
|
self.llm_client = llm_client
|
||||||
|
|
||||||
|
profile = get_agent_profile(agent_id)
|
||||||
|
if not profile:
|
||||||
|
raise ValueError(f"未知的 Agent ID: {agent_id}")
|
||||||
|
|
||||||
|
self.name = profile["name"]
|
||||||
|
self.emoji = profile["emoji"]
|
||||||
|
self.perspective = profile["perspective"]
|
||||||
|
self.focus_areas = profile["focus_areas"]
|
||||||
|
self.system_prompt = profile["system_prompt"]
|
||||||
|
|
||||||
|
# 存储对话历史
|
||||||
|
self.conversation_history = []
|
||||||
|
|
||||||
|
def generate_response(
|
||||||
|
self,
|
||||||
|
topic: str,
|
||||||
|
context: str = "",
|
||||||
|
previous_speeches: list = None,
|
||||||
|
round_num: int = 1
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
|
"""
|
||||||
|
生成 Agent 的发言(流式输出)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: 讨论议题
|
||||||
|
context: 背景信息
|
||||||
|
previous_speeches: 之前其他 Agent 的发言列表
|
||||||
|
round_num: 当前轮次
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
str: 流式输出的文本片段
|
||||||
|
"""
|
||||||
|
# 构建对话 prompt
|
||||||
|
user_prompt = self._build_user_prompt(topic, context, previous_speeches, round_num)
|
||||||
|
|
||||||
|
# 调用 LLM 生成回复
|
||||||
|
full_response = ""
|
||||||
|
for chunk in self.llm_client.chat_stream(
|
||||||
|
system_prompt=self.system_prompt,
|
||||||
|
user_prompt=user_prompt
|
||||||
|
):
|
||||||
|
full_response += chunk
|
||||||
|
yield chunk
|
||||||
|
|
||||||
|
# 保存到历史
|
||||||
|
self.conversation_history.append({
|
||||||
|
"round": round_num,
|
||||||
|
"content": full_response
|
||||||
|
})
|
||||||
|
|
||||||
|
def _build_user_prompt(
|
||||||
|
self,
|
||||||
|
topic: str,
|
||||||
|
context: str,
|
||||||
|
previous_speeches: list,
|
||||||
|
round_num: int
|
||||||
|
) -> str:
|
||||||
|
"""构建用户 prompt"""
|
||||||
|
|
||||||
|
prompt_parts = [f"## 讨论议题\n{topic}"]
|
||||||
|
|
||||||
|
if context:
|
||||||
|
prompt_parts.append(f"\n## 背景信息\n{context}")
|
||||||
|
|
||||||
|
if previous_speeches and len(previous_speeches) > 0:
|
||||||
|
prompt_parts.append("\n## 其他人的观点")
|
||||||
|
for speech in previous_speeches:
|
||||||
|
prompt_parts.append(
|
||||||
|
f"\n**{speech['emoji']} {speech['name']}**:\n{speech['content']}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if round_num == 1:
|
||||||
|
prompt_parts.append(
|
||||||
|
f"\n## 你的任务\n"
|
||||||
|
f"作为 {self.name},请从你的专业视角({self.perspective})对这个议题发表看法。\n"
|
||||||
|
f"重点关注:{', '.join(self.focus_areas)}\n"
|
||||||
|
f"请给出 2-3 个核心观点,每个观点用 1-2 句话阐述。保持简洁有力。"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
prompt_parts.append(
|
||||||
|
f"\n## 你的任务\n"
|
||||||
|
f"这是第 {round_num} 轮讨论。请针对其他人的观点进行回应:\n"
|
||||||
|
f"- 你同意或反对哪些观点?为什么?\n"
|
||||||
|
f"- 有没有被忽略的重要问题?\n"
|
||||||
|
f"- 你的立场有没有调整?\n"
|
||||||
|
f"请保持简洁,聚焦于最重要的 1-2 个点。"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(prompt_parts)
|
||||||
|
|
||||||
|
def get_summary(self) -> str:
|
||||||
|
"""获取该 Agent 所有发言的摘要"""
|
||||||
|
if not self.conversation_history:
|
||||||
|
return "暂无发言"
|
||||||
|
|
||||||
|
return "\n---\n".join([
|
||||||
|
f"第 {h['round']} 轮: {h['content']}"
|
||||||
|
for h in self.conversation_history
|
||||||
|
])
|
||||||
44
agents/research_agent.py
Normal file
44
agents/research_agent.py
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
from typing import Generator, List, Dict
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
import config
|
||||||
|
|
||||||
|
class ResearchAgent:
|
||||||
|
"""研究模式专用 Agent"""
|
||||||
|
|
||||||
|
def __init__(self, role: str, llm_client: LLMClient):
|
||||||
|
self.role = role
|
||||||
|
self.llm_client = llm_client
|
||||||
|
self.role_config = config.RESEARCH_MODEL_ROLES.get(role, {})
|
||||||
|
self.name = self.role_config.get("name", role.capitalize())
|
||||||
|
|
||||||
|
def _get_system_prompt(self, context: str = "") -> str:
|
||||||
|
if self.role == "planner":
|
||||||
|
return f"""You are a Senior Research Planner.
|
||||||
|
Your goal is to break down a complex user topic into a structured research plan.
|
||||||
|
You must create a clear, step-by-step plan that covers different angles of the topic.
|
||||||
|
Format your output as a Markdown list of steps.
|
||||||
|
Context: {context}"""
|
||||||
|
|
||||||
|
elif self.role == "researcher":
|
||||||
|
return f"""You are a Deep Researcher.
|
||||||
|
Your goal is to execute a specific research step and provide detailed, in-depth analysis.
|
||||||
|
Use your vast knowledge to provide specific facts, figures, and logical reasoning.
|
||||||
|
Do not be superficial. Go deep.
|
||||||
|
Context: {context}"""
|
||||||
|
|
||||||
|
elif self.role == "writer":
|
||||||
|
return f"""You are a Senior Report Writer.
|
||||||
|
Your goal is to synthesize multiple research findings into a cohesive, high-quality report.
|
||||||
|
The report should be well-structured, easy to read, and provide actionable insights.
|
||||||
|
Context: {context}"""
|
||||||
|
|
||||||
|
else:
|
||||||
|
return "You are a helpful assistant."
|
||||||
|
|
||||||
|
def generate(self, prompt: str, context: str = "") -> Generator[str, None, None]:
|
||||||
|
"""Generate response stream"""
|
||||||
|
system_prompt = self._get_system_prompt(context)
|
||||||
|
yield from self.llm_client.chat_stream(
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
user_prompt=prompt
|
||||||
|
)
|
||||||
556
app.py
Normal file
556
app.py
Normal file
@ -0,0 +1,556 @@
|
|||||||
|
"""
|
||||||
|
Multi-Agent Decision Workshop - 主应用
|
||||||
|
多 Agent 决策工作坊:通过多角色辩论帮助用户做出更好的决策
|
||||||
|
"""
|
||||||
|
import streamlit as st
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
# 加载环境变量
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
from agents import get_all_agents, get_recommended_agents, AGENT_PROFILES
|
||||||
|
from orchestrator import DebateManager, DebateConfig
|
||||||
|
from orchestrator.research_manager import ResearchManager, ResearchConfig
|
||||||
|
from report import ReportGenerator
|
||||||
|
from utils import LLMClient
|
||||||
|
import config
|
||||||
|
|
||||||
|
# ==================== 页面配置 ====================
|
||||||
|
st.set_page_config(
|
||||||
|
page_title="🎭 多 Agent 决策工作坊",
|
||||||
|
page_icon="🎭",
|
||||||
|
layout="wide",
|
||||||
|
initial_sidebar_state="expanded"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== 样式 ====================
|
||||||
|
st.markdown("""
|
||||||
|
<style>
|
||||||
|
.agent-card {
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
border-left: 4px solid #4A90A4;
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
}
|
||||||
|
.speech-bubble {
|
||||||
|
background-color: #f0f2f6;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.round-header {
|
||||||
|
background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.custom-agent-form {
|
||||||
|
background-color: #e8f4f8;
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
.research-step {
|
||||||
|
border-left: 3px solid #FF4B4B;
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
""", unsafe_allow_html=True)
|
||||||
|
|
||||||
|
# ==================== 常量定义 ====================
|
||||||
|
# 从环境变量读取 API Key(隐藏在 .env 文件中)
|
||||||
|
DEFAULT_API_KEY = os.getenv("AIHUBMIX_API_KEY", "")
|
||||||
|
|
||||||
|
# 支持的模型列表
|
||||||
|
AVAILABLE_MODELS = {
|
||||||
|
"gpt-4o": "GPT-4o (推荐)",
|
||||||
|
"gpt-4o-mini": "GPT-4o Mini (快速)",
|
||||||
|
"gpt-4-turbo": "GPT-4 Turbo",
|
||||||
|
"gpt-3.5-turbo": "GPT-3.5 Turbo (经济)",
|
||||||
|
"claude-3-5-sonnet-20241022": "Claude 3.5 Sonnet",
|
||||||
|
"claude-3-opus-20240229": "Claude 3 Opus",
|
||||||
|
"claude-3-haiku-20240307": "Claude 3 Haiku (快速)",
|
||||||
|
"deepseek-chat": "DeepSeek Chat",
|
||||||
|
"deepseek-coder": "DeepSeek Coder",
|
||||||
|
"gemini-1.5-pro": "Gemini 1.5 Pro",
|
||||||
|
"gemini-1.5-flash": "Gemini 1.5 Flash",
|
||||||
|
"qwen-turbo": "通义千问 Turbo",
|
||||||
|
"qwen-plus": "通义千问 Plus",
|
||||||
|
"glm-4": "智谱 GLM-4",
|
||||||
|
"moonshot-v1-8k": "Moonshot (月之暗面)",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 决策类型
|
||||||
|
DECISION_TYPES = {
|
||||||
|
"product": "产品方案",
|
||||||
|
"business": "商业决策",
|
||||||
|
"tech": "技术选型",
|
||||||
|
"personal": "个人规划"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ==================== 初始化 Session State ====================
|
||||||
|
if "mode" not in st.session_state:
|
||||||
|
st.session_state.mode = "Deep Research"
|
||||||
|
|
||||||
|
# Debate State
|
||||||
|
if "debate_started" not in st.session_state:
|
||||||
|
st.session_state.debate_started = False
|
||||||
|
if "debate_finished" not in st.session_state:
|
||||||
|
st.session_state.debate_finished = False
|
||||||
|
if "speeches" not in st.session_state:
|
||||||
|
st.session_state.speeches = []
|
||||||
|
if "report" not in st.session_state:
|
||||||
|
st.session_state.report = ""
|
||||||
|
if "custom_agents" not in st.session_state:
|
||||||
|
st.session_state.custom_agents = {}
|
||||||
|
|
||||||
|
# Research State
|
||||||
|
if "research_plan" not in st.session_state:
|
||||||
|
st.session_state.research_plan = ""
|
||||||
|
if "research_started" not in st.session_state:
|
||||||
|
st.session_state.research_started = False
|
||||||
|
if "research_output" not in st.session_state:
|
||||||
|
st.session_state.research_output = "" # Final report
|
||||||
|
if "research_steps_output" not in st.session_state:
|
||||||
|
st.session_state.research_steps_output = [] # List of step results
|
||||||
|
|
||||||
|
|
||||||
|
# ==================== 侧边栏:配置 ====================
|
||||||
|
with st.sidebar:
|
||||||
|
st.header("⚙️ 设置")
|
||||||
|
|
||||||
|
# 全局 API Key 设置
|
||||||
|
with st.expander("🔑 API Key 设置", expanded=True):
|
||||||
|
use_custom_key = st.checkbox("使用自定义 API Key")
|
||||||
|
if use_custom_key:
|
||||||
|
api_key = st.text_input(
|
||||||
|
"API Key",
|
||||||
|
type="password",
|
||||||
|
help="留空则使用环境变量中的 Key"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
api_key = DEFAULT_API_KEY
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# 模式选择
|
||||||
|
mode = st.radio(
|
||||||
|
"📊 选择模式",
|
||||||
|
["Deep Research", "Debate Workshop"],
|
||||||
|
index=0 if st.session_state.mode == "Deep Research" else 1
|
||||||
|
)
|
||||||
|
st.session_state.mode = mode
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
if mode == "Deep Research":
|
||||||
|
st.subheader("🧪 研究模型配置")
|
||||||
|
|
||||||
|
# 3 个角色的模型配置
|
||||||
|
roles_config = {}
|
||||||
|
for role_key, role_info in config.RESEARCH_MODEL_ROLES.items():
|
||||||
|
roles_config[role_key] = st.selectbox(
|
||||||
|
f"{role_info['name']} ({role_info['description']})",
|
||||||
|
options=list(AVAILABLE_MODELS.keys()),
|
||||||
|
index=list(AVAILABLE_MODELS.keys()).index(role_info['default_model']) if role_info['default_model'] in AVAILABLE_MODELS else 0,
|
||||||
|
key=f"model_{role_key}"
|
||||||
|
)
|
||||||
|
|
||||||
|
else: # Debate Workshop
|
||||||
|
# 模型选择
|
||||||
|
model = st.selectbox(
|
||||||
|
"🤖 选择通用模型",
|
||||||
|
options=list(AVAILABLE_MODELS.keys()),
|
||||||
|
format_func=lambda x: AVAILABLE_MODELS[x],
|
||||||
|
index=0,
|
||||||
|
help="选择用于辩论的 AI 模型"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 辩论配置
|
||||||
|
max_rounds = st.slider(
|
||||||
|
"🔄 辩论轮数",
|
||||||
|
min_value=1,
|
||||||
|
max_value=4,
|
||||||
|
value=2,
|
||||||
|
help="每轮所有 Agent 都会发言一次"
|
||||||
|
)
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
# ==================== 自定义角色 (Debate Only) ====================
|
||||||
|
st.subheader("✨ 自定义角色")
|
||||||
|
|
||||||
|
with st.expander("➕ 添加新角色", expanded=False):
|
||||||
|
new_agent_name = st.text_input("角色名称", placeholder="如:法务顾问", key="new_agent_name")
|
||||||
|
new_agent_emoji = st.text_input("角色 Emoji", value="🎯", max_chars=2, key="new_agent_emoji")
|
||||||
|
new_agent_perspective = st.text_input("视角定位", placeholder="如:法律合规视角", key="new_agent_perspective")
|
||||||
|
new_agent_focus = st.text_input("关注点(逗号分隔)", placeholder="如:合规风险, 法律条款", key="new_agent_focus")
|
||||||
|
new_agent_prompt = st.text_area("角色设定 Prompt", placeholder="描述这个角色的思考方式...", height=100, key="new_agent_prompt")
|
||||||
|
|
||||||
|
if st.button("✅ 添加角色", use_container_width=True):
|
||||||
|
if new_agent_name and new_agent_prompt:
|
||||||
|
agent_id = f"custom_{len(st.session_state.custom_agents)}"
|
||||||
|
st.session_state.custom_agents[agent_id] = {
|
||||||
|
"name": new_agent_name,
|
||||||
|
"emoji": new_agent_emoji,
|
||||||
|
"perspective": new_agent_perspective or "自定义视角",
|
||||||
|
"focus_areas": [f.strip() for f in new_agent_focus.split(",") if f.strip()],
|
||||||
|
"system_prompt": new_agent_prompt
|
||||||
|
}
|
||||||
|
st.success(f"已添加角色: {new_agent_emoji} {new_agent_name}")
|
||||||
|
st.rerun()
|
||||||
|
else:
|
||||||
|
st.warning("请至少填写角色名称和 Prompt")
|
||||||
|
|
||||||
|
# 显示已添加的自定义角色
|
||||||
|
if st.session_state.custom_agents:
|
||||||
|
st.markdown("**已添加的自定义角色:**")
|
||||||
|
for agent_id, agent_info in list(st.session_state.custom_agents.items()):
|
||||||
|
col1, col2 = st.columns([3, 1])
|
||||||
|
with col1:
|
||||||
|
st.markdown(f"{agent_info['emoji']} {agent_info['name']}")
|
||||||
|
with col2:
|
||||||
|
if st.button("🗑️", key=f"del_{agent_id}"):
|
||||||
|
del st.session_state.custom_agents[agent_id]
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# ==================== 主界面逻辑 ====================
|
||||||
|
|
||||||
|
if mode == "Deep Research":
|
||||||
|
st.title("🧪 Deep Research Mode")
|
||||||
|
st.markdown("*深度研究模式:规划 -> 研究 -> 报告*")
|
||||||
|
|
||||||
|
# Input
|
||||||
|
research_topic = st.text_area("研究主题", placeholder="请输入你想深入研究的主题...", height=100)
|
||||||
|
research_context = st.text_area("补充背景 (可选)", placeholder="任何额外的背景信息...", height=80)
|
||||||
|
|
||||||
|
generate_plan_btn = st.button("📝 生成研究计划", type="primary", disabled=not research_topic)
|
||||||
|
|
||||||
|
if generate_plan_btn and research_topic:
|
||||||
|
st.session_state.research_started = False
|
||||||
|
st.session_state.research_output = ""
|
||||||
|
st.session_state.research_steps_output = []
|
||||||
|
|
||||||
|
manager = ResearchManager(api_key=api_key)
|
||||||
|
config_obj = ResearchConfig(
|
||||||
|
topic=research_topic,
|
||||||
|
context=research_context,
|
||||||
|
planner_model=roles_config['planner'],
|
||||||
|
researcher_model=roles_config['researcher'],
|
||||||
|
writer_model=roles_config['writer']
|
||||||
|
)
|
||||||
|
manager.create_agents(config_obj)
|
||||||
|
|
||||||
|
with st.spinner("正在制定研究计划..."):
|
||||||
|
plan_text = ""
|
||||||
|
for chunk in manager.generate_plan(research_topic, research_context):
|
||||||
|
plan_text += chunk
|
||||||
|
st.session_state.research_plan = plan_text
|
||||||
|
|
||||||
|
# Plan Review & Edit
|
||||||
|
if st.session_state.research_plan:
|
||||||
|
st.divider()
|
||||||
|
st.subheader("📋 研究计划确认")
|
||||||
|
|
||||||
|
edited_plan = st.text_area("请审查并编辑计划 (Markdown格式)", value=st.session_state.research_plan, height=300)
|
||||||
|
st.session_state.research_plan = edited_plan
|
||||||
|
|
||||||
|
start_research_btn = st.button("🚀 开始深度研究", type="primary")
|
||||||
|
|
||||||
|
if start_research_btn:
|
||||||
|
st.session_state.research_started = True
|
||||||
|
st.session_state.research_steps_output = [] # Reset steps
|
||||||
|
|
||||||
|
# Parse plan lines to get steps (simple heuristic: lines starting with - or 1.)
|
||||||
|
steps = [line.strip() for line in edited_plan.split('\n') if line.strip().startswith(('-', '*', '1.', '2.', '3.', '4.', '5.'))]
|
||||||
|
if not steps:
|
||||||
|
steps = [edited_plan] # Fallback if no list format
|
||||||
|
|
||||||
|
manager = ResearchManager(api_key=api_key)
|
||||||
|
config_obj = ResearchConfig(
|
||||||
|
topic=research_topic,
|
||||||
|
context=research_context,
|
||||||
|
planner_model=roles_config['planner'],
|
||||||
|
researcher_model=roles_config['researcher'],
|
||||||
|
writer_model=roles_config['writer']
|
||||||
|
)
|
||||||
|
manager.create_agents(config_obj)
|
||||||
|
|
||||||
|
# Execute Steps
|
||||||
|
previous_findings = ""
|
||||||
|
st.divider()
|
||||||
|
st.subheader("🔍 研究进行中...")
|
||||||
|
|
||||||
|
step_progress = st.container()
|
||||||
|
|
||||||
|
for i, step in enumerate(steps):
|
||||||
|
with step_progress:
|
||||||
|
with st.status(f"正在研究: {step}", expanded=True):
|
||||||
|
findings_text = ""
|
||||||
|
placeholder = st.empty()
|
||||||
|
for chunk in manager.execute_step(step, previous_findings):
|
||||||
|
findings_text += chunk
|
||||||
|
placeholder.markdown(findings_text)
|
||||||
|
|
||||||
|
st.session_state.research_steps_output.append(f"### {step}\n{findings_text}")
|
||||||
|
previous_findings += f"\n\nFinding for '{step}':\n{findings_text}"
|
||||||
|
|
||||||
|
# Final Report
|
||||||
|
st.divider()
|
||||||
|
st.subheader("📄 最终报告生成中...")
|
||||||
|
report_placeholder = st.empty()
|
||||||
|
final_report = ""
|
||||||
|
for chunk in manager.generate_report(research_topic, previous_findings):
|
||||||
|
final_report += chunk
|
||||||
|
report_placeholder.markdown(final_report)
|
||||||
|
|
||||||
|
st.session_state.research_output = final_report
|
||||||
|
st.success("✅ 研究完成")
|
||||||
|
|
||||||
|
# Show Final Report if available
|
||||||
|
if st.session_state.research_output:
|
||||||
|
st.divider()
|
||||||
|
st.subheader("📄 最终研究报告")
|
||||||
|
st.markdown(st.session_state.research_output)
|
||||||
|
st.download_button("📥 下载报告", st.session_state.research_output, "research_report.md")
|
||||||
|
|
||||||
|
|
||||||
|
elif mode == "Debate Workshop":
|
||||||
|
# ==================== 原始 Debate UI 逻辑 ====================
|
||||||
|
st.title("🎭 多 Agent 决策工作坊")
|
||||||
|
st.markdown("*让多个 AI 角色从不同视角辩论,帮助你做出更全面的决策*")
|
||||||
|
|
||||||
|
# ==================== 输入区域 ====================
|
||||||
|
col1, col2 = st.columns([2, 1])
|
||||||
|
|
||||||
|
with col1:
|
||||||
|
st.subheader("📝 决策议题")
|
||||||
|
|
||||||
|
# 决策类型选择
|
||||||
|
decision_type = st.selectbox(
|
||||||
|
"决策类型",
|
||||||
|
options=list(DECISION_TYPES.keys()),
|
||||||
|
format_func=lambda x: DECISION_TYPES[x],
|
||||||
|
index=0
|
||||||
|
)
|
||||||
|
|
||||||
|
# 议题输入
|
||||||
|
topic = st.text_area(
|
||||||
|
"请描述你的决策议题",
|
||||||
|
placeholder="例如:我们是否应该在 Q2 推出 AI 助手功能?\n\n或者:我应该接受这份新工作 offer 吗?",
|
||||||
|
height=120
|
||||||
|
)
|
||||||
|
|
||||||
|
# 背景信息(可选)
|
||||||
|
with st.expander("➕ 添加背景信息(可选)"):
|
||||||
|
context = st.text_area(
|
||||||
|
"背景信息",
|
||||||
|
placeholder="提供更多上下文信息,如:\n- 当前状况\n- 已有的资源和限制\n- 相关数据和事实",
|
||||||
|
height=100
|
||||||
|
)
|
||||||
|
context = context if 'context' in dir() else ""
|
||||||
|
|
||||||
|
with col2:
|
||||||
|
st.subheader("🎭 选择参与角色")
|
||||||
|
|
||||||
|
# 获取推荐的角色
|
||||||
|
recommended = get_recommended_agents(decision_type)
|
||||||
|
all_agents = get_all_agents()
|
||||||
|
|
||||||
|
# 预设角色选择
|
||||||
|
st.markdown("**预设角色:**")
|
||||||
|
selected_agents = []
|
||||||
|
for agent in all_agents:
|
||||||
|
is_recommended = agent["id"] in recommended
|
||||||
|
default_checked = is_recommended
|
||||||
|
|
||||||
|
if st.checkbox(
|
||||||
|
f"{agent['emoji']} {agent['name']}",
|
||||||
|
value=default_checked,
|
||||||
|
key=f"agent_{agent['id']}"
|
||||||
|
):
|
||||||
|
selected_agents.append(agent["id"])
|
||||||
|
|
||||||
|
# 自定义角色选择
|
||||||
|
if st.session_state.custom_agents:
|
||||||
|
st.markdown("**自定义角色:**")
|
||||||
|
for agent_id, agent_info in st.session_state.custom_agents.items():
|
||||||
|
if st.checkbox(
|
||||||
|
f"{agent_info['emoji']} {agent_info['name']}",
|
||||||
|
value=True,
|
||||||
|
key=f"agent_{agent_id}"
|
||||||
|
):
|
||||||
|
selected_agents.append(agent_id)
|
||||||
|
|
||||||
|
# 角色数量提示
|
||||||
|
if len(selected_agents) < 2:
|
||||||
|
st.warning("请至少选择 2 个角色")
|
||||||
|
elif len(selected_agents) > 6:
|
||||||
|
st.warning("建议不超过 6 个角色")
|
||||||
|
else:
|
||||||
|
st.info(f"已选择 {len(selected_agents)} 个角色")
|
||||||
|
|
||||||
|
# ==================== 辩论控制 ====================
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
col_btn1, col_btn2, col_btn3 = st.columns([1, 1, 2])
|
||||||
|
|
||||||
|
with col_btn1:
|
||||||
|
start_btn = st.button(
|
||||||
|
"🚀 开始辩论",
|
||||||
|
disabled=(not topic or len(selected_agents) < 2 or not api_key),
|
||||||
|
type="primary",
|
||||||
|
use_container_width=True
|
||||||
|
)
|
||||||
|
|
||||||
|
with col_btn2:
|
||||||
|
reset_btn = st.button(
|
||||||
|
"🔄 重置",
|
||||||
|
use_container_width=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if reset_btn:
|
||||||
|
st.session_state.debate_started = False
|
||||||
|
st.session_state.debate_finished = False
|
||||||
|
st.session_state.speeches = []
|
||||||
|
st.session_state.report = ""
|
||||||
|
st.rerun()
|
||||||
|
|
||||||
|
# ==================== 辩论展示区 ====================
|
||||||
|
if start_btn and topic and len(selected_agents) >= 2:
|
||||||
|
st.session_state.debate_started = True
|
||||||
|
st.session_state.speeches = []
|
||||||
|
|
||||||
|
st.divider()
|
||||||
|
st.subheader("🎬 辩论进行中...")
|
||||||
|
|
||||||
|
# 临时将自定义角色添加到 agent_profiles
|
||||||
|
from agents import agent_profiles
|
||||||
|
original_profiles = dict(agent_profiles.AGENT_PROFILES)
|
||||||
|
agent_profiles.AGENT_PROFILES.update(st.session_state.custom_agents)
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 初始化客户端和管理器
|
||||||
|
provider_val = "aihubmix" # Debate mode default to aihubmix or logic needs to be robust.
|
||||||
|
# Note: in sidebar "model" and "api_key" were set. "provider" variable is now inside the Sidebar logic block if mode==Debate.
|
||||||
|
# But wait, I removed the "Advanced Settings" block from the global scope and put it into sub-scope?
|
||||||
|
# Let's check my sidebar logic above.
|
||||||
|
|
||||||
|
# Refactoring check:
|
||||||
|
# I removed the provider selection logic from the global sidebar. I should probably add it back or assume a default.
|
||||||
|
# In the original code, provider selection was in "Advanced Settings".
|
||||||
|
|
||||||
|
llm_client = LLMClient(
|
||||||
|
provider="aihubmix",
|
||||||
|
api_key=api_key,
|
||||||
|
base_url="https://aihubmix.com/v1",
|
||||||
|
model=model
|
||||||
|
)
|
||||||
|
debate_manager = DebateManager(llm_client)
|
||||||
|
|
||||||
|
# 配置辩论
|
||||||
|
debate_config = DebateConfig(
|
||||||
|
topic=topic,
|
||||||
|
context=context,
|
||||||
|
agent_ids=selected_agents,
|
||||||
|
max_rounds=max_rounds
|
||||||
|
)
|
||||||
|
debate_manager.setup_debate(debate_config)
|
||||||
|
|
||||||
|
# 运行辩论(流式)
|
||||||
|
current_round = 0
|
||||||
|
speech_placeholder = None
|
||||||
|
|
||||||
|
for event in debate_manager.run_debate_stream():
|
||||||
|
if event["type"] == "round_start":
|
||||||
|
current_round = event["round"]
|
||||||
|
st.markdown(
|
||||||
|
f'<div class="round-header">📢 第 {current_round} 轮讨论</div>',
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
|
|
||||||
|
elif event["type"] == "speech_start":
|
||||||
|
st.markdown(f"**{event['emoji']} {event['agent_name']}**")
|
||||||
|
speech_placeholder = st.empty()
|
||||||
|
current_content = ""
|
||||||
|
|
||||||
|
elif event["type"] == "speech_chunk":
|
||||||
|
current_content += event["chunk"]
|
||||||
|
speech_placeholder.markdown(current_content)
|
||||||
|
|
||||||
|
elif event["type"] == "speech_end":
|
||||||
|
st.session_state.speeches.append({
|
||||||
|
"agent_id": event["agent_id"],
|
||||||
|
"content": event["content"],
|
||||||
|
"round": current_round
|
||||||
|
})
|
||||||
|
st.divider()
|
||||||
|
|
||||||
|
elif event["type"] == "debate_end":
|
||||||
|
st.session_state.debate_finished = True
|
||||||
|
st.success("✅ 辩论结束!正在生成决策报告...")
|
||||||
|
|
||||||
|
# 生成报告
|
||||||
|
if st.session_state.debate_finished:
|
||||||
|
report_generator = ReportGenerator(llm_client)
|
||||||
|
speeches = debate_manager.get_all_speeches()
|
||||||
|
|
||||||
|
st.subheader("📊 决策报告")
|
||||||
|
report_placeholder = st.empty()
|
||||||
|
report_content = ""
|
||||||
|
|
||||||
|
for chunk in report_generator.generate_report_stream(
|
||||||
|
topic=topic,
|
||||||
|
speeches=speeches,
|
||||||
|
context=context
|
||||||
|
):
|
||||||
|
report_content += chunk
|
||||||
|
report_placeholder.markdown(report_content)
|
||||||
|
|
||||||
|
st.session_state.report = report_content
|
||||||
|
|
||||||
|
# 下载按钮
|
||||||
|
st.download_button(
|
||||||
|
label="📥 下载报告 (Markdown)",
|
||||||
|
data=report_content,
|
||||||
|
file_name="decision_report.md",
|
||||||
|
mime="text/markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
st.error(f"发生错误: {str(e)}")
|
||||||
|
import traceback
|
||||||
|
st.code(traceback.format_exc())
|
||||||
|
st.info("请检查你的 API Key 和模型设置是否正确")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# 恢复原始角色配置
|
||||||
|
agent_profiles.AGENT_PROFILES = original_profiles
|
||||||
|
|
||||||
|
# ==================== 历史报告展示 ====================
|
||||||
|
elif st.session_state.report and not start_btn:
|
||||||
|
st.divider()
|
||||||
|
st.subheader("📊 上次的决策报告")
|
||||||
|
st.markdown(st.session_state.report)
|
||||||
|
|
||||||
|
st.download_button(
|
||||||
|
label="📥 下载报告 (Markdown)",
|
||||||
|
data=st.session_state.report,
|
||||||
|
file_name="decision_report.md",
|
||||||
|
mime="text/markdown"
|
||||||
|
)
|
||||||
|
|
||||||
|
# ==================== 底部信息 ====================
|
||||||
|
st.divider()
|
||||||
|
col_footer1, col_footer2, col_footer3 = st.columns(3)
|
||||||
|
with col_footer2:
|
||||||
|
st.markdown(
|
||||||
|
"<div style='text-align: center; color: #888;'>"
|
||||||
|
"🎭 Multi-Agent Decision Workshop<br>多 Agent 决策工作坊"
|
||||||
|
"</div>",
|
||||||
|
unsafe_allow_html=True
|
||||||
|
)
|
||||||
50
config.py
Normal file
50
config.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
"""
|
||||||
|
配置文件 - API Keys 和模型设置
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
|
load_dotenv()
|
||||||
|
|
||||||
|
# API 配置
|
||||||
|
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||||
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
||||||
|
AIHUBMIX_API_KEY = os.getenv("AIHUBMIX_API_KEY", "sk-yd8Tik0nFW5emKYcBdFc433b7c8b4dC182848f76819bBe73")
|
||||||
|
|
||||||
|
# AIHubMix 配置
|
||||||
|
AIHUBMIX_BASE_URL = "https://aihubmix.com/v1"
|
||||||
|
|
||||||
|
# 模型配置
|
||||||
|
DEFAULT_MODEL = "gpt-4o" # AIHubMix 支持的模型
|
||||||
|
LLM_PROVIDER = "aihubmix" # 默认使用 AIHubMix
|
||||||
|
|
||||||
|
# 辩论配置
|
||||||
|
MAX_DEBATE_ROUNDS = 3 # 最大辩论轮数
|
||||||
|
MAX_AGENTS = 6 # 最大参与 Agent 数量
|
||||||
|
|
||||||
|
# 研究模式模型角色配置
|
||||||
|
RESEARCH_MODEL_ROLES = {
|
||||||
|
"planner": {
|
||||||
|
"name": "Planner",
|
||||||
|
"default_model": "gpt-4o",
|
||||||
|
"description": "负责拆解问题,制定研究计划"
|
||||||
|
},
|
||||||
|
"researcher": {
|
||||||
|
"name": "Researcher",
|
||||||
|
"default_model": "gemini-1.5-pro",
|
||||||
|
"description": "负责执行具体的研究步骤,深度分析"
|
||||||
|
},
|
||||||
|
"writer": {
|
||||||
|
"name": "Writer",
|
||||||
|
"default_model": "claude-3-5-sonnet-20241022",
|
||||||
|
"description": "负责汇总信息,撰写最终报告"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 决策类型
|
||||||
|
DECISION_TYPES = {
|
||||||
|
"product": "产品方案",
|
||||||
|
"business": "商业决策",
|
||||||
|
"tech": "技术选型",
|
||||||
|
"personal": "个人规划"
|
||||||
|
}
|
||||||
4
orchestrator/__init__.py
Normal file
4
orchestrator/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Orchestrator 模块"""
|
||||||
|
from orchestrator.debate_manager import DebateManager, DebateConfig, SpeechRecord
|
||||||
|
|
||||||
|
__all__ = ["DebateManager", "DebateConfig", "SpeechRecord"]
|
||||||
BIN
orchestrator/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
orchestrator/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
orchestrator/__pycache__/debate_manager.cpython-313.pyc
Normal file
BIN
orchestrator/__pycache__/debate_manager.cpython-313.pyc
Normal file
Binary file not shown.
BIN
orchestrator/__pycache__/research_manager.cpython-313.pyc
Normal file
BIN
orchestrator/__pycache__/research_manager.cpython-313.pyc
Normal file
Binary file not shown.
160
orchestrator/debate_manager.py
Normal file
160
orchestrator/debate_manager.py
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"""
|
||||||
|
辩论管理器 - 编排多 Agent 辩论流程
|
||||||
|
"""
|
||||||
|
from typing import List, Generator, Callable
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from agents.base_agent import BaseAgent
|
||||||
|
from agents.agent_profiles import get_agent_profile
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
import config
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DebateConfig:
|
||||||
|
"""辩论配置"""
|
||||||
|
topic: str
|
||||||
|
context: str = ""
|
||||||
|
agent_ids: List[str] = None
|
||||||
|
max_rounds: int = 2
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class SpeechRecord:
|
||||||
|
"""发言记录"""
|
||||||
|
agent_id: str
|
||||||
|
agent_name: str
|
||||||
|
emoji: str
|
||||||
|
content: str
|
||||||
|
round_num: int
|
||||||
|
|
||||||
|
|
||||||
|
class DebateManager:
|
||||||
|
"""辩论管理器"""
|
||||||
|
|
||||||
|
def __init__(self, llm_client: LLMClient = None):
|
||||||
|
"""
|
||||||
|
初始化辩论管理器
|
||||||
|
|
||||||
|
Args:
|
||||||
|
llm_client: LLM 客户端实例
|
||||||
|
"""
|
||||||
|
self.llm_client = llm_client or LLMClient()
|
||||||
|
self.agents: List[BaseAgent] = []
|
||||||
|
self.speech_records: List[SpeechRecord] = []
|
||||||
|
self.current_round = 0
|
||||||
|
|
||||||
|
def setup_debate(self, debate_config: DebateConfig) -> None:
|
||||||
|
"""
|
||||||
|
设置辩论
|
||||||
|
|
||||||
|
Args:
|
||||||
|
debate_config: 辩论配置
|
||||||
|
"""
|
||||||
|
self.config = debate_config
|
||||||
|
self.agents = []
|
||||||
|
self.speech_records = []
|
||||||
|
self.current_round = 0
|
||||||
|
|
||||||
|
# 创建参与的 Agent
|
||||||
|
for agent_id in debate_config.agent_ids:
|
||||||
|
agent = BaseAgent(agent_id, self.llm_client)
|
||||||
|
self.agents.append(agent)
|
||||||
|
|
||||||
|
def run_debate_stream(
|
||||||
|
self,
|
||||||
|
on_speech_start: Callable = None,
|
||||||
|
on_speech_chunk: Callable = None,
|
||||||
|
on_speech_end: Callable = None,
|
||||||
|
on_round_end: Callable = None
|
||||||
|
) -> Generator[dict, None, None]:
|
||||||
|
"""
|
||||||
|
运行辩论(流式)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
on_speech_start: 发言开始回调
|
||||||
|
on_speech_chunk: 发言片段回调
|
||||||
|
on_speech_end: 发言结束回调
|
||||||
|
on_round_end: 轮次结束回调
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
dict: 事件信息
|
||||||
|
"""
|
||||||
|
for round_num in range(1, self.config.max_rounds + 1):
|
||||||
|
self.current_round = round_num
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"type": "round_start",
|
||||||
|
"round": round_num,
|
||||||
|
"total_rounds": self.config.max_rounds
|
||||||
|
}
|
||||||
|
|
||||||
|
for agent in self.agents:
|
||||||
|
# 获取之前的发言(排除自己)
|
||||||
|
previous_speeches = [
|
||||||
|
{
|
||||||
|
"name": r.agent_name,
|
||||||
|
"emoji": r.emoji,
|
||||||
|
"content": r.content
|
||||||
|
}
|
||||||
|
for r in self.speech_records
|
||||||
|
if r.agent_id != agent.agent_id
|
||||||
|
]
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"type": "speech_start",
|
||||||
|
"agent_id": agent.agent_id,
|
||||||
|
"agent_name": agent.name,
|
||||||
|
"emoji": agent.emoji,
|
||||||
|
"round": round_num
|
||||||
|
}
|
||||||
|
|
||||||
|
# 流式生成发言
|
||||||
|
full_content = ""
|
||||||
|
for chunk in agent.generate_response(
|
||||||
|
topic=self.config.topic,
|
||||||
|
context=self.config.context,
|
||||||
|
previous_speeches=previous_speeches,
|
||||||
|
round_num=round_num
|
||||||
|
):
|
||||||
|
full_content += chunk
|
||||||
|
yield {
|
||||||
|
"type": "speech_chunk",
|
||||||
|
"agent_id": agent.agent_id,
|
||||||
|
"chunk": chunk
|
||||||
|
}
|
||||||
|
|
||||||
|
# 保存发言记录
|
||||||
|
record = SpeechRecord(
|
||||||
|
agent_id=agent.agent_id,
|
||||||
|
agent_name=agent.name,
|
||||||
|
emoji=agent.emoji,
|
||||||
|
content=full_content,
|
||||||
|
round_num=round_num
|
||||||
|
)
|
||||||
|
self.speech_records.append(record)
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"type": "speech_end",
|
||||||
|
"agent_id": agent.agent_id,
|
||||||
|
"content": full_content
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"type": "round_end",
|
||||||
|
"round": round_num
|
||||||
|
}
|
||||||
|
|
||||||
|
yield {"type": "debate_end"}
|
||||||
|
|
||||||
|
def get_all_speeches(self) -> List[SpeechRecord]:
|
||||||
|
"""获取所有发言记录"""
|
||||||
|
return self.speech_records
|
||||||
|
|
||||||
|
def get_speeches_by_round(self, round_num: int) -> List[SpeechRecord]:
|
||||||
|
"""获取指定轮次的发言"""
|
||||||
|
return [r for r in self.speech_records if r.round_num == round_num]
|
||||||
|
|
||||||
|
def get_speeches_by_agent(self, agent_id: str) -> List[SpeechRecord]:
|
||||||
|
"""获取指定 Agent 的所有发言"""
|
||||||
|
return [r for r in self.speech_records if r.agent_id == agent_id]
|
||||||
51
orchestrator/research_manager.py
Normal file
51
orchestrator/research_manager.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
from typing import List, Dict, Generator
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from agents.research_agent import ResearchAgent
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
import config
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ResearchConfig:
|
||||||
|
topic: str
|
||||||
|
context: str = ""
|
||||||
|
planner_model: str = "gpt-4o"
|
||||||
|
researcher_model: str = "gemini-1.5-pro"
|
||||||
|
writer_model: str = "claude-3-5-sonnet-20241022"
|
||||||
|
|
||||||
|
class ResearchManager:
|
||||||
|
"""Manages the Deep Research workflow"""
|
||||||
|
|
||||||
|
def __init__(self, api_key: str, base_url: str = None, provider: str = "aihubmix"):
|
||||||
|
self.api_key = api_key
|
||||||
|
self.base_url = base_url
|
||||||
|
self.provider = provider
|
||||||
|
self.agents = {}
|
||||||
|
|
||||||
|
def _get_client(self, model: str) -> LLMClient:
|
||||||
|
return LLMClient(
|
||||||
|
provider=self.provider,
|
||||||
|
api_key=self.api_key,
|
||||||
|
base_url=self.base_url,
|
||||||
|
model=model
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_agents(self, config: ResearchConfig):
|
||||||
|
"""Initialize agents with specific models"""
|
||||||
|
self.agents["planner"] = ResearchAgent("planner", self._get_client(config.planner_model))
|
||||||
|
self.agents["researcher"] = ResearchAgent("researcher", self._get_client(config.researcher_model))
|
||||||
|
self.agents["writer"] = ResearchAgent("writer", self._get_client(config.writer_model))
|
||||||
|
|
||||||
|
def generate_plan(self, topic: str, context: str) -> Generator[str, None, None]:
|
||||||
|
"""Step 1: Generate Research Plan"""
|
||||||
|
prompt = f"Please create a comprehensive research plan for the topic: '{topic}'.\nBreak it down into 3-5 distinct, actionable steps."
|
||||||
|
yield from self.agents["planner"].generate(prompt, context)
|
||||||
|
|
||||||
|
def execute_step(self, step: str, previous_findings: str) -> Generator[str, None, None]:
|
||||||
|
"""Step 2: Execute a single research step"""
|
||||||
|
prompt = f"Execute this research step: '{step}'.\nPrevious findings: {previous_findings}"
|
||||||
|
yield from self.agents["researcher"].generate(prompt)
|
||||||
|
|
||||||
|
def generate_report(self, topic: str, all_findings: str) -> Generator[str, None, None]:
|
||||||
|
"""Step 3: Generate Final Report"""
|
||||||
|
prompt = f"Write a final comprehensive report on '{topic}' based on these findings:\n{all_findings}"
|
||||||
|
yield from self.agents["writer"].generate(prompt)
|
||||||
4
report/__init__.py
Normal file
4
report/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Report 模块"""
|
||||||
|
from report.report_generator import ReportGenerator
|
||||||
|
|
||||||
|
__all__ = ["ReportGenerator"]
|
||||||
BIN
report/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
report/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
report/__pycache__/report_generator.cpython-313.pyc
Normal file
BIN
report/__pycache__/report_generator.cpython-313.pyc
Normal file
Binary file not shown.
143
report/report_generator.py
Normal file
143
report/report_generator.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""
|
||||||
|
报告生成器 - 汇总辩论内容并生成决策报告
|
||||||
|
"""
|
||||||
|
from typing import List
|
||||||
|
from orchestrator.debate_manager import SpeechRecord
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
|
||||||
|
|
||||||
|
class ReportGenerator:
|
||||||
|
"""决策报告生成器"""
|
||||||
|
|
||||||
|
def __init__(self, llm_client: LLMClient = None):
|
||||||
|
self.llm_client = llm_client or LLMClient()
|
||||||
|
|
||||||
|
def generate_report(
|
||||||
|
self,
|
||||||
|
topic: str,
|
||||||
|
speeches: List[SpeechRecord],
|
||||||
|
context: str = ""
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
生成决策报告
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: 讨论议题
|
||||||
|
speeches: 所有发言记录
|
||||||
|
context: 背景信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Markdown 格式的决策报告
|
||||||
|
"""
|
||||||
|
# 构建发言摘要
|
||||||
|
speeches_text = self._format_speeches(speeches)
|
||||||
|
|
||||||
|
system_prompt = """你是一位专业的决策分析师,擅长汇总多方观点并生成结构化的决策报告。
|
||||||
|
|
||||||
|
你的任务是根据多位专家的讨论,生成一份清晰、可操作的决策报告。
|
||||||
|
|
||||||
|
报告格式要求:
|
||||||
|
1. 使用 Markdown 格式
|
||||||
|
2. 结构清晰,重点突出
|
||||||
|
3. 提炼核心要点,不要罗列原文
|
||||||
|
4. 给出明确的建议和下一步行动"""
|
||||||
|
|
||||||
|
user_prompt = f"""## 讨论议题
|
||||||
|
{topic}
|
||||||
|
|
||||||
|
{f"## 背景信息" + chr(10) + context if context else ""}
|
||||||
|
|
||||||
|
## 专家讨论记录
|
||||||
|
{speeches_text}
|
||||||
|
|
||||||
|
## 你的任务
|
||||||
|
请生成一份决策报告,包含以下部分:
|
||||||
|
|
||||||
|
### 📋 议题概述
|
||||||
|
(1-2句话总结讨论的核心问题)
|
||||||
|
|
||||||
|
### ✅ 支持观点汇总
|
||||||
|
(列出支持该决策的主要理由,注明来源角色)
|
||||||
|
|
||||||
|
### ❌ 反对/风险观点汇总
|
||||||
|
(列出反对意见和风险点,注明来源角色)
|
||||||
|
|
||||||
|
### 🔑 关键决策要点
|
||||||
|
(3-5个需要重点考虑的因素)
|
||||||
|
|
||||||
|
### 💡 建议与下一步行动
|
||||||
|
(给出明确的建议,以及具体的下一步行动项)
|
||||||
|
|
||||||
|
### ⚖️ 决策框架
|
||||||
|
(提供一个简单的决策框架或检查清单,帮助做出最终决策)
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.llm_client.chat(
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
user_prompt=user_prompt,
|
||||||
|
max_tokens=2048
|
||||||
|
)
|
||||||
|
|
||||||
|
def _format_speeches(self, speeches: List[SpeechRecord]) -> str:
|
||||||
|
"""格式化发言记录"""
|
||||||
|
formatted = []
|
||||||
|
current_round = 0
|
||||||
|
|
||||||
|
for speech in speeches:
|
||||||
|
if speech.round_num != current_round:
|
||||||
|
current_round = speech.round_num
|
||||||
|
formatted.append(f"\n### 第 {current_round} 轮讨论\n")
|
||||||
|
|
||||||
|
formatted.append(
|
||||||
|
f"**{speech.emoji} {speech.agent_name}**:\n{speech.content}\n"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(formatted)
|
||||||
|
|
||||||
|
def generate_report_stream(
|
||||||
|
self,
|
||||||
|
topic: str,
|
||||||
|
speeches: List[SpeechRecord],
|
||||||
|
context: str = ""
|
||||||
|
):
|
||||||
|
"""流式生成决策报告"""
|
||||||
|
speeches_text = self._format_speeches(speeches)
|
||||||
|
|
||||||
|
system_prompt = """你是一位专业的决策分析师,擅长汇总多方观点并生成结构化的决策报告。"""
|
||||||
|
|
||||||
|
user_prompt = f"""## 讨论议题
|
||||||
|
{topic}
|
||||||
|
|
||||||
|
{f"## 背景信息" + chr(10) + context if context else ""}
|
||||||
|
|
||||||
|
## 专家讨论记录
|
||||||
|
{speeches_text}
|
||||||
|
|
||||||
|
## 你的任务
|
||||||
|
请生成一份决策报告,包含以下部分:
|
||||||
|
|
||||||
|
### 📋 议题概述
|
||||||
|
(1-2句话总结讨论的核心问题)
|
||||||
|
|
||||||
|
### ✅ 支持观点汇总
|
||||||
|
(列出支持该决策的主要理由,注明来源角色)
|
||||||
|
|
||||||
|
### ❌ 反对/风险观点汇总
|
||||||
|
(列出反对意见和风险点,注明来源角色)
|
||||||
|
|
||||||
|
### 🔑 关键决策要点
|
||||||
|
(3-5个需要重点考虑的因素)
|
||||||
|
|
||||||
|
### 💡 建议与下一步行动
|
||||||
|
(给出明确的建议,以及具体的下一步行动项)
|
||||||
|
|
||||||
|
### ⚖️ 决策框架
|
||||||
|
(提供一个简单的决策框架或检查清单)
|
||||||
|
"""
|
||||||
|
|
||||||
|
for chunk in self.llm_client.chat_stream(
|
||||||
|
system_prompt=system_prompt,
|
||||||
|
user_prompt=user_prompt,
|
||||||
|
max_tokens=2048
|
||||||
|
):
|
||||||
|
yield chunk
|
||||||
7
requirements.txt
Normal file
7
requirements.txt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# Multi-Agent Decision Workshop Dependencies
|
||||||
|
|
||||||
|
streamlit>=1.28.0
|
||||||
|
anthropic>=0.18.0
|
||||||
|
openai>=1.12.0
|
||||||
|
python-dotenv>=1.0.0
|
||||||
|
pydantic>=2.0.0
|
||||||
4
utils/__init__.py
Normal file
4
utils/__init__.py
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
"""Utils 模块"""
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
|
||||||
|
__all__ = ["LLMClient"]
|
||||||
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/llm_client.cpython-313.pyc
Normal file
BIN
utils/__pycache__/llm_client.cpython-313.pyc
Normal file
Binary file not shown.
141
utils/llm_client.py
Normal file
141
utils/llm_client.py
Normal file
@ -0,0 +1,141 @@
|
|||||||
|
"""
|
||||||
|
LLM 客户端封装 - 统一 Anthropic/OpenAI/AIHubMix 接口
|
||||||
|
"""
|
||||||
|
from typing import Generator
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
class LLMClient:
|
||||||
|
"""LLM API 统一客户端"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
provider: str = None,
|
||||||
|
api_key: str = None,
|
||||||
|
base_url: str = None,
|
||||||
|
model: str = None
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化 LLM 客户端
|
||||||
|
|
||||||
|
Args:
|
||||||
|
provider: 'anthropic', 'openai', 'aihubmix', 或 'custom'
|
||||||
|
api_key: API 密钥
|
||||||
|
base_url: 自定义 API 地址(用于 aihubmix/custom)
|
||||||
|
model: 指定模型名称
|
||||||
|
"""
|
||||||
|
self.provider = provider or "aihubmix"
|
||||||
|
self.model = model or "gpt-4o"
|
||||||
|
|
||||||
|
if self.provider == "anthropic":
|
||||||
|
from anthropic import Anthropic
|
||||||
|
self.client = Anthropic(api_key=api_key)
|
||||||
|
|
||||||
|
elif self.provider == "openai":
|
||||||
|
from openai import OpenAI
|
||||||
|
self.client = OpenAI(api_key=api_key)
|
||||||
|
self.model = model or "gpt-4o"
|
||||||
|
|
||||||
|
elif self.provider == "aihubmix":
|
||||||
|
# AIHubMix 兼容 OpenAI API 格式
|
||||||
|
from openai import OpenAI
|
||||||
|
self.client = OpenAI(
|
||||||
|
api_key=api_key,
|
||||||
|
base_url=base_url or "https://aihubmix.com/v1"
|
||||||
|
)
|
||||||
|
self.model = model or "gpt-4o"
|
||||||
|
|
||||||
|
elif self.provider == "custom":
|
||||||
|
# 自定义 OpenAI 兼容接口(vLLM、Ollama、TGI 等)
|
||||||
|
from openai import OpenAI
|
||||||
|
self.client = OpenAI(
|
||||||
|
api_key=api_key or "not-needed",
|
||||||
|
base_url=base_url or "http://localhost:8000/v1"
|
||||||
|
)
|
||||||
|
self.model = model or "local-model"
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(f"不支持的 provider: {self.provider}")
|
||||||
|
|
||||||
|
def chat_stream(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
max_tokens: int = 1024
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
|
"""
|
||||||
|
流式对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: 系统提示词
|
||||||
|
user_prompt: 用户输入
|
||||||
|
max_tokens: 最大输出 token 数
|
||||||
|
|
||||||
|
Yields:
|
||||||
|
str: 流式输出的文本片段
|
||||||
|
"""
|
||||||
|
if self.provider == "anthropic":
|
||||||
|
yield from self._anthropic_stream(system_prompt, user_prompt, max_tokens)
|
||||||
|
else:
|
||||||
|
yield from self._openai_stream(system_prompt, user_prompt, max_tokens)
|
||||||
|
|
||||||
|
def _anthropic_stream(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
max_tokens: int
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
|
"""Anthropic 流式调用"""
|
||||||
|
with self.client.messages.stream(
|
||||||
|
model=self.model,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
system=system_prompt,
|
||||||
|
messages=[{"role": "user", "content": user_prompt}]
|
||||||
|
) as stream:
|
||||||
|
for text in stream.text_stream:
|
||||||
|
yield text
|
||||||
|
|
||||||
|
def _openai_stream(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
max_tokens: int
|
||||||
|
) -> Generator[str, None, None]:
|
||||||
|
"""OpenAI 兼容接口流式调用(支持 AIHubMix、vLLM 等)"""
|
||||||
|
try:
|
||||||
|
stream = self.client.chat.completions.create(
|
||||||
|
model=self.model,
|
||||||
|
max_tokens=max_tokens,
|
||||||
|
stream=True,
|
||||||
|
messages=[
|
||||||
|
{"role": "system", "content": system_prompt},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for chunk in stream:
|
||||||
|
# 安全地获取 content,处理各种边界情况
|
||||||
|
if chunk.choices and len(chunk.choices) > 0:
|
||||||
|
delta = chunk.choices[0].delta
|
||||||
|
if delta and hasattr(delta, 'content') and delta.content:
|
||||||
|
yield delta.content
|
||||||
|
except Exception as e:
|
||||||
|
yield f"\n\n[错误: {str(e)}]"
|
||||||
|
|
||||||
|
def chat(
|
||||||
|
self,
|
||||||
|
system_prompt: str,
|
||||||
|
user_prompt: str,
|
||||||
|
max_tokens: int = 1024
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
非流式对话
|
||||||
|
|
||||||
|
Args:
|
||||||
|
system_prompt: 系统提示词
|
||||||
|
user_prompt: 用户输入
|
||||||
|
max_tokens: 最大输出 token 数
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 完整的响应文本
|
||||||
|
"""
|
||||||
|
return "".join(self.chat_stream(system_prompt, user_prompt, max_tokens))
|
||||||
Loading…
Reference in New Issue
Block a user