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.
@@ -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