"""Pure renderers: report dict in, HTML/Markdown/JSON string out. No I/O and no storage access — every function takes the dict produced by the generation side (``evaluation.report``) so rendering can be unit-tested from a hand-built dict. Report *content* decisions (which cases pass, which rates to show) belong to generation; this module only formats. """ import json from typing import Any, Optional from jinja2 import Template HTML_TEMPLATE = """ 评测报告 - {{ report.run_id }}

评测报告

评测对象:{{ report.target_name }}({{ report.target_id }})

评测场景:{{ report.scenario_name }}({{ report.scenario_id }})

执行时间:{{ report.started_at }} 至 {{ report.completed_at or '进行中' }}

{{ report.summary.total_cases }}
用例总数
{{ report.summary.passed_cases }}
通过用例
{{ report.summary.total_rules }}
规则总数
{{ "%.2f"|format(report.summary.pass_rate * 100) }}%
规则通过率
{% for case in report.cases %}
用例 {{ case.case_id }}
{% for turn in case.turns %}
用户消息
{{ turn.sent_text }}
智能体回复({{ turn.latency_ms }}ms)
{{ turn.reply_text or '(无回复)' }}
{% endfor %}
{% for result in case.results %}
{{ '通过' if result.passed else '失败' }} {{ result.rule_type }}: {{ result.reason }}
{% endfor %}
{% endfor %}
""" def _pct(v: Optional[float]) -> str: return "—" if v is None else f"{v * 100:.1f}%" def _ms(v: Optional[float]) -> str: return "—" if v is None else f"{v:.0f}ms" def render_html(report: dict[str, Any]) -> str: """Render a run report dict as an HTML string.""" return Template(HTML_TEMPLATE).render(report=report) def render_json(report: dict[str, Any]) -> str: """Render a report dict as a JSON string.""" return json.dumps(report, ensure_ascii=False, indent=2) def render_markdown(report: dict[str, Any]) -> str: """Render a run report dict as Markdown.""" s = report["summary"] judged_rate = s.get("judged_pass_rate") judged_rate_text = "—" if judged_rate is None else f"{judged_rate * 100:.1f}%" lines: list[str] = [ f"# 评测报告 — {report.get('scenario_name', report.get('run_id', ''))}", "", f"**评测对象**: {report.get('target_name', '-')} ", f"**评测场景**: {report.get('scenario_name', '-')} ", f"**状态**: {report.get('status', '-')} ", f"**开始时间**: {report.get('started_at', '-')} ", f"**完成时间**: {report.get('completed_at', '-')} ", "", "## 汇总", "", "| 指标 | 数值 |", "|------|------|", f"| 总用例数 | {s['total_cases']} |", f"| 通过用例 | {s['passed_cases']} |", f"| 失败用例 | {s['failed_cases']} |", f"| 总规则数 | {s['total_rules']} |", f"| 通过规则 | {s['passed_rules']} |", f"| 通过率 | {s['pass_rate'] * 100:.1f}% |", f"| 连通用例 | {s.get('connectivity_cases', 0)} |", f"| 判定型通过率 | {judged_rate_text} |", "", "## 用例明细", "", ] for case in report.get("cases", []): if case.get("connectivity"): badge = "🔗" else: badge = "✅" if case.get("passed") else "❌" title = f"### {badge} 用例 `{case['case_id']}`" if case.get("connectivity"): title += "(连通用例,未配置判定标准)" lines.append(title) lines.append("") for turn in case.get("turns", []): lines.append(f"**第 {turn['round']} 轮**") lines.append("") lines.append(f"> **用户**: {turn.get('sent_text', '—')}") lines.append("") reply = turn.get("reply_text") or "(无回复)" lines.append(f"> **智能体**: {reply}") if turn.get("latency_ms") is not None: lines.append(f"> *延迟: {turn['latency_ms']}ms*") lines.append("") if case.get("results"): lines.append("**规则评估结果**") lines.append("") lines.append("| 规则 | 结果 | 评分 | 说明 |") lines.append("|------|------|------|------|") for r in case["results"]: badge = "✅" if r["passed"] else "❌" score = f"{r['score']:.2f}" if r.get("score") is not None else "-" lines.append(f"| {r['rule_type']} | {badge} | {score} | {r.get('reason', '')} |") lines.append("") return "\n".join(lines) def render_campaign_markdown(report: dict[str, Any]) -> str: """Render a dual-axis campaign report dict as Markdown.""" s = report["summary"] lines: list[str] = [ f"# 活动周期报告 — {report['name']}", "", f"**状态**: {report['status']} ", f"**窗口**: {report['window_seconds']}s(倍速 {report['time_scale']}) ", f"**开始时间**: {report['started_at'] or '-'} ", f"**完成时间**: {report['completed_at'] or '-'} ", "", "## 汇总", "", "| 指标 | 数值 |", "|------|------|", f"| 子运行总数 | {s['total_runs']} |", f"| 已完成 | {s['completed_runs']} |", f"| 整窗通过率 | {_pct(s['overall_pass_rate'])} |", f"| 整窗可用性 | {_pct(s['overall_availability'])} |", f"| 平均时延 | {_ms(s['avg_latency_ms'])} |", "", "## 时间趋势", "", "| 时段(秒) | 运行数 | 通过率 | 可用性 | 时延 |", "|------|------|------|------|------|", ] for b in report["time_trend"]: lines.append( f"| {b['start_seconds']:.0f}–{b['end_seconds']:.0f} | {b['run_count']} | " f"{_pct(b['pass_rate'])} | {_pct(b['availability'])} | {_ms(b['avg_latency_ms'])} |" ) lines += [ "", "## 能力汇总", "", "| 场景 | 运行数 | 通过率 | 可用性 | 时延 |", "|------|------|------|------|------|", ] for c in report["capability_summary"]: lines.append( f"| {c['scenario_name']} | {c['run_count']} | {_pct(c['pass_rate'])} | " f"{_pct(c['availability'])} | {_ms(c['avg_latency_ms'])} |" ) return "\n".join(lines)