feat(cli): U6 CLI 协同事件 Rich 渲染
- chat.py 新增 _render_collaboration_contracts 和 _render_pm_collaboration_event - 4 种 PM 协同事件渲染: collaboration_contract_defined (cyan Panel) collaboration_notice (蓝→品红 文本) review_result (passed=green / failed=red Panel) risk_flagged (yellow Panel) - plan_update 中提取 collaboration_contracts 并渲染 - _print_help 更新项目经理模式说明 - 优雅降级:字段缺失回退到 ?,空契约不输出,整体 try/except 不中断编排 - 新增 11 个测试(TestPMCollaborationRendering 9 + TestPrintHelpPMMode 2) - ruff 通过,pytest 23 passed
This commit is contained in:
parent
34a4164430
commit
6016c087fe
|
|
@ -525,6 +525,87 @@ def _resolve_default_model(server_config: "ServerConfig") -> str:
|
||||||
return "default"
|
return "default"
|
||||||
|
|
||||||
|
|
||||||
|
def _render_collaboration_contracts(contracts: list[dict]) -> None:
|
||||||
|
"""Render collaboration contracts as a Panel (U6)."""
|
||||||
|
if not contracts:
|
||||||
|
return
|
||||||
|
lines = [
|
||||||
|
f" [blue]{c.get('from_expert', '?')}[/blue] → "
|
||||||
|
f"[magenta]{c.get('to_expert', '?')}[/magenta]: "
|
||||||
|
f"{c.get('content_description', '')} "
|
||||||
|
f"[dim]({c.get('status', 'pending')})[/dim]"
|
||||||
|
for c in contracts
|
||||||
|
]
|
||||||
|
rprint(
|
||||||
|
Panel(
|
||||||
|
"\n".join(lines),
|
||||||
|
title="[bold]协作契约[/bold]",
|
||||||
|
border_style="cyan",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _render_pm_collaboration_event(message: dict) -> bool:
|
||||||
|
"""Render PM collaboration events (U6).
|
||||||
|
|
||||||
|
Handles 4 event types: collaboration_contract_defined, collaboration_notice,
|
||||||
|
review_result, risk_flagged. Returns True if the event type was handled.
|
||||||
|
Best-effort: never raises on missing/malformed data.
|
||||||
|
"""
|
||||||
|
etype = message.get("type", "")
|
||||||
|
try:
|
||||||
|
if etype == "collaboration_contract_defined":
|
||||||
|
_render_collaboration_contracts(message.get("contracts", []))
|
||||||
|
return True
|
||||||
|
elif etype == "collaboration_notice":
|
||||||
|
from_e = message.get("from_expert", "?")
|
||||||
|
to_e = message.get("to_expert", "?")
|
||||||
|
content = message.get("content_description", "")
|
||||||
|
rprint(f" [blue]{from_e}[/blue] [dim]→[/dim] [magenta]{to_e}[/magenta]: {content}")
|
||||||
|
return True
|
||||||
|
elif etype == "review_result":
|
||||||
|
passed = bool(message.get("passed", False))
|
||||||
|
feedback = message.get("feedback", "")
|
||||||
|
phase_name = message.get("phase_name", "?")
|
||||||
|
expert = message.get("expert", "?")
|
||||||
|
rework_count = message.get("rework_count", 0)
|
||||||
|
color = "green" if passed else "red"
|
||||||
|
status_text = "验收通过" if passed else "验收未通过"
|
||||||
|
lines = [
|
||||||
|
f"[bold]阶段:[/bold] {phase_name} ({expert})",
|
||||||
|
f"[bold]结果:[/bold] [{color}]{status_text}[/{color}]",
|
||||||
|
]
|
||||||
|
if rework_count:
|
||||||
|
lines.append(f"[bold]返工次数:[/bold] {rework_count}")
|
||||||
|
if feedback:
|
||||||
|
lines.append(f"[bold]反馈:[/bold] {feedback}")
|
||||||
|
rprint(
|
||||||
|
Panel(
|
||||||
|
"\n".join(lines),
|
||||||
|
title=f"[bold]{'✓' if passed else '✗'} 验收结果[/bold]",
|
||||||
|
border_style=color,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
elif etype == "risk_flagged":
|
||||||
|
expert = message.get("expert", "?")
|
||||||
|
risk_desc = message.get("risk_description", "")
|
||||||
|
phase_name = message.get("phase_name", "?")
|
||||||
|
rprint(
|
||||||
|
Panel(
|
||||||
|
f"[bold]专家:[/bold] {expert}\n"
|
||||||
|
f"[bold]阶段:[/bold] {phase_name}\n"
|
||||||
|
f"[bold]风险:[/bold] {risk_desc}",
|
||||||
|
title="[bold]⚠ 风险标记[/bold]",
|
||||||
|
border_style="yellow",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
pass # Best-effort rendering; never break orchestration
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def _execute_team_cli(
|
async def _execute_team_cli(
|
||||||
user_input: str,
|
user_input: str,
|
||||||
gateway: "LLMGateway",
|
gateway: "LLMGateway",
|
||||||
|
|
@ -565,9 +646,7 @@ async def _execute_team_cli(
|
||||||
|
|
||||||
expert_configs = router.resolve_expert_configs(routing.specified_experts)
|
expert_configs = router.resolve_expert_configs(routing.specified_experts)
|
||||||
if not expert_configs:
|
if not expert_configs:
|
||||||
rprint(
|
rprint(f"[red]无法解析专家配置: {routing.specified_experts}[/red]")
|
||||||
f"[red]无法解析专家配置: {routing.specified_experts}[/red]"
|
|
||||||
)
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
team = ExpertTeam(pool=agent_pool, template_registry=template_registry)
|
team = ExpertTeam(pool=agent_pool, template_registry=template_registry)
|
||||||
|
|
@ -578,6 +657,10 @@ async def _execute_team_cli(
|
||||||
async def _event_handler(message: dict) -> None:
|
async def _event_handler(message: dict) -> None:
|
||||||
"""Render orchestration events with Rich (best-effort, never raises)."""
|
"""Render orchestration events with Rich (best-effort, never raises)."""
|
||||||
try:
|
try:
|
||||||
|
# U6: PM collaboration events (collaboration_contract_defined,
|
||||||
|
# collaboration_notice, review_result, risk_flagged)
|
||||||
|
if _render_pm_collaboration_event(message):
|
||||||
|
return
|
||||||
etype = message.get("type", "")
|
etype = message.get("type", "")
|
||||||
if etype == "team_formed":
|
if etype == "team_formed":
|
||||||
experts = message.get("experts", [])
|
experts = message.get("experts", [])
|
||||||
|
|
@ -596,7 +679,11 @@ async def _execute_team_cli(
|
||||||
)
|
)
|
||||||
elif etype == "plan_update":
|
elif etype == "plan_update":
|
||||||
phases = message.get("plan_phases", [])
|
phases = message.get("plan_phases", [])
|
||||||
icon_map = {"completed": ("✓", "green"), "in_progress": ("▶", "blue"), "failed": ("✗", "red")}
|
icon_map = {
|
||||||
|
"completed": ("✓", "green"),
|
||||||
|
"in_progress": ("▶", "blue"),
|
||||||
|
"failed": ("✗", "red"),
|
||||||
|
}
|
||||||
lines = []
|
lines = []
|
||||||
for ph in phases:
|
for ph in phases:
|
||||||
status = ph.get("status", "pending")
|
status = ph.get("status", "pending")
|
||||||
|
|
@ -615,6 +702,12 @@ async def _execute_team_cli(
|
||||||
border_style="cyan",
|
border_style="cyan",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# U6: render collaboration contracts embedded in phases
|
||||||
|
all_contracts: list[dict] = []
|
||||||
|
for ph in phases:
|
||||||
|
all_contracts.extend(ph.get("collaboration_contracts", []))
|
||||||
|
if all_contracts:
|
||||||
|
_render_collaboration_contracts(all_contracts)
|
||||||
elif etype == "phase_started":
|
elif etype == "phase_started":
|
||||||
rprint(
|
rprint(
|
||||||
f"\n[bold blue]▶ {message.get('phase_name', '?')}[/bold blue] "
|
f"\n[bold blue]▶ {message.get('phase_name', '?')}[/bold blue] "
|
||||||
|
|
@ -769,9 +862,14 @@ def _print_help() -> None:
|
||||||
" [cyan]/model <name>[/cyan] — Switch LLM model\n"
|
" [cyan]/model <name>[/cyan] — Switch LLM model\n"
|
||||||
" [cyan]/quit[/cyan] — Exit chat\n\n"
|
" [cyan]/quit[/cyan] — Exit chat\n\n"
|
||||||
"[bold]Multi-Agent[/bold]\n\n"
|
"[bold]Multi-Agent[/bold]\n\n"
|
||||||
" [magenta]@team <task>[/magenta] — 专家团协作(Lead 分解 + 专家并行 + 辩论)\n"
|
" [magenta]@team <task>[/magenta] — 专家团协作(项目经理模式:Lead 制定计划 + 协作契约 + 验收 + 辩论)\n"
|
||||||
" [dim]@team:dev_team <task>[/dim] — 使用 dev_team 模板\n"
|
" [dim]@team:dev_team <task>[/dim] — 使用 dev_team 模板\n"
|
||||||
" [dim]@team:expert1,expert2 <task>[/dim] — 指定专家\n\n"
|
" [dim]@team:expert1,expert2 <task>[/dim] — 指定专家\n\n"
|
||||||
|
"[bold]PM Collaboration Events (during @team)[/bold]\n\n"
|
||||||
|
" [cyan]协作契约[/cyan] — Lead 制定计划时定义专家间协作关系\n"
|
||||||
|
" [cyan]协作通知[/cyan] — 专家完成后按契约通知相关专家\n"
|
||||||
|
" [cyan]验收结果[/cyan] — Lead 验收阶段输出(通过/返工/失败)\n"
|
||||||
|
" [cyan]风险标记[/cyan] — 专家标记执行中的风险\n\n"
|
||||||
"[bold]Interventions (during @team)[/bold]\n\n"
|
"[bold]Interventions (during @team)[/bold]\n\n"
|
||||||
" [magenta]/debate <topic>[/magenta] — 手动发起辩论\n"
|
" [magenta]/debate <topic>[/magenta] — 手动发起辩论\n"
|
||||||
" [cyan]/stop[/cyan] — 终止团队执行\n"
|
" [cyan]/stop[/cyan] — 终止团队执行\n"
|
||||||
|
|
|
||||||
|
|
@ -190,3 +190,172 @@ class TestInterventionSupport:
|
||||||
text = captured.getvalue()
|
text = captured.getvalue()
|
||||||
assert "/stop" in text
|
assert "/stop" in text
|
||||||
assert "终止" in text
|
assert "终止" in text
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# U6: 项目经理模式协同事件渲染测试
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestPMCollaborationRendering:
|
||||||
|
"""U6: 项目经理模式协同事件渲染测试"""
|
||||||
|
|
||||||
|
def _capture_render(self, message: dict) -> str:
|
||||||
|
"""辅助:渲染 PM 事件并捕获输出。"""
|
||||||
|
from agentkit.cli.chat import _render_pm_collaboration_event
|
||||||
|
|
||||||
|
captured = io.StringIO()
|
||||||
|
console = Console(file=captured, width=120)
|
||||||
|
with patch(
|
||||||
|
"agentkit.cli.chat.rprint",
|
||||||
|
side_effect=lambda *a, **kw: console.print(*a, **kw),
|
||||||
|
):
|
||||||
|
_render_pm_collaboration_event(message)
|
||||||
|
return captured.getvalue()
|
||||||
|
|
||||||
|
def test_collaboration_contract_defined_renders_panel(self):
|
||||||
|
"""collaboration_contract_defined 事件渲染为 Panel"""
|
||||||
|
message = {
|
||||||
|
"type": "collaboration_contract_defined",
|
||||||
|
"contracts": [
|
||||||
|
{
|
||||||
|
"from_expert": "backend",
|
||||||
|
"to_expert": "frontend",
|
||||||
|
"content_description": "API 定义",
|
||||||
|
"status": "pending",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert "协作契约" in text
|
||||||
|
assert "backend" in text
|
||||||
|
assert "frontend" in text
|
||||||
|
assert "API 定义" in text
|
||||||
|
|
||||||
|
def test_collaboration_contract_defined_empty_contracts(self):
|
||||||
|
"""collaboration_contract_defined 空契约列表不产生输出"""
|
||||||
|
message = {"type": "collaboration_contract_defined", "contracts": []}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert text == ""
|
||||||
|
|
||||||
|
def test_collaboration_notice_renders_colored_text(self):
|
||||||
|
"""collaboration_notice 事件渲染为带颜色的文本"""
|
||||||
|
message = {
|
||||||
|
"type": "collaboration_notice",
|
||||||
|
"from_expert": "backend",
|
||||||
|
"to_expert": "frontend",
|
||||||
|
"content_description": "API 定义已就绪",
|
||||||
|
}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert "backend" in text
|
||||||
|
assert "frontend" in text
|
||||||
|
assert "API 定义已就绪" in text
|
||||||
|
|
||||||
|
def test_review_result_passed_renders_green(self):
|
||||||
|
"""review_result (passed=True) 渲染为绿色"""
|
||||||
|
message = {
|
||||||
|
"type": "review_result",
|
||||||
|
"phase_name": "后端开发",
|
||||||
|
"passed": True,
|
||||||
|
"feedback": "",
|
||||||
|
"expert": "backend_engineer",
|
||||||
|
}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert "验收通过" in text
|
||||||
|
assert "后端开发" in text
|
||||||
|
|
||||||
|
def test_review_result_failed_renders_red(self):
|
||||||
|
"""review_result (passed=False) 渲染为红色"""
|
||||||
|
message = {
|
||||||
|
"type": "review_result",
|
||||||
|
"phase_name": "后端开发",
|
||||||
|
"passed": False,
|
||||||
|
"feedback": "API 缺少错误处理",
|
||||||
|
"expert": "backend_engineer",
|
||||||
|
"rework_count": 1,
|
||||||
|
}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert "验收未通过" in text
|
||||||
|
assert "API 缺少错误处理" in text
|
||||||
|
assert "返工次数" in text
|
||||||
|
|
||||||
|
def test_risk_flagged_renders_yellow_panel(self):
|
||||||
|
"""risk_flagged 事件渲染为黄色 Panel"""
|
||||||
|
message = {
|
||||||
|
"type": "risk_flagged",
|
||||||
|
"expert": "backend_engineer",
|
||||||
|
"risk_description": "数据库连接池可能不足",
|
||||||
|
"phase_name": "后端开发",
|
||||||
|
}
|
||||||
|
text = self._capture_render(message)
|
||||||
|
assert "风险标记" in text
|
||||||
|
assert "数据库连接池可能不足" in text
|
||||||
|
assert "backend_engineer" in text
|
||||||
|
|
||||||
|
def test_missing_data_graceful_degradation(self):
|
||||||
|
"""事件数据缺失时优雅降级"""
|
||||||
|
# collaboration_notice 缺少字段 → 回退到 "?"
|
||||||
|
text = self._capture_render({"type": "collaboration_notice"})
|
||||||
|
assert "?" in text
|
||||||
|
|
||||||
|
# review_result 缺少字段 → 仍渲染(默认 failed=红色)
|
||||||
|
text = self._capture_render({"type": "review_result"})
|
||||||
|
assert "验收" in text
|
||||||
|
|
||||||
|
# risk_flagged 缺少字段 → 仍渲染
|
||||||
|
text = self._capture_render({"type": "risk_flagged"})
|
||||||
|
assert "风险标记" in text
|
||||||
|
|
||||||
|
def test_unhandled_event_returns_false(self):
|
||||||
|
"""非 PM 事件返回 False"""
|
||||||
|
from agentkit.cli.chat import _render_pm_collaboration_event
|
||||||
|
|
||||||
|
result = _render_pm_collaboration_event({"type": "team_formed"})
|
||||||
|
assert result is False
|
||||||
|
|
||||||
|
def test_pm_event_returns_true_when_handled(self):
|
||||||
|
"""PM 事件返回 True"""
|
||||||
|
from agentkit.cli.chat import _render_pm_collaboration_event
|
||||||
|
|
||||||
|
for etype in (
|
||||||
|
"collaboration_contract_defined",
|
||||||
|
"collaboration_notice",
|
||||||
|
"review_result",
|
||||||
|
"risk_flagged",
|
||||||
|
):
|
||||||
|
result = _render_pm_collaboration_event({"type": etype})
|
||||||
|
assert result is True, f"{etype} should return True"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrintHelpPMMode:
|
||||||
|
"""_print_help 包含项目经理模式说明测试"""
|
||||||
|
|
||||||
|
def test_help_includes_pm_mode(self):
|
||||||
|
"""帮助文本包含项目经理模式说明"""
|
||||||
|
from agentkit.cli.chat import _print_help
|
||||||
|
|
||||||
|
captured = io.StringIO()
|
||||||
|
console = Console(file=captured, width=120)
|
||||||
|
with patch(
|
||||||
|
"agentkit.cli.chat.rprint",
|
||||||
|
side_effect=lambda *a, **kw: console.print(*a, **kw),
|
||||||
|
):
|
||||||
|
_print_help()
|
||||||
|
text = captured.getvalue()
|
||||||
|
assert "项目经理" in text
|
||||||
|
|
||||||
|
def test_help_includes_collaboration_events(self):
|
||||||
|
"""帮助文本包含协同事件说明"""
|
||||||
|
from agentkit.cli.chat import _print_help
|
||||||
|
|
||||||
|
captured = io.StringIO()
|
||||||
|
console = Console(file=captured, width=120)
|
||||||
|
with patch(
|
||||||
|
"agentkit.cli.chat.rprint",
|
||||||
|
side_effect=lambda *a, **kw: console.print(*a, **kw),
|
||||||
|
):
|
||||||
|
_print_help()
|
||||||
|
text = captured.getvalue()
|
||||||
|
assert "协作契约" in text
|
||||||
|
assert "验收结果" in text
|
||||||
|
assert "风险标记" in text
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue