Coverage for agentos/subagent/collaboration.py: 34%

205 statements  

« prev     ^ index     » next       coverage.py v7.14.3, created at 2026-07-08 10:59 +0800

1""" 

2Agent 协作模式 — Debate/Vote/Review/Pipeline/Ensemble。 

3基于 SubAgentManager + 父子通信之上,提供高级多Agent协作原语。 

4 

5使用示例:: 

6 

7 mgr = SubAgentManager() 

8 collab = AgentCollaboration(mgr) 

9 

10 result = await collab.debate("Python vs Rust for web backend", agents=2) 

11 result = await collab.vote(["方案A", "方案B", "方案C"], agents=5) 

12 result = await collab.review("写一篇关于AI安全的文章", rounds=2) 

13 result = await collab.pipeline("分析Q2财报数据", stages=3) 

14 result = await collab.ensemble("设计系统架构方案", agents=3) 

15""" 

16 

17from __future__ import annotations 

18 

19import re 

20import time 

21import uuid 

22from collections.abc import Awaitable, Callable 

23from dataclasses import dataclass, field 

24from enum import StrEnum 

25from typing import Any 

26 

27from .manager import SubAgentManager, SubAgentResult, SubAgentSpec 

28from .parent_child import ChildContext, SharedState 

29 

30# ────────────────────────────────────────────── 

31# 枚举与数据类型 

32# ────────────────────────────────────────────── 

33 

34 

35class CollaborationMode(StrEnum): 

36 DEBATE = "debate" 

37 VOTE = "vote" 

38 REVIEW = "review" 

39 PIPELINE = "pipeline" 

40 ENSEMBLE = "ensemble" 

41 

42 

43class VoteStrategy(StrEnum): 

44 MAJORITY = "majority" 

45 WEIGHTED = "weighted" 

46 RANKED = "ranked" 

47 UNANIMOUS = "unanimous" 

48 

49 

50@dataclass 

51class DebateRound: 

52 """一轮辩论。""" 

53 

54 round: int 

55 arguments: list[str] # 各方论点 

56 rebuttals: list[str] # 反驳 

57 winner: int | None = None 

58 

59 

60@dataclass 

61class VoteBallot: 

62 """一张选票。""" 

63 

64 agent_id: str 

65 choice: str 

66 confidence: float = 1.0 

67 reasoning: str = "" 

68 

69 

70@dataclass 

71class ReviewPass: 

72 """一轮审查。""" 

73 

74 round: int 

75 draft: str 

76 feedback: str 

77 revised: str 

78 score: float = 0.0 

79 

80 

81@dataclass 

82class CollaborationResult: 

83 """协作结果。""" 

84 

85 mode: CollaborationMode 

86 agents: list[str] 

87 rounds: int 

88 final_output: str 

89 intermediate: list[Any] = field(default_factory=list) 

90 consensus: float = 0.0 

91 duration: float = 0.0 

92 

93 

94# ────────────────────────────────────────────── 

95# 角色性格库 

96# ────────────────────────────────────────────── 

97 

98_PERSONAS = [ 

99 "Be logical, data-driven, and cite evidence.", 

100 "Be creative, think outside the box, challenge assumptions.", 

101 "Focus on practical concerns: cost, timeline, feasibility.", 

102 "Advocate for user experience and human-centered design.", 

103 "Take a contrarian stance, find flaws in all arguments.", 

104] 

105 

106# ────────────────────────────────────────────── 

107# 核心协作引擎 

108# ────────────────────────────────────────────── 

109 

110 

111class AgentCollaboration: 

112 """多Agent协作引擎。 

113 

114 参数: 

115 manager: SubAgentManager 实例 

116 run_func: 执行函数 (task_str, ctx) -> (output, iterations) 

117 default_timeout: 每个子Agent默认超时(秒) 

118 """ 

119 

120 def __init__( 

121 self, 

122 manager: SubAgentManager | None = None, 

123 run_func: Callable[[SubAgentSpec, ChildContext], Awaitable[tuple[str, int]]] | None = None, 

124 default_timeout: float | None = 300.0, 

125 ): 

126 self._mgr = manager or SubAgentManager() 

