Compare commits
10 Commits
84b01a07a2
...
567a9c7cb0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
567a9c7cb0 | ||
|
|
27ec6b3d85 | ||
|
|
0de5aa038b | ||
|
|
35771a9a60 | ||
|
|
758785e57b | ||
|
|
02eea5bfb4 | ||
|
|
5913d2dc47 | ||
|
|
1b7d45fc34 | ||
|
|
8734b15be6 | ||
|
|
2e04312a7e |
10
.gitignore
vendored
Normal file
10
.gitignore
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# Python 字节码缓存
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
|
||||||
|
# 项目特定的存储/缓存文件夹
|
||||||
|
.storage/
|
||||||
|
|
||||||
|
# 环境变量文件(通常包含敏感信息)
|
||||||
|
.env
|
||||||
1
.python-version
Normal file
1
.python-version
Normal file
@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
BIN
.storage/assets/1767771534_1200.jpeg
Normal file
BIN
.storage/assets/1767771534_1200.jpeg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 355 KiB |
6
.storage/config.json
Normal file
6
.storage/config.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"provider": "AIHubMix",
|
||||||
|
"api_key": "sk-yd8Tik0nFW5emKYcBdFc433b7c8b4dC182848f76819bBe73",
|
||||||
|
"base_url": "https://aihubmix.com/v1",
|
||||||
|
"language": "Chinese"
|
||||||
|
}
|
||||||
16
.storage/history/1767772490_council_轻小说.json
Normal file
16
.storage/history/1767772490_council_轻小说.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"id": "1767772490",
|
||||||
|
"timestamp": 1767772490,
|
||||||
|
"date": "2026-01-07 15:54:50",
|
||||||
|
"type": "council",
|
||||||
|
"topic": "轻小说",
|
||||||
|
"content": "\n\n[错误: Error code: 401 - {'error': {'message': 'Invalid token: ca812c913baa474182f6d4e83e078302 (tid: 2026010707545042546382958168401)', 'type': 'Aihubmix_api_error'}}]",
|
||||||
|
"metadata": {
|
||||||
|
"rounds": 1,
|
||||||
|
"experts": [
|
||||||
|
"Expert 1",
|
||||||
|
"Expert 2"
|
||||||
|
],
|
||||||
|
"language": "Chinese"
|
||||||
|
}
|
||||||
|
}
|
||||||
17
.storage/history/1767772724_council_轻小说.json
Normal file
17
.storage/history/1767772724_council_轻小说.json
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"id": "1767772724",
|
||||||
|
"timestamp": 1767772724,
|
||||||
|
"date": "2026-01-07 15:58:44",
|
||||||
|
"type": "council",
|
||||||
|
"topic": "轻小说",
|
||||||
|
"content": "\n\n[错误: Error code: 400 - {'error': {'message': 'Model Not Exist', 'type': 'invalid_request_error', 'param': None, 'code': 'invalid_request_error'}}]",
|
||||||
|
"metadata": {
|
||||||
|
"rounds": 2,
|
||||||
|
"experts": [
|
||||||
|
"Expert 1",
|
||||||
|
"Expert 2",
|
||||||
|
"Expert 3 (Synthesizer)"
|
||||||
|
],
|
||||||
|
"language": "Chinese"
|
||||||
|
}
|
||||||
|
}
|
||||||
16
.storage/history/1767772840_council_轻小说.json
Normal file
16
.storage/history/1767772840_council_轻小说.json
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"id": "1767772840",
|
||||||
|
"timestamp": 1767772840,
|
||||||
|
"date": "2026-01-07 16:00:40",
|
||||||
|
"type": "council",
|
||||||
|
"topic": "轻小说",
|
||||||
|
"content": "好的。作为Expert 2,我将基于与Expert 1的讨论,整合我们双方的见解,形成一个关于构建可持续、有深度的“轻小说”生态系统的最终综合计划。\n\n### **关于“轻小说”与“玩乐关系”的综合分析与行动计划**\n\n经过讨论,我们(Expert 1 与 Expert 2)的核心共识在于:**“轻小说”的本质是一种以“玩乐关系”为核心驱动力的文化产品。** 这种关系超越了简单的阅读,构建了一个由 **“创作者-作品-读者社群-产业生态”** 共同参与的、动态的“叙事游乐场”。Expert 1 精准地指出了其“共玩”特性与边界模糊化,而我的补充则聚焦于这种关系背后的“玩家心态”与意义生产的“流动性”。\n\n我们共同认为,轻小说的挑战(如套路化、深度疑虑)与机遇(如强大的社群凝聚力、跨媒体潜力)皆根植于此。因此,我们的行动计划并非要否定或削弱这种“玩乐性”,而是旨在**引导、深化和结构化这种关系,使其朝着更健康、更具创造力与可持续性的方向发展。**\n\n以下是我们提出的三位一体具体行动计划:\n\n---\n\n### **行动计划:构建“共创式叙事游乐场”生态系统**\n\n本计划围绕三大核心参与方展开,由一个核心原则统御,旨在将松散的“玩乐关系”升级为有活力的“共创生态”。\n\n#### **核心原则:从“单向供给”到“有规则的共玩”**\n确立“作者拥有主叙事权,社群享有拓展权”的共识。官方作品提供坚实、自洽、富有弹性的“核心设定与初始剧情”(即主游戏),同时明确欢迎并预留空间给社群的二次创作与解读(即玩家自创模组)。这既保护了作品的原创引力,又激发了社群活力。\n\n#### **一、 对创作者:成为“世界架构师”与“游戏设计师”**\n1. **设计“开放世界”而非“线性赛道”**:\n * **行动**:在构思阶段,除了主线,需有意识地搭建具有扩展性的世界观底层规则(如魔力系统、社会结构)和留有“空白”的角色背景。这为读者的想象与二次创作提供了合法的“土壤”。\n * **目标**:将作品从封闭的故事,转变为可供探索的“世界”,",
|
||||||
|
"metadata": {
|
||||||
|
"rounds": 1,
|
||||||
|
"experts": [
|
||||||
|
"Expert 1",
|
||||||
|
"Expert 2"
|
||||||
|
],
|
||||||
|
"language": "Chinese"
|
||||||
|
}
|
||||||
|
}
|
||||||
153
README.md
153
README.md
@ -1,82 +1,91 @@
|
|||||||
# Multi-Agent Decision Workshop & Deep Research
|
# 🍎 智能决策工作坊 (Multi-Agent Council V4)
|
||||||
|
|
||||||
这是一个基于多智能体(Multi-Agent)的决策辅助和深度研究系统。它包含两个核心模式:
|
AI驱动的多智能体决策分析系统 - 基于多模型智囊团
|
||||||
1. **Deep Research Mode (Deep Research)**: 模仿 Gemini 研究模式,通过规划、执行、撰写三个阶段进行深度分析。
|
|
||||||
2. **Debate Workshop (辩论工作坊)**: 让多个 AI 角色从不同视角辩论,帮助你做出更全面的决策。
|
|
||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 核心功能
|
||||||
|
|
||||||
- **双模式切换**: 侧边栏一键切换 "Deep Research" 和 "Debate Workshop"。
|
### 🧪 Multi-Model Council V4 (智囊团模式)
|
||||||
- **自定义模型角色**:
|
- **多轮对话讨论**: 专家像真实会议一样进行多轮对话,互相批判、补充观点
|
||||||
- 在 Deep Research 模式下,可以分别指定 `Planner` (规划者), `Researcher` (研究员), `Writer` (作家) 使用不同的 LLM。
|
- **动态专家组建**: 自定义 2-5 位专家,为每位指定最擅长的模型
|
||||||
- **多模型支持**: 支持 OpenAI (GPT-4o), Anthropic (Claude 3.5), Gemini 等主流模型。
|
- **🪄 智能专家生成**: AI 根据主题自动推荐最合适的专家角色
|
||||||
- **交互式研究**: 生成研究计划后,用户可以介入修改,确保研究方向正确。
|
- **最终决策合成**: 最后一位专家综合全场观点,生成方案并绘制 Mermaid 路线图
|
||||||
- **流式输出**: 实时展示研究进度和辩论过程。
|
|
||||||
|
|
||||||
## 🛠️ 安装与使用
|
### 🎯 内置决策场景
|
||||||
|
系统预置 4 大典型决策场景,每个场景都配置了专业的典型问题:
|
||||||
|
|
||||||
### 1. 克隆项目
|
| 场景 | 描述 |
|
||||||
|
|------|------|
|
||||||
```bash
|
| 🚀 新产品发布评审 | 评估产品可行性、市场潜力和实施计划 |
|
||||||
git clone https://github.com/HomoDeusss/multi-agent.git
|
| 💰 投资审批决策 | 分析投资项目的 ROI、风险和战略价值 |
|
||||||
cd multi-agent
|
| 🤝 合作伙伴评估 | 评估合作伙伴的匹配度和合作价值 |
|
||||||
```
|
| 📦 供应商评估 | 对比分析供应商的综合能力 |
|
||||||
|
|
||||||
### 2. 安装依赖
|
|
||||||
|
|
||||||
确保你安装了 Python 3.8+。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 配置 API Key
|
|
||||||
|
|
||||||
你可以通过以下两种方式配置 API Key:
|
|
||||||
|
|
||||||
**方式 A: 创建 `.env` 文件 (推荐)**
|
|
||||||
复制 `.env.example` 为 `.env`,并填入你的 API Key。
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
编辑 `.env` 文件:
|
|
||||||
```env
|
|
||||||
AIHUBMIX_API_KEY=your_api_key_here
|
|
||||||
```
|
|
||||||
|
|
||||||
**方式 B: 在 UI 中输入**
|
|
||||||
启动应用后,在侧边栏的 "设置" -> "API Key" 输入框中填入。
|
|
||||||
|
|
||||||
### 4. 启动应用
|
|
||||||
|
|
||||||
运行 Streamlit 应用:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
streamlit run app.py
|
|
||||||
```
|
|
||||||
|
|
||||||
会自动在浏览器打开 `http://localhost:8501`。
|
|
||||||
|
|
||||||
## 📖 使用指南
|
|
||||||
|
|
||||||
### 🧪 Deep Research Mode (深度研究模式)
|
|
||||||
1. 在侧边栏选择模式为 **"Deep Research"**。
|
|
||||||
2. 在 "研究模型配置" 中,为 Planner, Researcher, Writer 选择合适的模型(推荐分别使用 GPT-4o, Gemini-1.5-pro, Claude-3.5-sonnet)。
|
|
||||||
3. 输入你的**研究主题** (例如: "2025年量子计算商业化前景")。
|
|
||||||
4. 点击 **"生成研究计划"**。
|
|
||||||
5. 系统生成计划后,你可以直接在文本框中**修改计划步骤**。
|
|
||||||
6. 点击 **"开始深度研究"**,观察 Agent 逐步执行研究任务。
|
|
||||||
7. 下载最终生成的 Markdown 报告。
|
|
||||||
|
|
||||||
### 🎭 Debate Workshop (辩论工作坊)
|
### 🎭 Debate Workshop (辩论工作坊)
|
||||||
1. 在侧边栏选择模式为 **"Debate Workshop"**。
|
让 AI 扮演不同立场角色,通过辩论帮助厘清复杂决策的利弊
|
||||||
2. 输入**决策议题** (例如: "我是否应该辞职创业?")。
|
|
||||||
3. 选择参与辩论的 **AI 角色** (如: CEO, 风险控制专家, 职业顾问)。
|
### 💬 用户反馈
|
||||||
4. 点击 **"开始辩论"**。
|
内置用户反馈系统,收集功能建议和使用体验
|
||||||
5. 观看不同角色之间的唇枪舌战,最后生成综合决策建议。
|
|
||||||
|
### 🌐 多平台支持
|
||||||
|
- **DeepSeek**: V3, R1, Coder
|
||||||
|
- **OpenAI**: GPT-4o, GPT-4o-mini
|
||||||
|
- **Anthropic**: Claude 3.5 Sonnet
|
||||||
|
- **Google**: Gemini 1.5/2.0
|
||||||
|
- **SiliconFlow / AIHubMix / Deepseek**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone https://github.com/HomoDeusss/multi-agent.git
|
||||||
|
cd multi-agent
|
||||||
|
|
||||||
|
# 初始化 uv 项目(如首次使用)
|
||||||
|
uv init
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
uv add streamlit openai anthropic python-dotenv
|
||||||
|
|
||||||
|
# 或者同步现有依赖
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run streamlit run app.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用步骤
|
||||||
|
|
||||||
|
1. **配置 API**: 在侧边栏选择 Provider 并输入 API Key
|
||||||
|
2. **选择场景**: 点击预置的决策场景或自定义主题
|
||||||
|
3. **生成专家**: 点击 "🪄 根据主题自动生成专家" 或手动配置
|
||||||
|
4. **开始决策**: 观察专家们如何互相对话,生成综合方案
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
multi_agent_workshop/
|
||||||
|
├── app.py # Streamlit 主应用
|
||||||
|
├── config.py # 配置文件
|
||||||
|
├── agents/ # Agent 定义
|
||||||
|
│ ├── agent_profiles.py # 预设角色配置
|
||||||
|
│ ├── base_agent.py # 基础 Agent 类
|
||||||
|
│ └── research_agent.py # 研究型 Agent
|
||||||
|
├── orchestrator/ # 编排器
|
||||||
|
│ ├── debate_manager.py # 辩论管理
|
||||||
|
│ └── research_manager.py # 智囊团管理
|
||||||
|
├── utils/
|
||||||
|
│ ├── llm_client.py # LLM 客户端封装
|
||||||
|
│ ├── storage.py # 存储管理
|
||||||
|
│ └── auto_agent_generator.py # 智能专家生成
|
||||||
|
└── report/ # 报告生成
|
||||||
|
```
|
||||||
|
|
||||||
## 📝 License
|
## 📝 License
|
||||||
|
|
||||||
[MIT License](LICENSE)
|
[MIT License](LICENSE)
|
||||||
BIN
__pycache__/app.cpython-312.pyc
Normal file
BIN
__pycache__/app.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
__pycache__/config.cpython-312.pyc
Normal file
BIN
__pycache__/config.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
agents/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
agents/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
agents/__pycache__/agent_profiles.cpython-312.pyc
Normal file
BIN
agents/__pycache__/agent_profiles.cpython-312.pyc
Normal file
Binary file not shown.
BIN
agents/__pycache__/base_agent.cpython-312.pyc
Normal file
BIN
agents/__pycache__/base_agent.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
agents/__pycache__/research_agent.cpython-312.pyc
Normal file
BIN
agents/__pycache__/research_agent.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -19,16 +19,18 @@ class AgentMessage:
|
|||||||
class BaseAgent:
|
class BaseAgent:
|
||||||
"""Agent 基类"""
|
"""Agent 基类"""
|
||||||
|
|
||||||
def __init__(self, agent_id: str, llm_client):
|
def __init__(self, agent_id: str, llm_client, language: str = "Chinese"):
|
||||||
"""
|
"""
|
||||||
初始化 Agent
|
初始化 Agent
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
agent_id: Agent 标识符 (如 'ceo', 'cto')
|
agent_id: Agent 标识符 (如 'ceo', 'cto')
|
||||||
llm_client: LLM 客户端实例
|
llm_client: LLM 客户端实例
|
||||||
|
language: 输出语言
|
||||||
"""
|
"""
|
||||||
self.agent_id = agent_id
|
self.agent_id = agent_id
|
||||||
self.llm_client = llm_client
|
self.llm_client = llm_client
|
||||||
|
self.language = language
|
||||||
|
|
||||||
profile = get_agent_profile(agent_id)
|
profile = get_agent_profile(agent_id)
|
||||||
if not profile:
|
if not profile:
|
||||||
@ -38,7 +40,7 @@ class BaseAgent:
|
|||||||
self.emoji = profile["emoji"]
|
self.emoji = profile["emoji"]
|
||||||
self.perspective = profile["perspective"]
|
self.perspective = profile["perspective"]
|
||||||
self.focus_areas = profile["focus_areas"]
|
self.focus_areas = profile["focus_areas"]
|
||||||
self.system_prompt = profile["system_prompt"]
|
self.system_prompt = f"{profile['system_prompt']}\n\nIMPORTANT: You MUST output your response in {self.language}."
|
||||||
|
|
||||||
# 存储对话历史
|
# 存储对话历史
|
||||||
self.conversation_history = []
|
self.conversation_history = []
|
||||||
|
|||||||
@ -5,26 +5,28 @@ import config
|
|||||||
class ResearchAgent:
|
class ResearchAgent:
|
||||||
"""研究模式专用 Agent"""
|
"""研究模式专用 Agent"""
|
||||||
|
|
||||||
def __init__(self, role: str, llm_client: LLMClient, name: str = None):
|
def __init__(self, role: str, llm_client: LLMClient, name: str = None, language: str = "Chinese"):
|
||||||
self.role = role
|
self.role = role
|
||||||
self.llm_client = llm_client
|
self.llm_client = llm_client
|
||||||
self.role_config = config.RESEARCH_MODEL_ROLES.get(role, {})
|
self.role_config = config.RESEARCH_MODEL_ROLES.get(role, {})
|
||||||
self.name = name if name else self.role_config.get("name", role.capitalize())
|
self.name = name if name else self.role_config.get("name", role.capitalize())
|
||||||
|
self.language = language
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def model_name(self) -> str:
|
def model_name(self) -> str:
|
||||||
return self.llm_client.model
|
return self.llm_client.model
|
||||||
|
|
||||||
def _get_system_prompt(self, context: str = "") -> str:
|
def _get_system_prompt(self, context: str = "") -> str:
|
||||||
|
base_prompt = ""
|
||||||
if self.role == "council_member":
|
if self.role == "council_member":
|
||||||
return f"""You are {self.name}, a member of the Multi-Model Decision Council.
|
base_prompt = f"""You are {self.name}, a member of the Multi-Model Decision Council.
|
||||||
Your goal is to participate in a round-table discussion to solve the user's problem.
|
Your goal is to participate in a round-table discussion to solve the user's problem.
|
||||||
Be conversational, insightful, and constructive.
|
Be conversational, insightful, and constructive.
|
||||||
Build upon others' ideas or respectfully disagree with valid reasoning.
|
Build upon others' ideas or respectfully disagree with valid reasoning.
|
||||||
Context: {context}"""
|
Context: {context}"""
|
||||||
|
|
||||||
elif self.role == "expert_a":
|
elif self.role == "expert_a":
|
||||||
return f"""You are Expert A, a Senior Analyst.
|
base_prompt = f"""You are Expert A, a Senior Analyst.
|
||||||
You are participating in a round-table discussion.
|
You are participating in a round-table discussion.
|
||||||
Your goal is to analyze the topic and propose solutions.
|
Your goal is to analyze the topic and propose solutions.
|
||||||
Be conversational, direct, and responsive to other experts.
|
Be conversational, direct, and responsive to other experts.
|
||||||
@ -32,21 +34,23 @@ Do not write a full final report; focus on the current discussion turn.
|
|||||||
Context: {context}"""
|
Context: {context}"""
|
||||||
|
|
||||||
elif self.role == "expert_b":
|
elif self.role == "expert_b":
|
||||||
return f"""You are Expert B, a Critical Reviewer.
|
base_prompt = f"""You are Expert B, a Critical Reviewer.
|
||||||
You are participating in a round-table discussion.
|
You are participating in a round-table discussion.
|
||||||
Your goal is to critique Expert A's points and offer alternative perspectives.
|
Your goal is to critique Expert A's points and offer alternative perspectives.
|
||||||
Be conversational and constructive. Challenge assumptions directly.
|
Be conversational and constructive. Challenge assumptions directly.
|
||||||
Context: {context}"""
|
Context: {context}"""
|
||||||
|
|
||||||
elif self.role == "expert_c":
|
elif self.role == "expert_c":
|
||||||
return f"""You are Expert C, a Senior Strategist and Visual Thinker.
|
base_prompt = f"""You are Expert C, a Senior Strategist and Visual Thinker.
|
||||||
Your goal is to synthesize the final output.
|
Your goal is to synthesize the final output.
|
||||||
Combine the structural strength of Expert A with the critical insights of Expert B.
|
Combine the structural strength of Expert A with the critical insights of Expert B.
|
||||||
Produce a final, polished, comprehensive plan or report.
|
Produce a final, polished, comprehensive plan or report.
|
||||||
CRITICAL: You MUST include a Mermaid.js diagram (using ```mermaid code block) to visualize the timeline, process, or architecture."""
|
CRITICAL: You MUST include a Mermaid.js diagram (using ```mermaid code block) to visualize the timeline, process, or architecture."""
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return "You are a helpful assistant."
|
base_prompt = "You are a helpful assistant."
|
||||||
|
|
||||||
|
return f"{base_prompt}\n\nIMPORTANT: You MUST output your response in {self.language}."
|
||||||
|
|
||||||
def generate(self, prompt: str, context: str = "") -> Generator[str, None, None]:
|
def generate(self, prompt: str, context: str = "") -> Generator[str, None, None]:
|
||||||
"""Generate response stream"""
|
"""Generate response stream"""
|
||||||
|
|||||||
@ -9,7 +9,7 @@ load_dotenv()
|
|||||||
# API 配置
|
# API 配置
|
||||||
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
|
||||||
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
OPENAI_API_KEY = os.getenv("OPENAI_API_KEY", "")
|
||||||
AIHUBMIX_API_KEY = os.getenv("AIHUBMIX_API_KEY", "sk-yd8Tik0nFW5emKYcBdFc433b7c8b4dC182848f76819bBe73")
|
AIHUBMIX_API_KEY = os.getenv("AIHUBMIX_API_KEY", "")
|
||||||
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
|
||||||
SILICONFLOW_API_KEY = os.getenv("SILICONFLOW_API_KEY", "")
|
SILICONFLOW_API_KEY = os.getenv("SILICONFLOW_API_KEY", "")
|
||||||
|
|
||||||
@ -93,6 +93,12 @@ AVAILABLE_MODELS = {
|
|||||||
MAX_DEBATE_ROUNDS = 3 # 最大辩论轮数
|
MAX_DEBATE_ROUNDS = 3 # 最大辩论轮数
|
||||||
MAX_AGENTS = 6 # 最大参与 Agent 数量
|
MAX_AGENTS = 6 # 最大参与 Agent 数量
|
||||||
|
|
||||||
|
# 支持的输出语言
|
||||||
|
SUPPORTED_LANGUAGES = ["Chinese", "English", "Japanese", "Spanish", "French", "German"]
|
||||||
|
|
||||||
|
# 生成配置
|
||||||
|
MAX_OUTPUT_TOKENS = 300 # 限制单次回复长度,保持精简
|
||||||
|
|
||||||
# 研究模式模型角色配置
|
# 研究模式模型角色配置
|
||||||
RESEARCH_MODEL_ROLES = {
|
RESEARCH_MODEL_ROLES = {
|
||||||
"expert_a": {
|
"expert_a": {
|
||||||
|
|||||||
6
main.py
Normal file
6
main.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
def main():
|
||||||
|
print("Hello from multi-agent!")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
BIN
orchestrator/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
orchestrator/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
orchestrator/__pycache__/debate_manager.cpython-312.pyc
Normal file
BIN
orchestrator/__pycache__/debate_manager.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
orchestrator/__pycache__/research_manager.cpython-312.pyc
Normal file
BIN
orchestrator/__pycache__/research_manager.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
@ -18,6 +18,7 @@ class DebateConfig:
|
|||||||
agent_ids: List[str] = None
|
agent_ids: List[str] = None
|
||||||
max_rounds: int = 2
|
max_rounds: int = 2
|
||||||
agent_clients: dict = None # Map[agent_id, LLMClient]
|
agent_clients: dict = None # Map[agent_id, LLMClient]
|
||||||
|
language: str = "Chinese"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
@ -64,7 +65,7 @@ class DebateManager:
|
|||||||
if hasattr(debate_config, 'agent_clients') and debate_config.agent_clients and agent_id in debate_config.agent_clients:
|
if hasattr(debate_config, 'agent_clients') and debate_config.agent_clients and agent_id in debate_config.agent_clients:
|
||||||
client = debate_config.agent_clients[agent_id]
|
client = debate_config.agent_clients[agent_id]
|
||||||
|
|
||||||
agent = BaseAgent(agent_id, client)
|
agent = BaseAgent(agent_id, client, language=debate_config.language)
|
||||||
self.agents.append(agent)
|
self.agents.append(agent)
|
||||||
|
|
||||||
def run_debate_stream(
|
def run_debate_stream(
|
||||||
|
|||||||
@ -10,6 +10,7 @@ class ResearchConfig:
|
|||||||
context: str = ""
|
context: str = ""
|
||||||
# Dynamic list of experts: [{"name": "Expert 1", "model": "gpt-4o", "role": "analyst"}, ...]
|
# Dynamic list of experts: [{"name": "Expert 1", "model": "gpt-4o", "role": "analyst"}, ...]
|
||||||
experts: List[Dict[str, str]] = None
|
experts: List[Dict[str, str]] = None
|
||||||
|
language: str = "Chinese"
|
||||||
|
|
||||||
class ResearchManager:
|
class ResearchManager:
|
||||||
"""Manages the Multi-Model Council workflow"""
|
"""Manages the Multi-Model Council workflow"""
|
||||||
@ -33,19 +34,12 @@ class ResearchManager:
|
|||||||
self.agents = []
|
self.agents = []
|
||||||
if config.experts:
|
if config.experts:
|
||||||
for idx, expert_conf in enumerate(config.experts):
|
for idx, expert_conf in enumerate(config.experts):
|
||||||
# Assign role based on position or config
|
|
||||||
# First agents are discussion members, last one is Synthesizer usually,
|
|
||||||
# but for equality we treat them all as members until the end.
|
|
||||||
# We'll assign a generic "member" role or specific if provided.
|
|
||||||
|
|
||||||
role_type = "council_member"
|
role_type = "council_member"
|
||||||
# If it's the last one, maybe give them synthesizer duty?
|
|
||||||
# For now, all are members, and we explicitly pick one for synthesis.
|
|
||||||
|
|
||||||
agent = ResearchAgent(
|
agent = ResearchAgent(
|
||||||
role=role_type,
|
role=role_type,
|
||||||
llm_client=self._get_client(expert_conf["model"]),
|
llm_client=self._get_client(expert_conf["model"]),
|
||||||
name=expert_conf.get("name", f"Expert {idx+1}")
|
name=expert_conf.get("name", f"Expert {idx+1}"),
|
||||||
|
language=config.language
|
||||||
)
|
)
|
||||||
self.agents.append(agent)
|
self.agents.append(agent)
|
||||||
|
|
||||||
|
|||||||
13
pyproject.toml
Normal file
13
pyproject.toml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
[project]
|
||||||
|
name = "multi-agent"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
dependencies = [
|
||||||
|
"anthropic>=0.75.0",
|
||||||
|
"openai>=2.14.0",
|
||||||
|
"pydantic>=2.12.5",
|
||||||
|
"python-dotenv>=1.2.1",
|
||||||
|
"streamlit>=1.52.2",
|
||||||
|
]
|
||||||
BIN
report/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
report/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
report/__pycache__/report_generator.cpython-312.pyc
Normal file
BIN
report/__pycache__/report_generator.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
utils/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/llm_client.cpython-312.pyc
Normal file
BIN
utils/__pycache__/llm_client.cpython-312.pyc
Normal file
Binary file not shown.
Binary file not shown.
BIN
utils/__pycache__/storage.cpython-312.pyc
Normal file
BIN
utils/__pycache__/storage.cpython-312.pyc
Normal file
Binary file not shown.
BIN
utils/__pycache__/storage.cpython-313.pyc
Normal file
BIN
utils/__pycache__/storage.cpython-313.pyc
Normal file
Binary file not shown.
108
utils/auto_agent_generator.py
Normal file
108
utils/auto_agent_generator.py
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
"""
|
||||||
|
Auto Agent Generator - 根据主题自动生成专家配置
|
||||||
|
Uses LLM to analyze the topic and suggest appropriate expert agents.
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from typing import List, Dict
|
||||||
|
from utils.llm_client import LLMClient
|
||||||
|
|
||||||
|
|
||||||
|
EXPERT_GENERATION_PROMPT = """You are an expert team composition advisor. Given a research/decision topic, you need to suggest the most appropriate team of experts to analyze it.
|
||||||
|
|
||||||
|
Instructions:
|
||||||
|
1. Analyze the topic carefully to understand its domain and key aspects
|
||||||
|
2. Generate {num_experts} distinct expert roles that would provide the most valuable perspectives
|
||||||
|
3. Each expert should have a unique focus area relevant to the topic
|
||||||
|
4. The LAST expert should always be a "Synthesizer" role who can integrate all perspectives
|
||||||
|
|
||||||
|
Output Format (MUST be valid JSON array):
|
||||||
|
[
|
||||||
|
{{"name": "Expert Name", "perspective": "Brief description of their viewpoint", "focus": "Key areas they analyze"}},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
Examples of good expert names based on topic:
|
||||||
|
- For "Should we launch an e-commerce platform?": "市场渠道分析师", "电商运营专家", "供应链顾问", "数字化转型综合师"
|
||||||
|
- For "Career transition to AI field": "职业发展顾问", "AI行业专家", "技能评估分析师", "综合规划师"
|
||||||
|
|
||||||
|
IMPORTANT:
|
||||||
|
- Use {language} for all names and descriptions
|
||||||
|
- Make names specific to the topic, not generic like "Expert 1"
|
||||||
|
- The last expert MUST be a synthesizer/integrator type
|
||||||
|
|
||||||
|
Topic: {topic}
|
||||||
|
|
||||||
|
Generate exactly {num_experts} experts as a JSON array:"""
|
||||||
|
|
||||||
|
|
||||||
|
def generate_experts_for_topic(
|
||||||
|
topic: str,
|
||||||
|
num_experts: int,
|
||||||
|
llm_client: LLMClient,
|
||||||
|
language: str = "Chinese"
|
||||||
|
) -> List[Dict[str, str]]:
|
||||||
|
"""
|
||||||
|
Use LLM to generate appropriate expert configurations based on the topic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
topic: The research/decision topic
|
||||||
|
num_experts: Number of experts to generate (2-5)
|
||||||
|
llm_client: LLM client instance for API calls
|
||||||
|
language: Output language (Chinese/English)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List of expert dicts: [{"name": "...", "perspective": "...", "focus": "..."}, ...]
|
||||||
|
"""
|
||||||
|
if not topic.strip():
|
||||||
|
return []
|
||||||
|
|
||||||
|
prompt = EXPERT_GENERATION_PROMPT.format(
|
||||||
|
topic=topic,
|
||||||
|
num_experts=num_experts,
|
||||||
|
language=language
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = llm_client.chat(
|
||||||
|
system_prompt="You are a helpful assistant that generates JSON output only. No markdown, no explanation.",
|
||||||
|
user_prompt=prompt,
|
||||||
|
max_tokens=800
|
||||||
|
)
|
||||||
|
|
||||||
|
# Extract JSON from response (handle potential markdown wrapping)
|
||||||
|
json_match = re.search(r'\[[\s\S]*\]', response)
|
||||||
|
if json_match:
|
||||||
|
experts = json.loads(json_match.group())
|
||||||
|
# Validate structure
|
||||||
|
if isinstance(experts, list) and len(experts) >= 1:
|
||||||
|
validated = []
|
||||||
|
for exp in experts[:num_experts]:
|
||||||
|
if isinstance(exp, dict) and "name" in exp:
|
||||||
|
validated.append({
|
||||||
|
"name": exp.get("name", "Expert"),
|
||||||
|
"perspective": exp.get("perspective", ""),
|
||||||
|
"focus": exp.get("focus", "")
|
||||||
|
})
|
||||||
|
return validated
|
||||||
|
except (json.JSONDecodeError, Exception) as e:
|
||||||
|
print(f"[AutoAgentGenerator] Error parsing LLM response: {e}")
|
||||||
|
|
||||||
|
# Fallback: return generic experts
|
||||||
|
fallback = []
|
||||||
|
for i in range(num_experts):
|
||||||
|
if i == num_experts - 1:
|
||||||
|
fallback.append({"name": f"综合分析师", "perspective": "整合视角", "focus": "综合决策"})
|
||||||
|
else:
|
||||||
|
fallback.append({"name": f"专家 {i+1}", "perspective": "分析视角", "focus": "专业分析"})
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_model_for_expert(expert_index: int, total_experts: int, available_models: list) -> str:
|
||||||
|
"""
|
||||||
|
Assign a default model to an expert based on their position.
|
||||||
|
Spreads experts across available models for diversity.
|
||||||
|
"""
|
||||||
|
if not available_models:
|
||||||
|
return "gpt-4o"
|
||||||
|
return available_models[expert_index % len(available_models)]
|
||||||
@ -5,6 +5,8 @@ from typing import Generator
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
import config
|
||||||
|
|
||||||
class LLMClient:
|
class LLMClient:
|
||||||
"""LLM API 统一客户端"""
|
"""LLM API 统一客户端"""
|
||||||
|
|
||||||
@ -62,7 +64,7 @@ class LLMClient:
|
|||||||
self,
|
self,
|
||||||
system_prompt: str,
|
system_prompt: str,
|
||||||
user_prompt: str,
|
user_prompt: str,
|
||||||
max_tokens: int = 1024
|
max_tokens: int = config.MAX_OUTPUT_TOKENS
|
||||||
) -> Generator[str, None, None]:
|
) -> Generator[str, None, None]:
|
||||||
"""
|
"""
|
||||||
流式对话
|
流式对话
|
||||||
|
|||||||
184
utils/storage.py
Normal file
184
utils/storage.py
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
"""
|
||||||
|
Storage Manager - Handle local persistence of configuration, history/reports, and assets.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
STORAGE_DIR = ".storage"
|
||||||
|
CONFIG_FILE = "config.json"
|
||||||
|
HISTORY_DIR = "history"
|
||||||
|
ASSETS_DIR = "assets"
|
||||||
|
|
||||||
|
class StorageManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.root_dir = Path(STORAGE_DIR)
|
||||||
|
self.config_path = self.root_dir / CONFIG_FILE
|
||||||
|
self.history_dir = self.root_dir / HISTORY_DIR
|
||||||
|
self.assets_dir = self.root_dir / ASSETS_DIR
|
||||||
|
|
||||||
|
# Ensure directories exist
|
||||||
|
self.root_dir.mkdir(exist_ok=True)
|
||||||
|
self.history_dir.mkdir(exist_ok=True)
|
||||||
|
self.assets_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
def save_config(self, config_data: Dict[str, Any]):
|
||||||
|
"""Save UI configuration to file"""
|
||||||
|
try:
|
||||||
|
with open(self.config_path, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(config_data, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving config: {e}")
|
||||||
|
|
||||||
|
def load_config(self) -> Dict[str, Any]:
|
||||||
|
"""Load UI configuration from file"""
|
||||||
|
if not self.config_path.exists():
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading config: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def save_asset(self, uploaded_file) -> str:
|
||||||
|
"""Save an uploaded file (e.g., background image) into assets directory.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
uploaded_file: a file-like object (Streamlit UploadedFile) or bytes-like
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The saved file path as string, or None on failure.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Determine filename
|
||||||
|
if hasattr(uploaded_file, 'name'):
|
||||||
|
filename = uploaded_file.name
|
||||||
|
else:
|
||||||
|
filename = f"asset_{int(time.time())}"
|
||||||
|
|
||||||
|
# sanitize
|
||||||
|
safe_name = "".join([c for c in filename if c.isalnum() or c in (' ', '.', '_', '-')]).strip().replace(' ', '_')
|
||||||
|
dest = self.assets_dir / f"{int(time.time())}_{safe_name}"
|
||||||
|
|
||||||
|
# Write bytes
|
||||||
|
with open(dest, 'wb') as out:
|
||||||
|
# Streamlit UploadedFile has getbuffer()
|
||||||
|
if hasattr(uploaded_file, 'getbuffer'):
|
||||||
|
out.write(uploaded_file.getbuffer())
|
||||||
|
else:
|
||||||
|
# try reading
|
||||||
|
data = uploaded_file.read()
|
||||||
|
if isinstance(data, str):
|
||||||
|
data = data.encode('utf-8')
|
||||||
|
out.write(data)
|
||||||
|
|
||||||
|
return str(dest)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving asset: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def save_history(self, session_type: str, topic: str, content: str, metadata: Dict[str, Any] = None):
|
||||||
|
"""
|
||||||
|
Save a session report/history
|
||||||
|
|
||||||
|
Args:
|
||||||
|
session_type: 'council' or 'debate'
|
||||||
|
topic: The main topic
|
||||||
|
content: The full markdown report or content
|
||||||
|
metadata: Additional info (model used, date, etc)
|
||||||
|
"""
|
||||||
|
timestamp = int(time.time())
|
||||||
|
date_str = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||||
|
|
||||||
|
# Create a safe filename
|
||||||
|
safe_topic = "".join([c for c in topic[:20] if c.isalnum() or c in (' ', '_', '-')]).strip().replace(' ', '_')
|
||||||
|
filename = f"{timestamp}_{session_type}_{safe_topic}.json"
|
||||||
|
|
||||||
|
data = {
|
||||||
|
"id": str(timestamp),
|
||||||
|
"timestamp": timestamp,
|
||||||
|
"date": date_str,
|
||||||
|
"type": session_type,
|
||||||
|
"topic": topic,
|
||||||
|
"content": content,
|
||||||
|
"metadata": metadata or {}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(self.history_dir / filename, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving history: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def list_history(self) -> List[Dict[str, Any]]:
|
||||||
|
"""List all history items (metadata only)"""
|
||||||
|
items = []
|
||||||
|
if not self.history_dir.exists():
|
||||||
|
return []
|
||||||
|
|
||||||
|
for file in self.history_dir.glob("*.json"):
|
||||||
|
try:
|
||||||
|
with open(file, 'r', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
# Return summary info
|
||||||
|
items.append({
|
||||||
|
"id": data.get("id"),
|
||||||
|
"date": data.get("date"),
|
||||||
|
"type": data.get("type"),
|
||||||
|
"topic": data.get("topic"),
|
||||||
|
"filename": file.name
|
||||||
|
})
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Sort by timestamp desc
|
||||||
|
return sorted(items, key=lambda x: x.get("date", ""), reverse=True)
|
||||||
|
|
||||||
|
def load_history_item(self, filename: str) -> Dict[str, Any]:
|
||||||
|
"""Load full content of a history item"""
|
||||||
|
path = self.history_dir / filename
|
||||||
|
if not path.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(path, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# ==================== Session Cache (Resume Functionality) ====================
|
||||||
|
def save_session_state(self, key: str, data: Dict[str, Any]):
|
||||||
|
"""Save temporary session state for recovery"""
|
||||||
|
try:
|
||||||
|
# We use a dedicated cache file per key
|
||||||
|
cache_file = self.root_dir / f"{key}_cache.json"
|
||||||
|
data["_timestamp"] = int(time.time())
|
||||||
|
with open(cache_file, 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error saving session cache: {e}")
|
||||||
|
|
||||||
|
def load_session_state(self, key: str) -> Dict[str, Any]:
|
||||||
|
"""Load temporary session state"""
|
||||||
|
cache_file = self.root_dir / f"{key}_cache.json"
|
||||||
|
if not cache_file.exists():
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
with open(cache_file, 'r', encoding='utf-8') as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def clear_session_state(self, key: str):
|
||||||
|
"""Clear temporary session state"""
|
||||||
|
cache_file = self.root_dir / f"{key}_cache.json"
|
||||||
|
if cache_file.exists():
|
||||||
|
try:
|
||||||
|
os.remove(cache_file)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
Loading…
Reference in New Issue
Block a user