AgentEvalTool/backend/agenteval/intelligent_eval/report.py
sinohqb 1317552701 feat(intelligent-eval): add backend for OpenClaw-driven intelligent evaluation (tickets 01-04)
Introduce 智能评估 as an evaluation paradigm parallel to static evaluation,
driven by OpenClaw. The platform supplies storage, lifecycle, and reporting;
OpenClaw plans and executes.

- Data model: IntelligentEval + Session + Message tables (new, not reusing exploration)
- Lifecycle state machine: draft → planning → pending_approval → executing → completed/cancelled/failed
- Session API: create/message (channel-forwarded)/close with turn accounting
- Report API: pydantic-validated structured report, executing → completed, Markdown export (pure renderer)
- Alembic migration for the three tables; domain glossary added to CONTEXT.md
2026-08-05 03:18:52 +08:00

131 lines
4.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Structured report for intelligent evaluation (票据 04).
Two concerns live here so they stay co-located:
* **Validation** — pydantic models describing the report contract OpenClaw
submits. A malformed body yields a 422 before any state change happens.
* **Rendering** — ``render_report_markdown`` is a pure function (dict in,
string out) with no DB or I/O, so it unit-tests trivially.
"""
from typing import Any, Optional
from pydantic import BaseModel, Field, model_validator
class ReportEvidence(BaseModel):
session_id: str = ""
turn_index: Optional[int] = None
user_said: str = ""
assistant_replied: str = ""
class ReportFinding(BaseModel):
issue: str = Field(min_length=1)
severity: str = Field(min_length=1)
dimension: str = Field(min_length=1)
evidence: list[ReportEvidence] = Field(default_factory=list)
suggestion: Optional[str] = None
related_sop: Optional[str] = None
class ReportHighlight(BaseModel):
description: str = ""
dimension: Optional[str] = None
class ReportModel(BaseModel):
summary: str = Field(min_length=1)
scores: Optional[dict[str, Any]] = None
findings: list[ReportFinding] = Field(default_factory=list)
highlights: list[ReportHighlight] = Field(default_factory=list)
priority_recommendations: list[str] = Field(default_factory=list)
@model_validator(mode="after")
def _require_findings(self) -> "ReportModel":
if not self.findings:
raise ValueError("findings 不能为空")
return self
_SEVERITY_ORDER = {"high": 0, "medium": 1, "low": 2}
def _severity_rank(severity: str) -> int:
return _SEVERITY_ORDER.get((severity or "").lower(), 99)
def render_report_markdown(report: dict[str, Any], *, name: str = "", eval_id: str = "") -> str:
"""Render a validated report dict to Markdown. Pure function, no I/O."""
validated = ReportModel.model_validate(report)
lines: list[str] = []
title = name or "智能评估报告"
lines.append(f"# {title}")
lines.append("")
if eval_id:
lines.append(f"> 评估 ID`{eval_id}`")
lines.append("")
lines.append("## 总体概述")
lines.append("")
lines.append(validated.summary.strip())
lines.append("")
if validated.scores:
lines.append("## 维度评分")
lines.append("")
lines.append("| 维度 | 分数 |")
lines.append("| --- | --- |")
for dimension, score in validated.scores.items():
lines.append(f"| {dimension} | {score} |")
lines.append("")
lines.append("## 问题发现")
lines.append("")
findings = sorted(validated.findings, key=lambda f: _severity_rank(f.severity))
for index, finding in enumerate(findings, start=1):
lines.append(f"### {index}. {finding.issue}")
lines.append("")
lines.append(f"- **严重程度**{finding.severity}")
lines.append(f"- **维度**{finding.dimension}")
if finding.suggestion:
lines.append(f"- **建议**{finding.suggestion}")
if finding.related_sop:
lines.append(f"- **关联 SOP**{finding.related_sop}")
if finding.evidence:
lines.append("")
lines.append("**证据**")
lines.append("")
for ev in finding.evidence:
header_bits = []
if ev.session_id:
header_bits.append(f"会话 `{ev.session_id}`")
if ev.turn_index is not None:
header_bits.append(f"{ev.turn_index}")
header = "" + "".join(header_bits) + "" if header_bits else ""
lines.append(f"> {header}")
if ev.user_said:
lines.append(f"> **用户**{ev.user_said}")
if ev.assistant_replied:
lines.append(f"> **对象**{ev.assistant_replied}")
lines.append("")
lines.append("")
if validated.highlights:
lines.append("## 亮点")
lines.append("")
for highlight in validated.highlights:
suffix = f"{highlight.dimension}" if highlight.dimension else ""
lines.append(f"- {highlight.description}{suffix}")
lines.append("")
if validated.priority_recommendations:
lines.append("## 优先改进建议")
lines.append("")
for recommendation in validated.priority_recommendations:
lines.append(f"- {recommendation}")
lines.append("")
return "\n".join(lines).rstrip() + "\n"