127 self._run = run_func 

128 self._timeout = default_timeout 

129 

130 # ── 便捷属性 ──────────────────────────── 

131 

132 @property 

133 def manager(self) -> SubAgentManager: 

134 return self._mgr 

135 

136 @property 

137 def shared_state(self) -> SharedState: 

138 return self._mgr.shared_state 

139 

140 async def cancel_all(self) -> None: 

141 await self._mgr.cancel_all() 

142 

143 @property 

144 def active_agents(self) -> int: 

145 return self._mgr.active_children 

146 

147 # ── 内部辅助 ──────────────────────────── 

148 

149 async def _spawn(self, task: str, timeout: float | None = None) -> SubAgentResult: 

150 """Fork 一个单Agent执行 task。""" 

151 return await self._mgr.spawn_fork( 

152 task=task, 

153 run_func=self._run, 

154 timeout=timeout or self._timeout, 

155 ) 

156 

157 async def _spawn_many( 

158 self, tasks: list[str], timeout: float | None = None 

159 ) -> list[SubAgentResult]: 

160 """Swarm 并行执行多个 task。""" 

161 return await self._mgr.spawn_swarm( 

162 tasks=tasks, 

163 run_func=self._run, 

164 timeout=timeout or self._timeout, 

165 ) 

166 

167 @staticmethod 

168 def _parse_score(output: str) -> float: 

169 """从输出中解析 SCORE: N 格式的评分。""" 

170 m = re.search(r"SCORE:\s*([\d.]+)", output, re.IGNORECASE) 

171 if m: 

172 return max(0.0, min(10.0, float(m.group(1)))) / 10.0 

173 return 0.7 

174 

175 @staticmethod 

176 def _parse_choice(output: str, option_count: int) -> tuple[int | None, float, str]: 

177 """解析 CHOICE: N | CONFIDENCE: X | REASONING: text。""" 

178 choice = None 

179 confidence = 0.5 

180 reasoning = output[:200] 

181 

182 m_choice = re.search(r"CHOICE:\s*(\d+)", output, re.IGNORECASE) 

183 if m_choice: 

184 num = int(m_choice.group(1)) 

185 if 1 <= num <= option_count: 

186 choice = num 

187 

188 m_conf = re.search(r"CONFIDENCE:\s*([\d.]+)", output, re.IGNORECASE) 

189 if m_conf: 

190 confidence = max(0.0, min(1.0, float(m_conf.group(1)))) 

191 

192 m_reason = re.search(r"REASONING:\s*(.+?)(?:\n|$)", output, re.IGNORECASE | re.DOTALL) 

193 if m_reason: 

194 reasoning = m_reason.group(1).strip()[:200] 

195 

196 return choice, confidence, reasoning 

197 

198 # ══════════════════════════════════════════ 

199 # 1. Debate 辩论模式 

200 # ══════════════════════════════════════════ 

201 

202 async def debate( 

203 self, 

204 topic: str, 

205 agents: int = 2, 

206 rounds: int = 3, 

207 timeout: float | None = None, 

208 ) -> CollaborationResult: 

209 """多个Agent辩论 topic,裁判总结。 

210 

211 流程: 

212 Round 0: 各方发表初始论点 

213 Round 1~N: 反驳对方 + 强化己方 

214 裁判: 综合所有论点给出最终裁决 

215 """ 

216 t0 = time.time() 

217 history: list[DebateRound] = [] 

218 all_agent_ids: list[str] = [] 

219 

220 # Round 0 — 初始论点 

221 r0_tasks = [ 

222 f"Debate topic: {topic}\n" 

223 f"You are debater {chr(65+i)}. {_PERSONAS[i % len(_PERSONAS)]}\n" 

224 f"Present your opening argument." 

225 for i in range(agents) 

226 ] 

227 r0_results = await self._spawn_many(r0_tasks, timeout) 

228 r0_args = [r.output for r in r0_results] 

229 history.append(DebateRound(round=0, arguments=r0_args, rebuttals=[])) 

230 all_agent_ids.extend(r.agent_id for r in r0_results) 

231 

232 # Round 1..N — 反驳强化 

