"""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)