Cheng-1 commited on
Commit
5b9f9a3
·
verified ·
1 Parent(s): 33833e2

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,13 +1,40 @@
1
  ---
2
- title: CodeAgent MCP
3
- emoji: 🏃
4
- colorFrom: green
5
- colorTo: indigo
6
  sdk: gradio
7
- sdk_version: 6.14.0
8
- python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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)