233 for rnd in range(1, rounds): 

234 rebut_tasks = [] 

235 for i in range(agents): 

236 opponent_args = [history[-1].arguments[j] for j in range(agents) if j != i] 

237 rebut_tasks.append( 

238 f"Debate topic: {topic}\n" 

239 f"You are debater {chr(65+i)}. {_PERSONAS[i % len(_PERSONAS)]}\n" 

240 f"Opponent arguments: {' | '.join(opponent_args)}\n" 

241 f"Provide your rebuttal and strengthen your position." 

242 ) 

243 rebut_results = await self._spawn_many(rebut_tasks, timeout) 

244 rebuttals = [r.output for r in rebut_results] 

245 history.append( 

246 DebateRound( 

247 round=rnd, 

248 arguments=[history[-1].arguments[i] for i in range(agents)], 

249 rebuttals=rebuttals, 

250 ) 

251 ) 

252 all_agent_ids.extend(r.agent_id for r in rebut_results) 

253 

254 # 裁判总结 

255 all_args = "\n\n".join( 

256 [ 

257 f"Debater {chr(65+i)} initial: {history[0].arguments[i]}\n" 

258 f"Debater {chr(65+i)} final rebuttal: " 

259 f"{history[-1].rebuttals[i] if i < len(history[-1].rebuttals) else 'N/A'}" 

260 for i in range(agents) 

261 ] 

262 ) 

263 judge = await self._spawn( 

264 f"As an impartial judge, synthesize this debate on '{topic}' and give your verdict:\n" 

265 f"{all_args}", 

266 timeout, 

267 ) 

268 all_agent_ids.append(judge.agent_id) 

269 

270 return CollaborationResult( 

271 mode=CollaborationMode.DEBATE, 

272 agents=all_agent_ids, 

273 rounds=rounds + 1, 

274 final_output=judge.output, 

275 intermediate=history, 

276 consensus=0.5 + 0.1 * min(agents, 5), 

277 duration=time.time() - t0, 

278 ) 

279 

280 # ══════════════════════════════════════════ 

281 # 2. Vote 投票模式 

282 # ══════════════════════════════════════════ 

283 

284 async def vote( 

285 self, 

286 options: list[str], 

287 agents: int = 3, 

288 strategy: VoteStrategy = VoteStrategy.MAJORITY, 

289 timeout: float | None = None, 

290 ) -> CollaborationResult: 

291 """多个Agent投票选择最优方案。 

292 

293 参数: 

294 options: 候选选项列表 

295 agents: 投票Agent数 

296 strategy: 统计策略 

297 """ 

298 t0 = time.time() 

299 option_list = "\n".join(f"{i+1}. {opt}" for i, opt in enumerate(options)) 

300 ballots: list[VoteBallot] = [] 

301 

302 vote_tasks = [ 

303 f"You are voter {i+1}/{agents}. {_PERSONAS[i % len(_PERSONAS)]}\n" 

304 f"Evaluate these options and vote for ONE:\n{option_list}\n" 

305 f"Format: CHOICE: <number> | CONFIDENCE: <0.0-1.0> | REASONING: <text>" 

306 for i in range(agents) 

307 ] 

308 results = await self._spawn_many(vote_tasks, timeout) 

309 

310 for r in results: 

311 c, conf, reason = self._parse_choice(r.output, len(options)) 

312 if c is not None: 

313 ballots.append( 

314 VoteBallot( 

315 agent_id=r.agent_id, 

316 choice=options[c - 1], 

317 confidence=conf, 

318 reasoning=reason, 

319 ) 

320 ) 

321 

322 _tally: dict[str, int] = {} 

323 _weighted: dict[str, float] = {} 

324 for b in ballots: 

325 _tally[b.choice] = _tally.get(b.choice, 0) + 1 

326 _weighted[b.choice] = _weighted.get(b.choice, 0) + b.confidence 

327 

328 if not ballots: 

329 return CollaborationResult( 

330 mode=CollaborationMode.VOTE, 

331 agents=[], 

332 rounds=1, 

333 final_output="No valid votes cast.", 

334 consensus=0.0, 

335 duration=time.time() - t0, 

336 ) 

337 

338 if strategy == VoteStrategy.WEIGHTED: 

339 winner = max(_weighted, key=_weighted.get) 

340 consensus = _weighted[winner] / sum(_weighted.values()) 

341 summary = f"Weighted winner: {winner} (score: {_weighted[winner]:.2f})" 

342 elif strategy == VoteStrategy.UNANIMOUS: 

343 if len(_tally) == 1 and list(_tally.values())[0] == agents: 

344 winner, consensus = list(_tally.keys())[0], 1.0 

345 summary = f"Unanimous: {winner}" 

346 else: 

347 winner, consensus = "NO CONSENSUS", 0.0 

348 summary = "Unanimous vote FAILED" 

349 else: 

350 winner = max(_tally, key=_tally.get) 

351 consensus = _tally[winner] / len(ballots) 

352 summary = f"Majority winner: {winner} ({_tally[winner]}/{len(ballots)} votes)" 

353 

354 report = f"{summary}\n\nVote details:\n" 

355 for b in ballots: 

356 report += f"- [{b.agent_id}] '{b.choice}' (conf={b.confidence:.2f}): {b.reasoning}\n" 

357 

358 return CollaborationResult( 

359 mode=CollaborationMode.VOTE, 

360 agents=[b.agent_id for b in ballots], 

361 rounds=1, 

362 final_output=report, 

363 intermediate=ballots, 

364 consensus=consensus, 

365 duration=time.time() - t0, 

366 ) 

367 

368 # ══════════════════════════════════════════ 

369 # 3. Review 审查模式 

370 # ══════════════════════════════════════════ 

371 

372 async def review( 

373 self, 

374 task: str, 

375 rounds: int = 2, 

376 timeout: float | None = None, 

377 ) -> CollaborationResult: 

378 """Writer产出 → Reviewer审查 → 多轮迭代。 

379 

380 流程: 

381 1. Writer 生成初稿 

382 2. Reviewer 审查 → 打分 + 反馈 

383 3. Writer 根据反馈修改 

384 4. 重复 rounds 次 

385 """ 

386 t0 = time.time() 

387 passes: list[ReviewPass] = [] 

388 writer_id = uuid.uuid4().hex[:8] 

389 reviewer_id = uuid.uuid4().hex[:8] 

390 

391 draft_result = await self._spawn( 

392 f"As a writer, complete: {task}", 

393 timeout, 

394 ) 

395 draft = draft_result.output 

396 

397 for rnd in range(rounds): 

398 review_result = await self._spawn( 

399 f"As a reviewer, evaluate this draft (round {rnd+1}):\n{draft}\n" 

400 f"Provide specific feedback. Format: SCORE: <0-10> | FEEDBACK: <text>", 

401 timeout, 

402 ) 

403 feedback = review_result.output 

404 score = self._parse_score(feedback) 

405 

406 revise_result = await self._spawn( 

407 f"As a writer, revise your draft based on this feedback:\n{feedback}\n\n" 

408 f"Original draft:\n{draft}\n\nProvide the revised version.", 

409 timeout, 

410 ) 

411 revised = revise_result.output 

412 

413 passes.append( 

414 ReviewPass( 

415 round=rnd + 1, 

416 draft=draft, 

417 feedback=feedback, 

418 revised=revised, 

419 score=score, 

420 ) 

421 ) 

422 draft = revised 

423 

424 return CollaborationResult( 

425 mode=CollaborationMode.REVIEW, 

426 agents=[writer_id, reviewer_id], 

427 rounds=rounds, 

428 final_output=draft, 

429 intermediate=passes, 

430 consensus=passes[-1].score if passes else 0.0, 

431 duration=time.time() - t0, 

432 ) 

433 

434 # ══════════════════════════════════════════ 

435 # 4. Pipeline 流水线模式 

436 # ══════════════════════════════════════════ 

437 

438 async def pipeline( 

439 self, 

440 task: str, 

441 stages: int = 3, 

442 stage_names: list[str] | None = None, 

443 timeout: float | None = None, 

444 ) -> CollaborationResult: 

445 """多Agent串联处理,前一输出是后一输入。 

446 

447 参数: 

448 task: 初始输入 

449 stages: 流水线段数 

450 stage_names: 自定义阶段名,默认 ['Analyzer','Processor','Refiner',...] 

451 """ 

452 t0 = time.time() 

453 if stage_names is None: 

454 stage_names = ["Analyzer", "Processor", "Refiner", "Polisher", "Validator"][:stages] 

455 

456 intermediates: list[str] = [] 

457 agent_ids: list[str] = [] 

458 current_input = task 

459 

460 for name in stage_names: 

461 result = await self._spawn( 

462 f"You are the {name} stage in a processing pipeline.\n" 

463 f"Input: {current_input}\nProcess and output for the next stage.", 

464 timeout, 

465 ) 

466 current_input = result.output 

467 intermediates.append(current_input) 

468 agent_ids.append(result.agent_id) 

469 

470 return CollaborationResult( 

471 mode=CollaborationMode.PIPELINE, 

472 agents=agent_ids, 

473 rounds=stages, 

474 final_output=current_input, 

475 intermediate=intermediates, 

476 consensus=1.0, 

477 duration=time.time() - t0, 

478 ) 

479 

480 # ══════════════════════════════════════════ 

481 # 5. Ensemble 集成模式 

482 # ══════════════════════════════════════════ 

483 

484 async def ensemble( 

485 self, 

486 task: str, 

487 agents: int = 3, 

488 merge_strategy: str = "best_of", 

489 timeout: float | None = None, 

490 ) -> CollaborationResult: 

491 """多个Agent独立求解,合并最优。 

492 

493 参数: 

494 task: 需求描述 

495 agents: 求解Agent数 

496 merge_strategy: 'best_of' | 'merge' | 'weighted' 

497 """ 

498 t0 = time.time() 

499 

500 tasks = [ 

501 f"You are solver {i+1}/{agents}. {_PERSONAS[i % len(_PERSONAS)]}\n" 

502 f"Complete: {task}\nAt the end self-evaluate: SELF_SCORE: <0-10>" 

503 for i in range(agents) 

504 ] 

505 results = await self._spawn_many(tasks, timeout) 

506 

507 scored: list[tuple[float, str, str]] = [] 

508 for r in results: 

509 s = self._parse_score(r.output) * 10 # 0-10 

510 scored.append((s, r.output, r.agent_id)) 

511 scored.sort(reverse=True, key=lambda x: x[0]) 

512 

513 if not scored: 

514 return CollaborationResult( 

515 mode=CollaborationMode.ENSEMBLE, 

516 agents=[], 

517 rounds=1, 

518 final_output="No results.", 

519 duration=time.time() - t0, 

520 ) 

521 

522 if merge_strategy == "best_of": 

523 bs, bo, ba = scored[0] 

524 final = f"Best solution (score: {bs:.1f}/10) from [{ba}]:\n\n{bo}" 

525 elif merge_strategy == "merge": 

526 parts = [] 

527 for i, (s, o, a) in enumerate(scored): 

528 parts.append(f"[Solution {i+1}, score={s:.1f}]:\n{o[:500]}") 

529 merge_result = await self._spawn( 

530 f"As a meta-synthesizer, merge these {agents} solutions:\n\n" 

531 + "\n---\n".join(parts), 

532 timeout, 

533 ) 

534 final = merge_result.output 

535 else: # weighted 

536 total = sum(s for s, _, _ in scored) 

537 weights = [(s / total) if total else 0 for s, _, _ in scored] 

538 parts = [] 

539 for (s, o, a), w in zip(scored, weights): 

540 parts.append(f"[{a}] weight={w:.2f}:\n{o[:300]}") 

541 final = f"Weighted ensemble ({agents} solvers):\n\n" + "\n---\n".join(parts) 

542 

543 return CollaborationResult( 

544 mode=CollaborationMode.ENSEMBLE, 

545 agents=[a for _, _, a in scored], 

546 rounds=1, 

547 final_output=final, 

548 intermediate=scored, 

549 consensus=scored[0][0] / 10.0, 

550 duration=time.time() - t0, 

551 )