Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- README.md +34 -7
- app.py +143 -0
- config/agents.yaml +73 -0
- config/mcp_servers.yaml +24 -0
- config/settings.yaml +28 -0
- requirements.txt +6 -0
- src/__init__.py +0 -0
- src/agents/__init__.py +3 -0
- src/agents/coder.py +31 -0
- src/agents/planner.py +12 -0
- src/agents/reviewer.py +19 -0
- src/core/__init__.py +3 -0
- src/core/agent_base.py +88 -0
- src/core/config.py +27 -0
- src/core/llm_client.py +83 -0
- src/core/message.py +42 -0
- src/core/orchestrator.py +193 -0
- src/utils/__init__.py +0 -0
- src/utils/logger.py +18 -0
README.md
CHANGED
|
@@ -1,13 +1,40 @@
|
|
| 1 |
---
|
| 2 |
-
title: CodeAgent
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: CodeAgent-MCP
|
| 3 |
+
emoji: "\U0001F916"
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: purple
|
| 6 |
sdk: gradio
|
| 7 |
+
sdk_version: "4.44.1"
|
|
|
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: mit
|
| 11 |
---
|
| 12 |
|
| 13 |
+
# CodeAgent-MCP
|
| 14 |
+
|
| 15 |
+
Multi-Agent Code Generation System with MCP Protocol.
|
| 16 |
+
|
| 17 |
+
**Planner** (task decomposition) -> **Coder** (code generation) -> **Reviewer** (code review) feedback loop.
|
| 18 |
+
|
| 19 |
+
## How to use
|
| 20 |
+
|
| 21 |
+
1. Enter your DeepSeek / OpenAI API key
|
| 22 |
+
2. Choose a provider (default = DeepSeek)
|
| 23 |
+
3. Describe the code you want to generate
|
| 24 |
+
4. Click "Start" and watch the multi-agent system work
|
| 25 |
+
|
| 26 |
+
## Architecture
|
| 27 |
+
|
| 28 |
+
- **Planner Agent**: Decomposes complex requirements into 2-4 subtasks
|
| 29 |
+
- **Coder Agent**: Generates code with optional MCP tool integration
|
| 30 |
+
- **Reviewer Agent**: Scores code quality (1-10) and provides improvement suggestions
|
| 31 |
+
- **Orchestrator**: Manages Coder-Reviewer feedback loop until quality threshold is met
|
| 32 |
+
|
| 33 |
+
## Project Series
|
| 34 |
+
|
| 35 |
+
1. [small-llms-tool-use](https://github.com/XIECHENG6/small-llms-tool-use) - Function calling fine-tuning (86-89% exact match)
|
| 36 |
+
2. [agenttune](https://github.com/XIECHENG6/agenttune) - Multi-step ReAct reasoning (100% task success rate)
|
| 37 |
+
3. [smallrag](https://github.com/XIECHENG6/smallrag) - RAG optimization (chunk_size=512 + MMR + top-k=5)
|
| 38 |
+
4. **CodeAgent-MCP** (this project) - Multi-Agent system integration
|
| 39 |
+
|
| 40 |
+
[GitHub](https://github.com/XIECHENG6/CodeAgent-MCP)
|
app.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
CodeAgent-MCP — HuggingFace Spaces Demo (Gradio).
|
| 3 |
+
|
| 4 |
+
Multi-Agent code generation with Planner → Coder → Reviewer loop.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import asyncio
|
| 8 |
+
import json
|
| 9 |
+
import os
|
| 10 |
+
import time
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
|
| 14 |
+
from src.core.config import load_settings, load_agents_config
|
| 15 |
+
from src.core.llm_client import LLMClient
|
| 16 |
+
from src.core.orchestrator import Orchestrator
|
| 17 |
+
from src.agents import PlannerAgent, CoderAgent, ReviewerAgent
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
EXAMPLE_TASKS = [
|
| 21 |
+
"实现一个 LRU Cache,支持 get 和 put 操作,要求 O(1) 时间复杂度",
|
| 22 |
+
"实现一个简单的 Stack 数据结构,支持 push, pop, peek, is_empty 方法",
|
| 23 |
+
"编写一个配置管理器,支持从 YAML/JSON 加载,支持点号路径访问如 config.get('db.host')",
|
| 24 |
+
"实现一个令牌桶限流器 TokenBucketRateLimiter,支持 acquire() 和装饰器用法",
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def run_agents(requirement: str, api_key: str, provider: str, progress=gr.Progress()):
|
| 29 |
+
if not api_key.strip():
|
| 30 |
+
return "请输入 API Key", "", ""
|
| 31 |
+
|
| 32 |
+
os.environ["OPENAI_API_KEY"] = api_key.strip()
|
| 33 |
+
|
| 34 |
+
settings = load_settings()
|
| 35 |
+
agents_config = load_agents_config()
|
| 36 |
+
|
| 37 |
+
llm = LLMClient.from_settings(provider, settings)
|
| 38 |
+
planner = PlannerAgent(agents_config["planner"], llm)
|
| 39 |
+
coder = CoderAgent(agents_config["coder"], llm, mcp_manager=None)
|
| 40 |
+
reviewer = ReviewerAgent(agents_config["reviewer"], llm)
|
| 41 |
+
orchestrator = Orchestrator(
|
| 42 |
+
planner=planner, coder=coder, reviewer=reviewer,
|
| 43 |
+
config=settings["orchestrator"],
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
progress(0.1, desc="Planner 正在拆解任务...")
|
| 47 |
+
start = time.time()
|
| 48 |
+
result = await orchestrator.run(requirement)
|
| 49 |
+
elapsed = time.time() - start
|
| 50 |
+
|
| 51 |
+
code_blocks = []
|
| 52 |
+
log_lines = []
|
| 53 |
+
|
| 54 |
+
log_lines.append(f"**任务拆分**: {len(result.plan)} 个子任务")
|
| 55 |
+
for i, task in enumerate(result.plan):
|
| 56 |
+
log_lines.append(f" T{i+1}: {task['description'][:80]}")
|
| 57 |
+
log_lines.append("")
|
| 58 |
+
|
| 59 |
+
for i, r in enumerate(result.results):
|
| 60 |
+
score = r["review"]["score"] if r.get("review") else "N/A"
|
| 61 |
+
status_icon = "✅" if r["status"] == "completed" else "⚠️"
|
| 62 |
+
log_lines.append(
|
| 63 |
+
f"{status_icon} **Task {i+1}**: score={score}/10, "
|
| 64 |
+
f"attempts={r['attempts']}, status={r['status']}"
|
| 65 |
+
)
|
| 66 |
+
if r.get("code"):
|
| 67 |
+
code_blocks.append(r["code"])
|
| 68 |
+
|
| 69 |
+
log_lines.append("")
|
| 70 |
+
log_lines.append(f"**总 Token**: {result.total_tokens:,}")
|
| 71 |
+
log_lines.append(f"**耗时**: {elapsed:.1f}s")
|
| 72 |
+
|
| 73 |
+
completed = sum(1 for r in result.results if r["status"] == "completed")
|
| 74 |
+
scores = [r["review"]["score"] for r in result.results if r.get("review")]
|
| 75 |
+
avg_score = sum(scores) / len(scores) if scores else 0
|
| 76 |
+
|
| 77 |
+
stats = json.dumps({
|
| 78 |
+
"completion_rate": f"{completed}/{len(result.results)}",
|
| 79 |
+
"avg_score": round(avg_score, 1),
|
| 80 |
+
"total_tokens": result.total_tokens,
|
| 81 |
+
"elapsed_seconds": round(elapsed, 1),
|
| 82 |
+
}, indent=2, ensure_ascii=False)
|
| 83 |
+
|
| 84 |
+
return "\n\n".join(code_blocks), "\n".join(log_lines), stats
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def create_demo():
|
| 88 |
+
with gr.Blocks(title="CodeAgent-MCP", theme=gr.themes.Soft()) as demo:
|
| 89 |
+
gr.Markdown(
|
| 90 |
+
"# 🤖 CodeAgent-MCP\n"
|
| 91 |
+
"**Multi-Agent Code Generation System** — "
|
| 92 |
+
"Planner (任务拆解) → Coder (代码生成) → Reviewer (代码审查) 反馈循环\n\n"
|
| 93 |
+
"基于 MCP 协议的多 Agent 协作代码开发系统。"
|
| 94 |
+
)
|
| 95 |
+
|
| 96 |
+
with gr.Row():
|
| 97 |
+
with gr.Column(scale=1):
|
| 98 |
+
api_key = gr.Textbox(
|
| 99 |
+
label="API Key (DeepSeek / OpenAI)",
|
| 100 |
+
type="password",
|
| 101 |
+
placeholder="sk-...",
|
| 102 |
+
)
|
| 103 |
+
provider = gr.Dropdown(
|
| 104 |
+
choices=["default", "siliconflow", "openai"],
|
| 105 |
+
value="default",
|
| 106 |
+
label="LLM Provider",
|
| 107 |
+
)
|
| 108 |
+
requirement = gr.Textbox(
|
| 109 |
+
label="开发需求",
|
| 110 |
+
placeholder="请描述你想实现的功能...",
|
| 111 |
+
lines=3,
|
| 112 |
+
)
|
| 113 |
+
examples = gr.Examples(
|
| 114 |
+
examples=[[e] for e in EXAMPLE_TASKS],
|
| 115 |
+
inputs=[requirement],
|
| 116 |
+
)
|
| 117 |
+
run_btn = gr.Button("🚀 开始生成", variant="primary")
|
| 118 |
+
|
| 119 |
+
with gr.Column(scale=2):
|
| 120 |
+
code_output = gr.Markdown(label="生成的代码")
|
| 121 |
+
with gr.Row():
|
| 122 |
+
log_output = gr.Markdown(label="执行日志")
|
| 123 |
+
stats_output = gr.Code(label="统计数据", language="json")
|
| 124 |
+
|
| 125 |
+
run_btn.click(
|
| 126 |
+
fn=run_agents,
|
| 127 |
+
inputs=[requirement, api_key, provider],
|
| 128 |
+
outputs=[code_output, log_output, stats_output],
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
gr.Markdown(
|
| 132 |
+
"---\n"
|
| 133 |
+
"**架构**: 自研 300 行编排器,不依赖 LangChain | "
|
| 134 |
+
"**项目系列**: small-llms-tool-use → agenttune → smallrag → CodeAgent-MCP\n\n"
|
| 135 |
+
"[GitHub](https://github.com/XIECHENG6/CodeAgent-MCP)"
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
return demo
|
| 139 |
+
|
| 140 |
+
|
| 141 |
+
if __name__ == "__main__":
|
| 142 |
+
demo = create_demo()
|
| 143 |
+
demo.launch()
|
config/agents.yaml
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
planner:
|
| 2 |
+
name: "Planner Agent"
|
| 3 |
+
provider: "default"
|
| 4 |
+
temperature: 0.3
|
| 5 |
+
system_prompt: |
|
| 6 |
+
你是一个经验丰富的技术负责人。你的职责是:
|
| 7 |
+
1. 分析用户的开发需求
|
| 8 |
+
2. 将需求拆解为具体的、可执行的子任务列表
|
| 9 |
+
3. 为每个子任务标注依赖关系和执行顺序
|
| 10 |
+
|
| 11 |
+
输出格式要求(严格JSON):
|
| 12 |
+
{
|
| 13 |
+
"tasks": [
|
| 14 |
+
{
|
| 15 |
+
"task_id": "T1",
|
| 16 |
+
"description": "具体要做什么",
|
| 17 |
+
"dependencies": []
|
| 18 |
+
}
|
| 19 |
+
]
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
注意:
|
| 23 |
+
- 每个子任务应该足够具体,能被一个开发者独立完成
|
| 24 |
+
- 合理安排依赖关系,无依赖的任务排在前面
|
| 25 |
+
- 不要拆分过细,2-4个子任务为宜
|
| 26 |
+
- 同一个类/模块的方法应合并为一个任务,不要逐个方法拆分
|
| 27 |
+
- 典型拆分:T1=核心实现(所有类和方法), T2=测试, T3=集成验证(可选)
|
| 28 |
+
|
| 29 |
+
coder:
|
| 30 |
+
name: "Coder Agent"
|
| 31 |
+
provider: "default"
|
| 32 |
+
temperature: 0.5
|
| 33 |
+
max_tool_rounds: 10
|
| 34 |
+
system_prompt: |
|
| 35 |
+
你是一个高级Python开发工程师。根据任务描述编写高质量的 Python 代码,包含类型注解,遵循 PEP 8。
|
| 36 |
+
|
| 37 |
+
当你有工具可用时,严格按以下顺序操作:
|
| 38 |
+
第1步:如果 workspace 有已有文件,file_read 需要参考的文件
|
| 39 |
+
第2步:用 file_write 写入主源码文件(一次调用写完整个文件)
|
| 40 |
+
第3步:用 file_write 写入测试文件
|
| 41 |
+
第4步(可选):用 shell_exec 运行 pytest 验证
|
| 42 |
+
|
| 43 |
+
禁止事项:
|
| 44 |
+
- 不要在空目录调用 file_list 或 file_search
|
| 45 |
+
- 不要写完文件后再 file_read 自己刚写的文件
|
| 46 |
+
- 不要多次小片段追加写入,一次 file_write 写完整个文件
|
| 47 |
+
|
| 48 |
+
完成后简要说明做了什么。
|
| 49 |
+
|
| 50 |
+
reviewer:
|
| 51 |
+
name: "Reviewer Agent"
|
| 52 |
+
provider: "default"
|
| 53 |
+
temperature: 0.2
|
| 54 |
+
system_prompt: |
|
| 55 |
+
你是一个资深代码审查专家。你的职责是:
|
| 56 |
+
1. 审查 Coder 生成的代码
|
| 57 |
+
2. 从以下维度评分(0-10):
|
| 58 |
+
- 正确性:逻辑是否正确,边界条件是否处理
|
| 59 |
+
- 可读性:命名、结构是否清晰
|
| 60 |
+
- 健壮性:异常处理、输入校验是否完善
|
| 61 |
+
- 测试覆盖:是否有足够的测试
|
| 62 |
+
3. 给出综合评分和具体改进建议
|
| 63 |
+
|
| 64 |
+
输出格式(严格JSON):
|
| 65 |
+
{
|
| 66 |
+
"score": 8.5,
|
| 67 |
+
"passed": true,
|
| 68 |
+
"issues": ["issue1", "issue2"],
|
| 69 |
+
"suggestions": ["suggestion1"],
|
| 70 |
+
"summary": "总体评价"
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
评分标准:>= 7.0 为通过,< 7.0 需要返回给 Coder 修改。
|
config/mcp_servers.yaml
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
servers:
|
| 2 |
+
file-server:
|
| 3 |
+
command: "python"
|
| 4 |
+
args: ["-m", "src.mcp.servers.file_server"]
|
| 5 |
+
description: "文件读写与搜索"
|
| 6 |
+
enabled: true
|
| 7 |
+
|
| 8 |
+
shell-server:
|
| 9 |
+
command: "python"
|
| 10 |
+
args: ["-m", "src.mcp.servers.shell_server"]
|
| 11 |
+
description: "Shell 命令执行 (受限安全沙箱)"
|
| 12 |
+
enabled: true
|
| 13 |
+
|
| 14 |
+
git-server:
|
| 15 |
+
command: "python"
|
| 16 |
+
args: ["-m", "src.mcp.servers.git_server"]
|
| 17 |
+
description: "Git 操作 (status/diff/commit/log)"
|
| 18 |
+
enabled: true
|
| 19 |
+
|
| 20 |
+
rag-server:
|
| 21 |
+
command: "python"
|
| 22 |
+
args: ["-m", "src.mcp.servers.rag_server"]
|
| 23 |
+
description: "代码知识库 RAG 检索 (需要 sentence-transformers + faiss)"
|
| 24 |
+
enabled: false
|
config/settings.yaml
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
providers:
|
| 2 |
+
default:
|
| 3 |
+
base_url: "https://api.deepseek.com"
|
| 4 |
+
model: "deepseek-chat"
|
| 5 |
+
temperature: 0.5
|
| 6 |
+
max_tokens: 4096
|
| 7 |
+
|
| 8 |
+
siliconflow:
|
| 9 |
+
base_url: "https://api.siliconflow.cn/v1"
|
| 10 |
+
model: "Qwen/Qwen2.5-72B-Instruct"
|
| 11 |
+
temperature: 0.5
|
| 12 |
+
max_tokens: 4096
|
| 13 |
+
|
| 14 |
+
openai:
|
| 15 |
+
base_url: "https://api.openai.com/v1"
|
| 16 |
+
model: "gpt-4o-mini"
|
| 17 |
+
temperature: 0.5
|
| 18 |
+
max_tokens: 4096
|
| 19 |
+
|
| 20 |
+
orchestrator:
|
| 21 |
+
max_review_rounds: 3
|
| 22 |
+
review_threshold: 7.0
|
| 23 |
+
skip_review_for_simple: false
|
| 24 |
+
|
| 25 |
+
logging:
|
| 26 |
+
level: "INFO"
|
| 27 |
+
show_tool_calls: true
|
| 28 |
+
show_token_usage: true
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
openai>=1.0
|
| 2 |
+
pydantic>=2.0
|
| 3 |
+
pyyaml>=6.0
|
| 4 |
+
rich>=13.0
|
| 5 |
+
gradio>=4.0
|
| 6 |
+
nest_asyncio
|
src/__init__.py
ADDED
|
File without changes
|
src/agents/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .planner import PlannerAgent
|
| 2 |
+
from .coder import CoderAgent
|
| 3 |
+
from .reviewer import ReviewerAgent
|
src/agents/coder.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..core.agent_base import AgentBase
|
| 2 |
+
from ..core.llm_client import LLMClient
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class CoderAgent(AgentBase):
|
| 6 |
+
def __init__(self, config: dict, llm_client: LLMClient, mcp_manager=None):
|
| 7 |
+
super().__init__(config, llm_client, mcp_manager)
|
| 8 |
+
self.workspace_files: list[str] = []
|
| 9 |
+
|
| 10 |
+
def set_workspace_files(self, files: list[str]):
|
| 11 |
+
self.workspace_files = files
|
| 12 |
+
|
| 13 |
+
def format_input(self, task) -> str:
|
| 14 |
+
if isinstance(task, str):
|
| 15 |
+
prompt = task
|
| 16 |
+
elif isinstance(task, dict):
|
| 17 |
+
desc = task.get("description", str(task))
|
| 18 |
+
deps = task.get("dependencies", [])
|
| 19 |
+
prompt = f"请完成以下任务:\n{desc}"
|
| 20 |
+
if deps:
|
| 21 |
+
prompt += f"\n\n依赖的前置任务: {', '.join(deps)}"
|
| 22 |
+
else:
|
| 23 |
+
prompt = str(task)
|
| 24 |
+
|
| 25 |
+
if self.mcp and self.workspace_files:
|
| 26 |
+
prompt += f"\n\nWorkspace 已有文件: {', '.join(self.workspace_files)}"
|
| 27 |
+
prompt += "\n可以用 file_read 读取已有文件作为参考,然后直接 file_write 写入新文件。"
|
| 28 |
+
elif self.mcp:
|
| 29 |
+
prompt += "\n\nWorkspace 为空,请直接用 file_write 写入代码文件,不需要先 file_list。"
|
| 30 |
+
|
| 31 |
+
return prompt
|
src/agents/planner.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..core.agent_base import AgentBase
|
| 2 |
+
from ..core.llm_client import LLMClient
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class PlannerAgent(AgentBase):
|
| 6 |
+
def __init__(self, config: dict, llm_client: LLMClient):
|
| 7 |
+
super().__init__(config, llm_client, mcp_manager=None)
|
| 8 |
+
|
| 9 |
+
def format_input(self, task) -> str:
|
| 10 |
+
if isinstance(task, str):
|
| 11 |
+
return task
|
| 12 |
+
return f"请分析以下开发需求并拆解为子任务:\n\n{task}"
|
src/agents/reviewer.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from ..core.agent_base import AgentBase
|
| 2 |
+
from ..core.llm_client import LLMClient
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class ReviewerAgent(AgentBase):
|
| 6 |
+
def __init__(self, config: dict, llm_client: LLMClient):
|
| 7 |
+
super().__init__(config, llm_client, mcp_manager=None)
|
| 8 |
+
|
| 9 |
+
def format_input(self, task) -> str:
|
| 10 |
+
if isinstance(task, dict) and "task" in task and "code" in task:
|
| 11 |
+
task_info = task["task"]
|
| 12 |
+
code = task["code"]
|
| 13 |
+
desc = task_info.get("description", str(task_info)) if isinstance(task_info, dict) else str(task_info)
|
| 14 |
+
return (
|
| 15 |
+
f"任务需求: {desc}\n\n"
|
| 16 |
+
f"Coder 的输出:\n{code}\n\n"
|
| 17 |
+
f"请审查代码质量并给出评分。"
|
| 18 |
+
)
|
| 19 |
+
return f"请审查以下代码:\n\n{task}"
|
src/core/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .message import Role, AgentType, Message, TaskItem, ReviewResult
|
| 2 |
+
from .llm_client import LLMClient
|
| 3 |
+
from .orchestrator import Orchestrator
|
src/core/agent_base.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
from abc import ABC, abstractmethod
|
| 4 |
+
|
| 5 |
+
from .llm_client import LLMClient
|
| 6 |
+
|
| 7 |
+
logger = logging.getLogger(__name__)
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class AgentBase(ABC):
|
| 11 |
+
def __init__(self, config: dict, llm_client: LLMClient, mcp_manager=None):
|
| 12 |
+
self.name = config["name"]
|
| 13 |
+
self.system_prompt = config["system_prompt"]
|
| 14 |
+
self.llm = llm_client
|
| 15 |
+
self.mcp = mcp_manager
|
| 16 |
+
self.max_tool_rounds = config.get("max_tool_rounds", 5)
|
| 17 |
+
self.max_tool_result_chars = config.get("max_tool_result_chars", 8000)
|
| 18 |
+
self.conversation: list[dict] = []
|
| 19 |
+
self.total_tokens_used = 0
|
| 20 |
+
|
| 21 |
+
async def run(self, user_input: str) -> str:
|
| 22 |
+
self.conversation = [
|
| 23 |
+
{"role": "system", "content": self.system_prompt},
|
| 24 |
+
{"role": "user", "content": user_input},
|
| 25 |
+
]
|
| 26 |
+
|
| 27 |
+
tools = self.mcp.get_openai_tools() if self.mcp else None
|
| 28 |
+
|
| 29 |
+
for round_idx in range(self.max_tool_rounds):
|
| 30 |
+
response = await self.llm.chat(
|
| 31 |
+
messages=self.conversation,
|
| 32 |
+
tools=tools,
|
| 33 |
+
)
|
| 34 |
+
self.total_tokens_used += (
|
| 35 |
+
response["usage"]["prompt_tokens"] + response["usage"]["completion_tokens"]
|
| 36 |
+
)
|
| 37 |
+
|
| 38 |
+
if not response["tool_calls"]:
|
| 39 |
+
return response["content"]
|
| 40 |
+
|
| 41 |
+
assistant_msg = {"role": "assistant", "content": response["content"]}
|
| 42 |
+
assistant_msg["tool_calls"] = [
|
| 43 |
+
{
|
| 44 |
+
"id": tc["id"],
|
| 45 |
+
"type": "function",
|
| 46 |
+
"function": {
|
| 47 |
+
"name": tc["function"],
|
| 48 |
+
"arguments": json.dumps(tc["arguments"], ensure_ascii=False),
|
| 49 |
+
},
|
| 50 |
+
}
|
| 51 |
+
for tc in response["tool_calls"]
|
| 52 |
+
]
|
| 53 |
+
self.conversation.append(assistant_msg)
|
| 54 |
+
|
| 55 |
+
for tc in response["tool_calls"]:
|
| 56 |
+
tool_result = await self._execute_tool(tc["function"], tc["arguments"])
|
| 57 |
+
self.conversation.append({
|
| 58 |
+
"role": "tool",
|
| 59 |
+
"tool_call_id": tc["id"],
|
| 60 |
+
"content": tool_result,
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
logger.info(f"[{self.name}] Round {round_idx + 1}: "
|
| 64 |
+
f"called {[tc['function'] for tc in response['tool_calls']]}")
|
| 65 |
+
|
| 66 |
+
self.conversation.append({
|
| 67 |
+
"role": "user",
|
| 68 |
+
"content": "你已达到最大工具调用轮次,请基于当前进度给出最终回答。",
|
| 69 |
+
})
|
| 70 |
+
response = await self.llm.chat(messages=self.conversation)
|
| 71 |
+
return response["content"]
|
| 72 |
+
|
| 73 |
+
async def _execute_tool(self, tool_name: str, arguments: dict) -> str:
|
| 74 |
+
if not self.mcp:
|
| 75 |
+
return f"Error: No MCP manager available to execute tool '{tool_name}'"
|
| 76 |
+
try:
|
| 77 |
+
result = await self.mcp.call_tool(tool_name, arguments)
|
| 78 |
+
text = str(result)
|
| 79 |
+
if len(text) > self.max_tool_result_chars:
|
| 80 |
+
text = text[:self.max_tool_result_chars] + f"\n... (truncated, {len(str(result))} chars total)"
|
| 81 |
+
return text
|
| 82 |
+
except Exception as e:
|
| 83 |
+
logger.error(f"[{self.name}] Tool '{tool_name}' failed: {e}")
|
| 84 |
+
return f"工具调用失败: {type(e).__name__}: {str(e)}"
|
| 85 |
+
|
| 86 |
+
@abstractmethod
|
| 87 |
+
def format_input(self, task) -> str:
|
| 88 |
+
pass
|
src/core/config.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from pathlib import Path
|
| 3 |
+
import yaml
|
| 4 |
+
|
| 5 |
+
PROJECT_ROOT = Path(__file__).parent.parent.parent
|
| 6 |
+
CONFIG_DIR = PROJECT_ROOT / "config"
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
def load_yaml(path: Path) -> dict:
|
| 10 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 11 |
+
return yaml.safe_load(f)
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def load_settings() -> dict:
|
| 15 |
+
return load_yaml(CONFIG_DIR / "settings.yaml")
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def load_agents_config() -> dict:
|
| 19 |
+
return load_yaml(CONFIG_DIR / "agents.yaml")
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def load_mcp_config() -> dict:
|
| 23 |
+
return load_yaml(CONFIG_DIR / "mcp_servers.yaml")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def get_project_root() -> Path:
|
| 27 |
+
return PROJECT_ROOT
|
src/core/llm_client.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import logging
|
| 4 |
+
from openai import AsyncOpenAI
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class LLMClient:
|
| 10 |
+
def __init__(self, config: dict):
|
| 11 |
+
self.client = AsyncOpenAI(
|
| 12 |
+
api_key=config.get("api_key") or os.getenv("OPENAI_API_KEY"),
|
| 13 |
+
base_url=config.get("base_url", "https://api.deepseek.com"),
|
| 14 |
+
)
|
| 15 |
+
self.model = config.get("model", "deepseek-chat")
|
| 16 |
+
self.temperature = config.get("temperature", 0.5)
|
| 17 |
+
self.max_tokens = config.get("max_tokens", 4096)
|
| 18 |
+
|
| 19 |
+
@classmethod
|
| 20 |
+
def from_settings(cls, provider: str = "default", settings: dict | None = None) -> "LLMClient":
|
| 21 |
+
if settings is None:
|
| 22 |
+
from .config import load_settings
|
| 23 |
+
settings = load_settings()
|
| 24 |
+
provider_config = settings["providers"][provider]
|
| 25 |
+
return cls(provider_config)
|
| 26 |
+
|
| 27 |
+
async def chat(
|
| 28 |
+
self,
|
| 29 |
+
messages: list[dict],
|
| 30 |
+
tools: list[dict] | None = None,
|
| 31 |
+
tool_choice: str = "auto",
|
| 32 |
+
temperature: float | None = None,
|
| 33 |
+
) -> dict:
|
| 34 |
+
kwargs = {
|
| 35 |
+
"model": self.model,
|
| 36 |
+
"messages": messages,
|
| 37 |
+
"temperature": temperature or self.temperature,
|
| 38 |
+
"max_tokens": self.max_tokens,
|
| 39 |
+
}
|
| 40 |
+
if tools:
|
| 41 |
+
kwargs["tools"] = tools
|
| 42 |
+
kwargs["tool_choice"] = tool_choice
|
| 43 |
+
|
| 44 |
+
response = await self.client.chat.completions.create(**kwargs)
|
| 45 |
+
choice = response.choices[0]
|
| 46 |
+
|
| 47 |
+
result = {
|
| 48 |
+
"content": choice.message.content or "",
|
| 49 |
+
"tool_calls": [],
|
| 50 |
+
"usage": {
|
| 51 |
+
"prompt_tokens": response.usage.prompt_tokens if response.usage else 0,
|
| 52 |
+
"completion_tokens": response.usage.completion_tokens if response.usage else 0,
|
| 53 |
+
},
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if choice.message.tool_calls:
|
| 57 |
+
for tc in choice.message.tool_calls:
|
| 58 |
+
arguments = self._safe_parse_arguments(tc.function.arguments)
|
| 59 |
+
result["tool_calls"].append({
|
| 60 |
+
"id": tc.id,
|
| 61 |
+
"function": tc.function.name,
|
| 62 |
+
"arguments": arguments,
|
| 63 |
+
})
|
| 64 |
+
|
| 65 |
+
logger.debug(
|
| 66 |
+
f"LLM call: model={self.model}, "
|
| 67 |
+
f"tokens={result['usage']['prompt_tokens']}+{result['usage']['completion_tokens']}"
|
| 68 |
+
)
|
| 69 |
+
return result
|
| 70 |
+
|
| 71 |
+
def _safe_parse_arguments(self, raw: str) -> dict:
|
| 72 |
+
try:
|
| 73 |
+
return json.loads(raw)
|
| 74 |
+
except json.JSONDecodeError:
|
| 75 |
+
import re
|
| 76 |
+
match = re.search(r'\{.*\}', raw, re.DOTALL)
|
| 77 |
+
if match:
|
| 78 |
+
try:
|
| 79 |
+
return json.loads(match.group())
|
| 80 |
+
except json.JSONDecodeError:
|
| 81 |
+
pass
|
| 82 |
+
logger.warning(f"Failed to parse tool arguments: {raw[:200]}")
|
| 83 |
+
return {"_raw": raw}
|
src/core/message.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, field
|
| 2 |
+
from enum import Enum
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class Role(Enum):
|
| 7 |
+
SYSTEM = "system"
|
| 8 |
+
USER = "user"
|
| 9 |
+
ASSISTANT = "assistant"
|
| 10 |
+
TOOL = "tool"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class AgentType(Enum):
|
| 14 |
+
PLANNER = "planner"
|
| 15 |
+
CODER = "coder"
|
| 16 |
+
REVIEWER = "reviewer"
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
@dataclass
|
| 20 |
+
class Message:
|
| 21 |
+
role: Role
|
| 22 |
+
content: str
|
| 23 |
+
tool_call_id: Optional[str] = None
|
| 24 |
+
name: Optional[str] = None
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
@dataclass
|
| 28 |
+
class TaskItem:
|
| 29 |
+
task_id: str
|
| 30 |
+
description: str
|
| 31 |
+
status: str = "pending"
|
| 32 |
+
dependencies: list[str] = field(default_factory=list)
|
| 33 |
+
result: Optional[str] = None
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
@dataclass
|
| 37 |
+
class ReviewResult:
|
| 38 |
+
score: float
|
| 39 |
+
passed: bool
|
| 40 |
+
issues: list[str] = field(default_factory=list)
|
| 41 |
+
suggestions: list[str] = field(default_factory=list)
|
| 42 |
+
summary: str = ""
|
src/core/orchestrator.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import logging
|
| 3 |
+
import re
|
| 4 |
+
from dataclasses import dataclass, field
|
| 5 |
+
|
| 6 |
+
logger = logging.getLogger(__name__)
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass
|
| 10 |
+
class ExecutionResult:
|
| 11 |
+
plan: list[dict]
|
| 12 |
+
results: list[dict]
|
| 13 |
+
execution_log: list[dict]
|
| 14 |
+
total_tokens: int = 0
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class Orchestrator:
|
| 18 |
+
def __init__(self, planner, coder, reviewer, config: dict):
|
| 19 |
+
self.planner = planner
|
| 20 |
+
self.coder = coder
|
| 21 |
+
self.reviewer = reviewer
|
| 22 |
+
self.max_review_rounds = config.get("max_review_rounds", 3)
|
| 23 |
+
self.review_threshold = config.get("review_threshold", 7.0)
|
| 24 |
+
self.execution_log: list[dict] = []
|
| 25 |
+
|
| 26 |
+
async def run(self, user_requirement: str) -> ExecutionResult:
|
| 27 |
+
self.execution_log = []
|
| 28 |
+
|
| 29 |
+
plan_output = await self.planner.run(user_requirement)
|
| 30 |
+
tasks = self._parse_plan(plan_output)
|
| 31 |
+
self._log("plan", {"raw_output": plan_output, "parsed_tasks": tasks})
|
| 32 |
+
|
| 33 |
+
logger.info(f"[Orchestrator] Plan: {len(tasks)} tasks")
|
| 34 |
+
|
| 35 |
+
results = []
|
| 36 |
+
for i, task in enumerate(tasks):
|
| 37 |
+
logger.info(f"[Orchestrator] Executing task {i+1}/{len(tasks)}: {task['description'][:60]}")
|
| 38 |
+
task_result = await self._execute_task(task)
|
| 39 |
+
results.append(task_result)
|
| 40 |
+
|
| 41 |
+
total_tokens = (
|
| 42 |
+
self.planner.total_tokens_used
|
| 43 |
+
+ self.coder.total_tokens_used
|
| 44 |
+
+ self.reviewer.total_tokens_used
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
return ExecutionResult(
|
| 48 |
+
plan=tasks,
|
| 49 |
+
results=results,
|
| 50 |
+
execution_log=self.execution_log,
|
| 51 |
+
total_tokens=total_tokens,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
async def _execute_task(self, task: dict) -> dict:
|
| 55 |
+
code_output = None
|
| 56 |
+
review = None
|
| 57 |
+
|
| 58 |
+
for attempt in range(self.max_review_rounds):
|
| 59 |
+
await self._sync_workspace_files()
|
| 60 |
+
|
| 61 |
+
if attempt == 0:
|
| 62 |
+
coder_input = self.coder.format_input(task)
|
| 63 |
+
else:
|
| 64 |
+
coder_input = self.coder.format_input(task)
|
| 65 |
+
coder_input += (
|
| 66 |
+
f"\n\n--- Reviewer 反馈 (得分: {review['score']}/10) ---\n"
|
| 67 |
+
f"问题: {json.dumps(review['issues'], ensure_ascii=False)}\n"
|
| 68 |
+
f"建议: {json.dumps(review['suggestions'], ensure_ascii=False)}\n"
|
| 69 |
+
f"请根据反馈修改代码。如果 workspace 已有文件,用 file_read 读取后修改再 file_write 写回。"
|
| 70 |
+
)
|
| 71 |
+
|
| 72 |
+
code_output = await self.coder.run(coder_input)
|
| 73 |
+
self._log("coder", {"task_id": task.get("task_id"), "attempt": attempt, "output_preview": code_output[:500]})
|
| 74 |
+
|
| 75 |
+
reviewer_input = self.reviewer.format_input({
|
| 76 |
+
"task": task,
|
| 77 |
+
"code": code_output,
|
| 78 |
+
})
|
| 79 |
+
review_raw = await self.reviewer.run(reviewer_input)
|
| 80 |
+
review = self._parse_review(review_raw)
|
| 81 |
+
self._log("reviewer", {"task_id": task.get("task_id"), "attempt": attempt, "review": review})
|
| 82 |
+
|
| 83 |
+
logger.info(f" [Review] Attempt {attempt+1}: score={review['score']}, passed={review['passed']}")
|
| 84 |
+
|
| 85 |
+
if review["passed"]:
|
| 86 |
+
return {
|
| 87 |
+
"task": task,
|
| 88 |
+
"code": code_output,
|
| 89 |
+
"review": review,
|
| 90 |
+
"attempts": attempt + 1,
|
| 91 |
+
"status": "completed",
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
return {
|
| 95 |
+
"task": task,
|
| 96 |
+
"code": code_output,
|
| 97 |
+
"review": review,
|
| 98 |
+
"attempts": self.max_review_rounds,
|
| 99 |
+
"status": "max_attempts_reached",
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
def _parse_plan(self, plan_text: str) -> list[dict]:
|
| 103 |
+
parsed = self._extract_json(plan_text)
|
| 104 |
+
if parsed and "tasks" in parsed:
|
| 105 |
+
return parsed["tasks"]
|
| 106 |
+
|
| 107 |
+
tasks = []
|
| 108 |
+
lines = plan_text.strip().split("\n")
|
| 109 |
+
for i, line in enumerate(lines):
|
| 110 |
+
line = line.strip()
|
| 111 |
+
if re.match(r'^[\d]+[.)\-]', line):
|
| 112 |
+
desc = re.sub(r'^[\d]+[.)\-]\s*', '', line)
|
| 113 |
+
tasks.append({"task_id": f"T{i+1}", "description": desc, "dependencies": []})
|
| 114 |
+
|
| 115 |
+
if not tasks:
|
| 116 |
+
tasks = [{"task_id": "T1", "description": plan_text, "dependencies": []}]
|
| 117 |
+
|
| 118 |
+
return tasks
|
| 119 |
+
|
| 120 |
+
def _parse_review(self, review_text: str) -> dict:
|
| 121 |
+
parsed = self._extract_json(review_text)
|
| 122 |
+
if parsed and "score" in parsed:
|
| 123 |
+
parsed.setdefault("passed", parsed["score"] >= self.review_threshold)
|
| 124 |
+
parsed.setdefault("issues", [])
|
| 125 |
+
parsed.setdefault("suggestions", [])
|
| 126 |
+
parsed.setdefault("summary", "")
|
| 127 |
+
return parsed
|
| 128 |
+
|
| 129 |
+
score_match = re.search(r'(\d+\.?\d*)\s*/\s*10', review_text)
|
| 130 |
+
score = float(score_match.group(1)) if score_match else 5.0
|
| 131 |
+
|
| 132 |
+
return {
|
| 133 |
+
"score": score,
|
| 134 |
+
"passed": score >= self.review_threshold,
|
| 135 |
+
"issues": [],
|
| 136 |
+
"suggestions": [],
|
| 137 |
+
"summary": review_text[:200],
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
def _extract_json(self, text: str) -> dict | None:
|
| 141 |
+
if "```json" in text:
|
| 142 |
+
match = re.search(r'```json\s*(.*?)```', text, re.DOTALL)
|
| 143 |
+
if match:
|
| 144 |
+
try:
|
| 145 |
+
return json.loads(match.group(1).strip())
|
| 146 |
+
except json.JSONDecodeError:
|
| 147 |
+
pass
|
| 148 |
+
if "```" in text:
|
| 149 |
+
match = re.search(r'```\s*(.*?)```', text, re.DOTALL)
|
| 150 |
+
if match:
|
| 151 |
+
try:
|
| 152 |
+
return json.loads(match.group(1).strip())
|
| 153 |
+
except json.JSONDecodeError:
|
| 154 |
+
pass
|
| 155 |
+
try:
|
| 156 |
+
return json.loads(text)
|
| 157 |
+
except json.JSONDecodeError:
|
| 158 |
+
pass
|
| 159 |
+
match = re.search(r'\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}', text, re.DOTALL)
|
| 160 |
+
if match:
|
| 161 |
+
try:
|
| 162 |
+
return json.loads(match.group())
|
| 163 |
+
except json.JSONDecodeError:
|
| 164 |
+
pass
|
| 165 |
+
return None
|
| 166 |
+
|
| 167 |
+
async def _sync_workspace_files(self):
|
| 168 |
+
if not hasattr(self.coder, 'set_workspace_files'):
|
| 169 |
+
return
|
| 170 |
+
if not self.coder.mcp:
|
| 171 |
+
return
|
| 172 |
+
try:
|
| 173 |
+
result = await self.coder.mcp.call_tool("file_list", {"directory": "."})
|
| 174 |
+
text = str(result)
|
| 175 |
+
if text == "(empty directory)" or text.startswith("Error"):
|
| 176 |
+
self.coder.set_workspace_files([])
|
| 177 |
+
return
|
| 178 |
+
files = []
|
| 179 |
+
for line in text.split('\n'):
|
| 180 |
+
line = line.strip()
|
| 181 |
+
if not line or line.endswith('/'):
|
| 182 |
+
continue
|
| 183 |
+
name = re.sub(r'\s*\(\d+B\)\s*$', '', line)
|
| 184 |
+
if name:
|
| 185 |
+
files.append(name)
|
| 186 |
+
self.coder.set_workspace_files(files)
|
| 187 |
+
logger.info(f"[Orchestrator] Workspace files: {files}")
|
| 188 |
+
except Exception as e:
|
| 189 |
+
logger.debug(f"[Orchestrator] Could not list workspace: {e}")
|
| 190 |
+
self.coder.set_workspace_files([])
|
| 191 |
+
|
| 192 |
+
def _log(self, stage: str, data: dict):
|
| 193 |
+
self.execution_log.append({"stage": stage, **data})
|
src/utils/__init__.py
ADDED
|
File without changes
|
src/utils/logger.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
from rich.console import Console
|
| 5 |
+
from rich.logging import RichHandler
|
| 6 |
+
|
| 7 |
+
console = Console()
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def setup_logging(level: str = "INFO"):
|
| 11 |
+
logging.basicConfig(
|
| 12 |
+
level=getattr(logging, level.upper(), logging.INFO),
|
| 13 |
+
format="%(message)s",
|
| 14 |
+
datefmt="[%X]",
|
| 15 |
+
handlers=[RichHandler(console=console, rich_tracebacks=True, show_path=False)],
|
| 16 |
+
)
|
| 17 |
+
logging.getLogger("httpx").setLevel(logging.WARNING)
|
| 18 |
+
logging.getLogger("openai").setLevel(logging.WARNING)
|