AgentEvalTool/backend/cli/run.py
sinohqb 12481cd1b8 v0.3-s1: 规则层异步化 + 工具函数去重 + HTTP 通道
## 核心变更

### 规则层全面异步化(DEBT-1)
- EvalRule.evaluate() 签名改为 async def,全量同步改造(无兼容层)
- LlmScoreRule._call_llm: requests.post → httpx.AsyncClient,彻底消除事件循环阻塞
- engine._save_rule_results: rule.evaluate() → await rule.evaluate()

### 工具函数去重(DEBT-2)
- 新建 agenteval/utils/llm.py,统一三个函数:
  - extract_reply_text (原 5 处重复)
  - extract_content_from_llm_response (原 2 处重复)
  - parse_json_from_llm_text (统一 LLM 输出 JSON 解析)
- engine.py / llm_score.py / runs.py / report.py 全部切换到 utils.llm

### HTTP 通用通道(S1-3)
- 新建 channels/http.py (HttpChannel)
  - 配置化 send_url / reply_url 模板 ({message}, {msg_id} 占位)
  - dot-path 提取 msg_id 和 reply_text
  - 可选 reply_ready_path 就绪标志
  - 长连接 AsyncClient 复用
- ChannelFactory 注册 ChannelType.HTTP → HttpChannel

### 测试
- 新增 tests/unit/test_http_channel_and_rules.py (19 个测试)
- _get_path / health_check / send / poll_reply / 超时 / 就绪标志 / async 规则评估
- 测试总数:24 → 43,全部通过

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-17 10:52:32 +08:00

132 lines
4.3 KiB
Python

"""CLI commands for evaluation execution."""
import asyncio
from typing import Any
import typer
from agenteval.evaluation.engine import EvalEngine
from agenteval.storage.repository import RunRepository, ScenarioRepository, TargetRepository
from rich.console import Console
from rich.table import Table
app = typer.Typer(help="评测执行")
console = Console()
def _progress(event: str, data: dict[str, Any]) -> None:
if event == "case_start":
console.print(f"[用例 {data['index']}/{data['total']}] {data['case_id']}")
elif event == "turn_end":
console.print(f"{data['round']} 轮回复,耗时 {data['latency_ms']}ms")
elif event == "rule_result":
style = "green" if data["passed"] else "red"
console.print(
f" [{data['rule_type']}] {'通过' if data['passed'] else '失败'} - {data['reason']}", style=style
)
elif event == "error":
console.print(f"执行异常: {data['error']}", style="red")
@app.command("start", help="启动一次评测")
def start_run(
target_id: str = typer.Option(..., help="评测对象 ID"),
scenario_id: str = typer.Option(..., help="评测场景 ID"),
output_json: bool = typer.Option(False, "--output-json", help="运行结束后输出 JSON 报告"),
) -> None:
target = TargetRepository().get(target_id)
if not target:
console.print(f"评测对象不存在: {target_id}", style="red")
raise typer.Exit(1)
scenario = ScenarioRepository().get(scenario_id)
if not scenario:
console.print(f"评测场景不存在: {scenario_id}", style="red")
raise typer.Exit(1)
console.print(f"开始评测: 对象={target.name}, 场景={scenario.name}")
engine = EvalEngine(target=target, scenario=scenario)
try:
run = asyncio.run(engine.run(progress_callback=_progress))
except Exception as exc:
console.print(f"评测失败: {exc}", style="red")
raise typer.Exit(1)
console.print(f"评测完成: run_id={run.id}, status={run.status.value}")
if run.summary:
console.print_json(data=run.summary)
if output_json:
from agenteval.evaluation.report import render_json_report
console.print(render_json_report(run.id))
@app.command("list", help="列出评测执行记录")
def list_runs() -> None:
runs = RunRepository().list_all()
if not runs:
console.print("暂无评测记录")
return
table = Table(show_header=True, header_style="bold")
table.add_column("Run ID")
table.add_column("对象 ID")
table.add_column("场景 ID")
table.add_column("状态")
table.add_column("开始时间")
table.add_column("完成时间")
for run in runs:
table.add_row(
run.id or "",
run.target_id,
run.scenario_id,
run.status.value,
str(run.started_at)[:19] if run.started_at else "",
str(run.completed_at)[:19] if run.completed_at else "",
)
console.print(table)
@app.command("status", help="查看评测执行状态")
def run_status(run_id: str) -> None:
run = RunRepository().get(run_id)
if not run:
console.print(f"评测记录不存在: {run_id}", style="red")
raise typer.Exit(1)
console.print_json(data=run.model_dump())
@app.command("logs", help="查看评测执行日志(对话轮次)")
def run_logs(run_id: str) -> None:
repo = RunRepository()
run = repo.get(run_id)
if not run:
console.print(f"评测记录不存在: {run_id}", style="red")
raise typer.Exit(1)
turns = repo.get_turns(run_id)
if not turns:
console.print("暂无对话记录")
return
table = Table(show_header=True, header_style="bold")
table.add_column("Case ID")
table.add_column("轮次")
table.add_column("消息 ID")
table.add_column("耗时(ms)")
table.add_column("发送时间")
table.add_column("回复时间")
for turn in turns:
table.add_row(
turn.case_id,
str(turn.round_index),
turn.question_msg_id or "",
str(turn.latency_ms) if turn.latency_ms is not None else "",
str(turn.sent_at)[:19] if turn.sent_at else "",
str(turn.received_at)[:19] if turn.received_at else "",
)
console.print(table)