full-stack-coding-assistant-agent 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agents/__init__.py +0 -0
- agents/audit_agent.py +223 -0
- agents/backend_agent.py +179 -0
- agents/base_agent.py +406 -0
- agents/frontend_agent.py +148 -0
- agents/test_agent.py +155 -0
- coordinator/__init__.py +0 -0
- coordinator/coordinator.py +452 -0
- coordinator/dag.py +147 -0
- executor/__init__.py +0 -0
- executor/cb_integration.py +160 -0
- full_stack_coding_assistant_agent/__init__.py +6 -0
- full_stack_coding_assistant_agent/cli.py +10 -0
- full_stack_coding_assistant_agent/main.py +686 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/METADATA +849 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/RECORD +31 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/WHEEL +5 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/entry_points.txt +2 -0
- full_stack_coding_assistant_agent-0.1.0.dist-info/top_level.txt +7 -0
- model/__init__.py +0 -0
- model/config.py +62 -0
- model/model_router.py +150 -0
- storage/__init__.py +0 -0
- storage/context_db.py +274 -0
- utils/__init__.py +0 -0
- utils/agent_selector.py +243 -0
- utils/config_validator.py +143 -0
- utils/logger.py +95 -0
- utils/output_manager.py +1572 -0
- utils/pdf_reader.py +122 -0
- utils/version.py +188 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
"""
|
|
2
|
+
主协调器 - 负责任务拆解、调度和上下文管理
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from agents.audit_agent import AuditAgent
|
|
10
|
+
from agents.backend_agent import BackendAgent
|
|
11
|
+
from agents.frontend_agent import FrontendAgent
|
|
12
|
+
from agents.test_agent import TestAgent
|
|
13
|
+
from coordinator.dag import DAGScheduler
|
|
14
|
+
from model.model_router import ModelRouter
|
|
15
|
+
from storage.context_db import ContextDB
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class Coordinator:
|
|
19
|
+
"""主协调器"""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
config_path: str = "config.yaml",
|
|
24
|
+
db_path: Optional[str] = None,
|
|
25
|
+
output_dir: Optional[Path] = None,
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
初始化协调器
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
config_path: 配置文件路径
|
|
32
|
+
db_path: 数据库路径,None 则使用配置文件中的默认值
|
|
33
|
+
output_dir: 输出目录 Path,None 则不写入文件系统
|
|
34
|
+
"""
|
|
35
|
+
# 初始化核心组件
|
|
36
|
+
self.model_router = ModelRouter()
|
|
37
|
+
self.db = ContextDB(db_path)
|
|
38
|
+
self.dag = DAGScheduler()
|
|
39
|
+
self.config_path = config_path
|
|
40
|
+
self.output_dir = output_dir
|
|
41
|
+
|
|
42
|
+
# 初始化 Agents
|
|
43
|
+
self.agents = {
|
|
44
|
+
"frontend": FrontendAgent("frontend", self.model_router, self.db),
|
|
45
|
+
"backend": BackendAgent("backend", self.model_router, self.db),
|
|
46
|
+
"test": TestAgent("test", self.model_router, self.db),
|
|
47
|
+
"audit": AuditAgent("audit", self.model_router, self.db),
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# 将 output_dir 注入到所有 Agent
|
|
51
|
+
if output_dir is not None:
|
|
52
|
+
for agent in self.agents.values():
|
|
53
|
+
agent._set_output_dir(output_dir)
|
|
54
|
+
|
|
55
|
+
def reset_dag(self):
|
|
56
|
+
"""
|
|
57
|
+
重置 DAG 调度器,用于迭代模式
|
|
58
|
+
|
|
59
|
+
清空 DAG 的任务和状态,但保留 Agent 和数据库连接。
|
|
60
|
+
每次新的迭代前调用此方法。
|
|
61
|
+
"""
|
|
62
|
+
self.dag.reset()
|
|
63
|
+
|
|
64
|
+
def submit_iteration_task(
|
|
65
|
+
self,
|
|
66
|
+
description: str,
|
|
67
|
+
requirements: str = "",
|
|
68
|
+
agent_types: Optional[List[str]] = None,
|
|
69
|
+
) -> str:
|
|
70
|
+
"""
|
|
71
|
+
提交迭代任务,只创建指定的 Agent 任务
|
|
72
|
+
|
|
73
|
+
与 submit_task() 不同,此方法允许只运行部分 Agent,
|
|
74
|
+
用于交互模式下的智能 Agent 选择。
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
description: 任务描述(用户的新需求)
|
|
78
|
+
requirements: 详细需求
|
|
79
|
+
agent_types: Agent 类型列表,None 则运行全部
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
主任务 ID
|
|
83
|
+
"""
|
|
84
|
+
agent_types = agent_types or ["backend", "frontend", "test", "audit"]
|
|
85
|
+
agent_types_set = set(agent_types)
|
|
86
|
+
|
|
87
|
+
main_task_id = f"task_{uuid.uuid4().hex[:8]}"
|
|
88
|
+
self.db.create_task(main_task_id, description)
|
|
89
|
+
|
|
90
|
+
# 基础 context(所有任务共享)
|
|
91
|
+
base_context = {
|
|
92
|
+
"description": description,
|
|
93
|
+
"requirements": requirements,
|
|
94
|
+
"is_iteration": True,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# 收集已有代码上下文(从 output_dir 读取)
|
|
98
|
+
if self.output_dir is not None:
|
|
99
|
+
base_context["existing_backend_code"] = self._read_generated_code("backend")
|
|
100
|
+
base_context["existing_frontend_code"] = self._read_generated_code(
|
|
101
|
+
"frontend"
|
|
102
|
+
)
|
|
103
|
+
base_context["existing_test_code"] = self._read_generated_code("tests")
|
|
104
|
+
|
|
105
|
+
created_tasks: Dict[str, str] = {} # suffix -> task_id
|
|
106
|
+
|
|
107
|
+
def _add_task(suffix: str, agent_type: str, deps: List[str]):
|
|
108
|
+
"""添加任务到 DAG,记录到 created_tasks"""
|
|
109
|
+
task_id = f"{main_task_id}_{suffix}"
|
|
110
|
+
self.db.create_task(task_id, f"{description} - {suffix}")
|
|
111
|
+
self.dag.add_task(
|
|
112
|
+
task_id,
|
|
113
|
+
agent_type=agent_type,
|
|
114
|
+
dependencies=deps,
|
|
115
|
+
context={**base_context},
|
|
116
|
+
)
|
|
117
|
+
created_tasks[suffix] = task_id
|
|
118
|
+
return task_id
|
|
119
|
+
|
|
120
|
+
# 1. 后端任务(无依赖,如果选中)
|
|
121
|
+
if "backend" in agent_types_set:
|
|
122
|
+
_add_task("backend", "backend", [])
|
|
123
|
+
|
|
124
|
+
# 2. 前端任务(依赖 backend,如果选中)
|
|
125
|
+
if "frontend" in agent_types_set:
|
|
126
|
+
deps = [created_tasks["backend"]] if "backend" in created_tasks else []
|
|
127
|
+
_add_task("frontend", "frontend", deps)
|
|
128
|
+
|
|
129
|
+
# 3. 测试任务(依赖 backend + frontend,如果选中)
|
|
130
|
+
if "test" in agent_types_set:
|
|
131
|
+
deps = []
|
|
132
|
+
if "backend" in created_tasks:
|
|
133
|
+
deps.append(created_tasks["backend"])
|
|
134
|
+
if "frontend" in created_tasks:
|
|
135
|
+
deps.append(created_tasks["frontend"])
|
|
136
|
+
_add_task("test", "test", deps)
|
|
137
|
+
|
|
138
|
+
# 4. 审计任务(依赖所有其他任务,如果选中)
|
|
139
|
+
if "audit" in agent_types_set:
|
|
140
|
+
deps = []
|
|
141
|
+
for suffix in ["backend", "frontend", "test"]:
|
|
142
|
+
if suffix in created_tasks:
|
|
143
|
+
deps.append(created_tasks[suffix])
|
|
144
|
+
_add_task("audit", "audit", deps)
|
|
145
|
+
|
|
146
|
+
self._main_task_id = main_task_id
|
|
147
|
+
return main_task_id
|
|
148
|
+
|
|
149
|
+
def submit_task(
|
|
150
|
+
self,
|
|
151
|
+
description: str,
|
|
152
|
+
requirements: str = "",
|
|
153
|
+
is_iteration: bool = False,
|
|
154
|
+
existing_context: Optional[Dict] = None,
|
|
155
|
+
) -> str:
|
|
156
|
+
"""
|
|
157
|
+
提交任务 - 拆解任务并添加到 DAG
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
description: 任务描述
|
|
161
|
+
requirements: 详细需求
|
|
162
|
+
is_iteration: 是否为迭代模式
|
|
163
|
+
existing_context: 已有项目上下文(迭代模式时使用)
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
主任务 ID
|
|
167
|
+
"""
|
|
168
|
+
main_task_id = f"task_{uuid.uuid4().hex[:8]}"
|
|
169
|
+
self.db.create_task(main_task_id, description)
|
|
170
|
+
|
|
171
|
+
# 基础 context(所有任务共享)
|
|
172
|
+
base_context = {
|
|
173
|
+
"description": description,
|
|
174
|
+
"requirements": requirements,
|
|
175
|
+
"is_iteration": is_iteration,
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
# 迭代模式:注入已有代码上下文
|
|
179
|
+
if is_iteration and existing_context:
|
|
180
|
+
base_context["existing_backend_code"] = existing_context.get(
|
|
181
|
+
"backend_code", ""
|
|
182
|
+
)
|
|
183
|
+
base_context["existing_frontend_code"] = existing_context.get(
|
|
184
|
+
"frontend_code", ""
|
|
185
|
+
)
|
|
186
|
+
base_context["existing_test_code"] = existing_context.get("test_code", "")
|
|
187
|
+
|
|
188
|
+
# 1. 后端任务(无依赖)
|
|
189
|
+
backend_task_id = f"{main_task_id}_backend"
|
|
190
|
+
self.db.create_task(backend_task_id, f"{description} - 后端开发")
|
|
191
|
+
self.dag.add_task(
|
|
192
|
+
backend_task_id,
|
|
193
|
+
agent_type="backend",
|
|
194
|
+
dependencies=[],
|
|
195
|
+
context={**base_context},
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# 2. 前端任务(依赖后端完成,以获取 API 契约)
|
|
199
|
+
frontend_task_id = f"{main_task_id}_frontend"
|
|
200
|
+
self.db.create_task(frontend_task_id, f"{description} - 前端开发")
|
|
201
|
+
self.dag.add_task(
|
|
202
|
+
frontend_task_id,
|
|
203
|
+
agent_type="frontend",
|
|
204
|
+
dependencies=[backend_task_id],
|
|
205
|
+
context={**base_context},
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
# 3. 测试任务(依赖前后端完成)
|
|
209
|
+
test_task_id = f"{main_task_id}_test"
|
|
210
|
+
self.db.create_task(test_task_id, f"{description} - 测试")
|
|
211
|
+
self.dag.add_task(
|
|
212
|
+
test_task_id,
|
|
213
|
+
agent_type="test",
|
|
214
|
+
dependencies=[backend_task_id, frontend_task_id],
|
|
215
|
+
context={
|
|
216
|
+
**base_context,
|
|
217
|
+
"frontend_code": "",
|
|
218
|
+
"backend_code": "",
|
|
219
|
+
},
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# 4. 审计任务(异步,依赖所有任务完成)
|
|
223
|
+
audit_task_id = f"{main_task_id}_audit"
|
|
224
|
+
self.db.create_task(audit_task_id, f"{description} - 审计")
|
|
225
|
+
self.dag.add_task(
|
|
226
|
+
audit_task_id,
|
|
227
|
+
agent_type="audit",
|
|
228
|
+
dependencies=[backend_task_id, frontend_task_id, test_task_id],
|
|
229
|
+
context={**base_context},
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
# 保存 main_task_id 供 run() 使用
|
|
233
|
+
self._main_task_id = main_task_id
|
|
234
|
+
return main_task_id
|
|
235
|
+
|
|
236
|
+
def run(self, main_task_id: str) -> Dict:
|
|
237
|
+
"""
|
|
238
|
+
主运行循环 - DAG 调度执行
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
所有任务的执行结果,包含 token 用量汇总
|
|
242
|
+
"""
|
|
243
|
+
results = {}
|
|
244
|
+
|
|
245
|
+
while not self.dag.is_completed():
|
|
246
|
+
runnable = self.dag.get_runnable_tasks()
|
|
247
|
+
|
|
248
|
+
if not runnable:
|
|
249
|
+
if not self.dag.is_completed():
|
|
250
|
+
raise RuntimeError("任务调度死锁:没有可运行任务但 DAG 未完成")
|
|
251
|
+
break
|
|
252
|
+
|
|
253
|
+
for task_id in runnable:
|
|
254
|
+
agent_type = self.dag.tasks[task_id]["agent_type"]
|
|
255
|
+
agent = self.agents.get(agent_type)
|
|
256
|
+
|
|
257
|
+
if not agent:
|
|
258
|
+
self.dag.mark_failed(task_id)
|
|
259
|
+
continue
|
|
260
|
+
|
|
261
|
+
# 获取任务上下文
|
|
262
|
+
context = self.dag.get_task_context(task_id)
|
|
263
|
+
|
|
264
|
+
# 为测试任务注入前后端代码
|
|
265
|
+
if agent_type == "test":
|
|
266
|
+
context["frontend_code"] = self._read_generated_code("frontend")
|
|
267
|
+
context["backend_code"] = self._read_generated_code("backend")
|
|
268
|
+
|
|
269
|
+
# 为审计任务注入待审计代码
|
|
270
|
+
if agent_type == "audit":
|
|
271
|
+
context["code"] = (
|
|
272
|
+
self._read_generated_code("backend")
|
|
273
|
+
+ "\n"
|
|
274
|
+
+ self._read_generated_code("frontend")
|
|
275
|
+
)
|
|
276
|
+
context["file_path"] = str(self.output_dir or "unknown")
|
|
277
|
+
context["agent_type"] = agent_type
|
|
278
|
+
|
|
279
|
+
try:
|
|
280
|
+
result = agent.execute(task_id, context)
|
|
281
|
+
self.dag.tasks[task_id]["result"] = result
|
|
282
|
+
self.dag.mark_done(task_id)
|
|
283
|
+
results[task_id] = result
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
import traceback
|
|
287
|
+
|
|
288
|
+
from utils.logger import error as log_error
|
|
289
|
+
|
|
290
|
+
error_detail = traceback.format_exc()
|
|
291
|
+
log_error(f"任务 [{task_id}] 执行失败 (agent={agent_type}): {e}")
|
|
292
|
+
log_error(f"详细堆栈:\n{error_detail}")
|
|
293
|
+
self.dag.mark_failed(task_id)
|
|
294
|
+
self.db.update_task_status(task_id, "failed", error=str(e))
|
|
295
|
+
# 同步更新被级联标记失败的下游任务到数据库
|
|
296
|
+
for tid, info in self.dag.tasks.items():
|
|
297
|
+
if self.dag.status.get(tid) == "failed" and tid != task_id:
|
|
298
|
+
self.db.update_task_status(
|
|
299
|
+
tid,
|
|
300
|
+
"failed",
|
|
301
|
+
error=f"上游任务 {task_id} 失败,本级联取消",
|
|
302
|
+
)
|
|
303
|
+
results[task_id] = {
|
|
304
|
+
"status": "failed",
|
|
305
|
+
"error": str(e),
|
|
306
|
+
"traceback": error_detail,
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
# 收集并汇总所有 Agent 的 token 用量
|
|
310
|
+
usage_summary = self._collect_usage_summary()
|
|
311
|
+
results["_usage_summary"] = usage_summary
|
|
312
|
+
|
|
313
|
+
# 打印用量汇总
|
|
314
|
+
self._print_usage_summary(usage_summary)
|
|
315
|
+
|
|
316
|
+
# 保存用量汇总文件
|
|
317
|
+
self._save_usage_summary(usage_summary)
|
|
318
|
+
|
|
319
|
+
return results
|
|
320
|
+
|
|
321
|
+
def _collect_usage_summary(self) -> List[Dict]:
|
|
322
|
+
"""收集所有 Agent 的 token 用量"""
|
|
323
|
+
summaries = []
|
|
324
|
+
for agent_type, agent in self.agents.items():
|
|
325
|
+
summary = agent.get_usage_summary()
|
|
326
|
+
if summary["total_tokens"] > 0:
|
|
327
|
+
summaries.append(summary)
|
|
328
|
+
return summaries
|
|
329
|
+
|
|
330
|
+
def _print_usage_summary(self, summaries: List[Dict]):
|
|
331
|
+
"""打印 token 用量汇总表"""
|
|
332
|
+
if not summaries:
|
|
333
|
+
return
|
|
334
|
+
|
|
335
|
+
from utils.logger import info as log_info
|
|
336
|
+
|
|
337
|
+
total_tokens = sum(s["total_tokens"] for s in summaries)
|
|
338
|
+
total_latency = sum(s["total_latency_ms"] for s in summaries)
|
|
339
|
+
|
|
340
|
+
log_info("=" * 72)
|
|
341
|
+
log_info("📊 Token 用量汇总")
|
|
342
|
+
log_info(
|
|
343
|
+
f"{'Agent':<12} {'Steps':<8} {'Prompt':<12} {'Completion':<14} {'Total':<12} {'耗时(ms)'}"
|
|
344
|
+
)
|
|
345
|
+
log_info("-" * 72)
|
|
346
|
+
for s in summaries:
|
|
347
|
+
log_info(
|
|
348
|
+
f"{s['agent_type']:<12} "
|
|
349
|
+
f"{s['steps']:<8} "
|
|
350
|
+
f"{s['total_prompt_tokens']:<12,} "
|
|
351
|
+
f"{s['total_completion_tokens']:<14,} "
|
|
352
|
+
f"{s['total_tokens']:<12,} "
|
|
353
|
+
f"{s['total_latency_ms']:.0f}"
|
|
354
|
+
)
|
|
355
|
+
log_info("-" * 72)
|
|
356
|
+
log_info(
|
|
357
|
+
f"{'合计':<12} {'—':<8} {'—':<12} {'—':<14} {total_tokens:<12,} {total_latency:.0f}"
|
|
358
|
+
)
|
|
359
|
+
log_info("=" * 72)
|
|
360
|
+
|
|
361
|
+
def _save_usage_summary(self, summaries: List[Dict]):
|
|
362
|
+
"""保存 token 用量汇总到文件"""
|
|
363
|
+
if not summaries or self.output_dir is None:
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
total_tokens = sum(s["total_tokens"] for s in summaries)
|
|
367
|
+
total_latency = sum(s["total_latency_ms"] for s in summaries)
|
|
368
|
+
|
|
369
|
+
lines = [
|
|
370
|
+
"# Token 用量汇总",
|
|
371
|
+
"",
|
|
372
|
+
f"- **总 Token 数**: {total_tokens:,}",
|
|
373
|
+
f"- **总耗时**: {total_latency:.0f} ms",
|
|
374
|
+
"",
|
|
375
|
+
"## 各 Agent 明细",
|
|
376
|
+
"",
|
|
377
|
+
"| Agent | 调用次数 | Prompt Tokens | Completion Tokens | Total Tokens | 耗时(ms) |",
|
|
378
|
+
"|-------|---------|---------------|-------------------|--------------|----------|",
|
|
379
|
+
]
|
|
380
|
+
for s in summaries:
|
|
381
|
+
lines.append(
|
|
382
|
+
f"| {s['agent_type']} "
|
|
383
|
+
f"| {s['steps']} "
|
|
384
|
+
f"| {s['total_prompt_tokens']:,} "
|
|
385
|
+
f"| {s['total_completion_tokens']:,} "
|
|
386
|
+
f"| {s['total_tokens']:,} "
|
|
387
|
+
f"| {s['total_latency_ms']:.0f} |"
|
|
388
|
+
)
|
|
389
|
+
lines.append(
|
|
390
|
+
f"| **合计** | — | — | — | **{total_tokens:,}** | **{total_latency:.0f}** |"
|
|
391
|
+
)
|
|
392
|
+
lines.append("")
|
|
393
|
+
lines.append("## 每步明细")
|
|
394
|
+
lines.append("")
|
|
395
|
+
|
|
396
|
+
for s in summaries:
|
|
397
|
+
lines.append(f"### {s['agent_type']}")
|
|
398
|
+
lines.append("")
|
|
399
|
+
lines.append("| Step | 模型 | Prompt | Completion | Total | 耗时(ms) |")
|
|
400
|
+
lines.append("|------|------|--------|------------|-------|----------|")
|
|
401
|
+
for d in s.get("details", []):
|
|
402
|
+
lines.append(
|
|
403
|
+
f"| {d['step']} | {d['model']} "
|
|
404
|
+
f"| {d['prompt_tokens']:,} "
|
|
405
|
+
f"| {d['completion_tokens']:,} "
|
|
406
|
+
f"| {d['total_tokens']:,} "
|
|
407
|
+
f"| {d['latency_ms']} |"
|
|
408
|
+
)
|
|
409
|
+
lines.append("")
|
|
410
|
+
|
|
411
|
+
(self.output_dir / "usage_summary.md").write_text(
|
|
412
|
+
"\n".join(lines), encoding="utf-8"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
def _read_generated_code(self, sub_dir: str) -> str:
|
|
416
|
+
"""
|
|
417
|
+
读取输出目录下指定子目录的所有代码,
|
|
418
|
+
用于注入到下游 Agent 的 context
|
|
419
|
+
"""
|
|
420
|
+
if self.output_dir is None:
|
|
421
|
+
return ""
|
|
422
|
+
target_dir = self.output_dir / sub_dir
|
|
423
|
+
if not target_dir.exists():
|
|
424
|
+
return ""
|
|
425
|
+
|
|
426
|
+
parts = []
|
|
427
|
+
for file_path in sorted(target_dir.rglob("*")):
|
|
428
|
+
if file_path.is_file():
|
|
429
|
+
rel = file_path.relative_to(self.output_dir)
|
|
430
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
431
|
+
parts.append(f"### FILE: {rel}\n{content}\n")
|
|
432
|
+
return "\n".join(parts)
|
|
433
|
+
|
|
434
|
+
def get_task_status(self, main_task_id: str) -> Dict:
|
|
435
|
+
"""获取任务执行状态"""
|
|
436
|
+
status = {
|
|
437
|
+
"main_task_id": main_task_id,
|
|
438
|
+
"tasks": {},
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for suffix in ["_backend", "_frontend", "_test", "_audit"]:
|
|
442
|
+
task_id = f"{main_task_id}{suffix}"
|
|
443
|
+
task_info = self.db.get_task(task_id)
|
|
444
|
+
if task_info:
|
|
445
|
+
status["tasks"][suffix[1:]] = {
|
|
446
|
+
"task_id": task_id,
|
|
447
|
+
"status": task_info["status"],
|
|
448
|
+
"result": task_info.get("result"),
|
|
449
|
+
"error": task_info.get("error"),
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return status
|
coordinator/dag.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DAG 调度器 - 管理任务依赖关系
|
|
3
|
+
实现有向无环图的任务编排
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from collections import defaultdict, deque
|
|
7
|
+
from typing import Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class DAGScheduler:
|
|
11
|
+
"""有向无环图任务调度器"""
|
|
12
|
+
|
|
13
|
+
def __init__(self):
|
|
14
|
+
# 邻接表:task_id -> [依赖它的下游任务]
|
|
15
|
+
self.graph: Dict[str, List[str]] = defaultdict(list)
|
|
16
|
+
# 入度:task_id -> 依赖数量
|
|
17
|
+
self.in_degree: Dict[str, int] = defaultdict(int)
|
|
18
|
+
# 任务信息存储
|
|
19
|
+
self.tasks: Dict[str, Dict] = {}
|
|
20
|
+
# 任务状态
|
|
21
|
+
self.status: Dict[str, str] = defaultdict(lambda: "pending")
|
|
22
|
+
|
|
23
|
+
def add_task(
|
|
24
|
+
self,
|
|
25
|
+
task_id: str,
|
|
26
|
+
agent_type: str,
|
|
27
|
+
dependencies: Optional[List[str]] = None,
|
|
28
|
+
context: Optional[Dict] = None,
|
|
29
|
+
):
|
|
30
|
+
"""
|
|
31
|
+
添加任务到 DAG
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
task_id: 任务 ID
|
|
35
|
+
agent_type: Agent 类型
|
|
36
|
+
dependencies: 依赖的任务 ID 列表
|
|
37
|
+
context: 任务上下文
|
|
38
|
+
"""
|
|
39
|
+
dependencies = dependencies or []
|
|
40
|
+
self.tasks[task_id] = {
|
|
41
|
+
"agent_type": agent_type,
|
|
42
|
+
"dependencies": dependencies,
|
|
43
|
+
"context": context or {},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
# 更新入度和邻接表
|
|
47
|
+
self.in_degree[task_id] = len(dependencies)
|
|
48
|
+
for dep in dependencies:
|
|
49
|
+
self.graph[dep].append(task_id)
|
|
50
|
+
|
|
51
|
+
def get_runnable_tasks(self) -> List[str]:
|
|
52
|
+
"""
|
|
53
|
+
获取当前可运行的任务(所有依赖已完成)
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
可运行任务 ID 列表
|
|
57
|
+
"""
|
|
58
|
+
runnable = []
|
|
59
|
+
for task_id, deg in self.in_degree.items():
|
|
60
|
+
if deg == 0 and self.status[task_id] == "pending":
|
|
61
|
+
# 检查所有依赖是否都已完成
|
|
62
|
+
deps = self.tasks[task_id]["dependencies"]
|
|
63
|
+
if all(self.status[d] == "completed" for d in deps):
|
|
64
|
+
runnable.append(task_id)
|
|
65
|
+
return runnable
|
|
66
|
+
|
|
67
|
+
def mark_done(self, task_id: str):
|
|
68
|
+
"""标记任务完成,并更新下游任务的入度"""
|
|
69
|
+
self.status[task_id] = "completed"
|
|
70
|
+
|
|
71
|
+
# 更新下游任务的入度
|
|
72
|
+
for downstream in self.graph[task_id]:
|
|
73
|
+
self.in_degree[downstream] -= 1
|
|
74
|
+
|
|
75
|
+
def mark_failed(self, task_id: str):
|
|
76
|
+
"""标记任务失败,并级联标记所有下游任务为失败"""
|
|
77
|
+
self.status[task_id] = "failed"
|
|
78
|
+
# 级联标记所有下游任务为失败,避免死锁
|
|
79
|
+
self._cascade_failure(task_id)
|
|
80
|
+
|
|
81
|
+
def _cascade_failure(self, task_id: str):
|
|
82
|
+
"""递归标记所有下游任务为失败(BFS 遍历)"""
|
|
83
|
+
from collections import deque
|
|
84
|
+
|
|
85
|
+
queue = deque([task_id])
|
|
86
|
+
while queue:
|
|
87
|
+
current = queue.popleft()
|
|
88
|
+
for downstream in self.graph.get(current, []):
|
|
89
|
+
if self.status[downstream] not in ("completed", "failed"):
|
|
90
|
+
self.status[downstream] = "failed"
|
|
91
|
+
queue.append(downstream)
|
|
92
|
+
|
|
93
|
+
def is_completed(self) -> bool:
|
|
94
|
+
"""检查所有任务是否已完成"""
|
|
95
|
+
return all(
|
|
96
|
+
self.status[task_id] in ("completed", "failed") for task_id in self.tasks
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def get_task_context(self, task_id: str) -> Dict:
|
|
100
|
+
"""
|
|
101
|
+
获取任务的完整上下文(包括依赖任务的结果)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
合并了所有依赖任务结果的上下文
|
|
105
|
+
"""
|
|
106
|
+
context = self.tasks[task_id]["context"].copy()
|
|
107
|
+
|
|
108
|
+
# 合并依赖任务的结果
|
|
109
|
+
for dep_id in self.tasks[task_id]["dependencies"]:
|
|
110
|
+
dep_result = self.tasks[dep_id].get("result", {})
|
|
111
|
+
context[f"{self.tasks[dep_id]['agent_type']}_result"] = dep_result
|
|
112
|
+
|
|
113
|
+
return context
|
|
114
|
+
|
|
115
|
+
def topological_sort(self) -> List[str]:
|
|
116
|
+
"""
|
|
117
|
+
拓扑排序(用于获取执行顺序)
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
拓扑排序后的任务 ID 列表
|
|
121
|
+
"""
|
|
122
|
+
in_deg = self.in_degree.copy()
|
|
123
|
+
queue = deque([t for t, d in in_deg.items() if d == 0])
|
|
124
|
+
order = []
|
|
125
|
+
|
|
126
|
+
while queue:
|
|
127
|
+
task = queue.popleft()
|
|
128
|
+
order.append(task)
|
|
129
|
+
for downstream in self.graph[task]:
|
|
130
|
+
in_deg[downstream] -= 1
|
|
131
|
+
if in_deg[downstream] == 0:
|
|
132
|
+
queue.append(downstream)
|
|
133
|
+
|
|
134
|
+
return order
|
|
135
|
+
|
|
136
|
+
def reset(self):
|
|
137
|
+
"""
|
|
138
|
+
清空 DAG 所有状态,用于迭代模式下重新提交任务
|
|
139
|
+
|
|
140
|
+
保留已完成的任务结果不清除(由 Coordinator 管理上下文),
|
|
141
|
+
此处仅清空调度相关状态。
|
|
142
|
+
"""
|
|
143
|
+
self.graph.clear()
|
|
144
|
+
self.in_degree.clear()
|
|
145
|
+
self.tasks.clear()
|
|
146
|
+
# status 使用 defaultdict,清空需要重新初始化
|
|
147
|
+
self.status = defaultdict(lambda: "pending")
|
executor/__init__.py
ADDED
|
File without changes
|