Source code for core_genai.prompt.report
# -*- coding: utf-8 -*-
"""HTML report builder for prompt evaluation results."""
import html as html_lib
from statistics import mean
from core_genai.prompt.types import EvaluationResult
[docs]
def generate_prompt_evaluation_report(
evaluation_results: list[EvaluationResult],
pass_threshold: int = 7,
) -> str:
"""Build and return a self-contained HTML evaluation report."""
total_tests = len(evaluation_results)
scores = [result["score"] for result in evaluation_results]
avg_score = mean(scores) if scores else 0
pass_rate = (
100 * len([s for s in scores if s >= pass_threshold]) / total_tests
if total_tests
else 0
)
parts = [
f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Prompt Evaluation Report</title>
<style>
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
body {{
font-family: system-ui, -apple-system, 'Segoe UI', sans-serif;
background: #eef6f0;
color: #1a2e1f;
min-height: 100vh;
}}
/* Header */
.page-header {{
background: linear-gradient(135deg, #1d5c3a 0%, #2d7a50 100%);
padding: 40px 48px 36px;
color: #fff;
}}
.page-header h1 {{
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
margin-bottom: 28px;
}}
.stat-row {{
display: flex;
gap: 16px;
flex-wrap: wrap;
}}
.stat-card {{
background: rgba(255,255,255,0.12);
border: 1px solid rgba(255,255,255,0.2);
border-radius: 12px;
padding: 16px 24px;
min-width: 160px;
backdrop-filter: blur(4px);
}}
.stat-label {{
font-size: 0.75rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.06em;
color: rgba(255,255,255,0.7);
margin-bottom: 6px;
}}
.stat-value {{
font-size: 2rem;
font-weight: 700;
color: #fff;
line-height: 1;
}}
.stat-sub {{
font-size: 0.8rem;
color: rgba(255,255,255,0.6);
margin-top: 4px;
}}
/* Content */
.content {{
padding: 32px 48px 48px;
}}
/* Table card */
.table-card {{
background: #fff;
border-radius: 16px;
box-shadow: 0 1px 4px rgba(0,0,0,0.06), 0 4px 16px rgba(0,0,0,0.04);
overflow: hidden;
}}
table {{
width: 100%;
border-collapse: collapse;
}}
thead th {{
background: #f4faf6;
color: #3a6b4a;
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
padding: 14px 16px;
text-align: left;
border-bottom: 1px solid #daeee1;
}}
tbody tr {{
border-bottom: 1px solid #f0f7f2;
transition: background 0.15s;
}}
tbody tr:last-child {{ border-bottom: none; }}
tbody tr:hover {{ background: #f8fcf9; }}
td {{
padding: 16px;
vertical-align: top;
font-size: 0.875rem;
line-height: 1.55;
color: #2c4a35;
}}
.col-scenario {{ width: 14%; font-weight: 600; color: #1a2e1f; }}
.col-inputs {{ width: 20%; }}
.col-criteria {{ width: 16%; }}
.col-output {{ width: 26%; }}
.col-score {{ width: 6%; text-align: center; }}
.col-reasoning {{ width: 18%; color: #4a6b54; }}
/* Score badge */
.badge {{
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
font-size: 0.875rem;
font-weight: 700;
}}
.badge-high {{ background: #d1fae5; color: #065f46; }}
.badge-medium {{ background: #fef3c7; color: #92400e; }}
.badge-low {{ background: #fee2e2; color: #991b1b; }}
/* Prompt inputs */
.input-key {{
font-size: 0.7rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #3a6b4a;
margin-bottom: 2px;
}}
.input-val {{
color: #2c4a35;
margin-bottom: 10px;
word-break: break-word;
}}
.input-val:last-child {{ margin-bottom: 0; }}
/* Criteria list */
.criteria-item {{
display: flex;
gap: 6px;
margin-bottom: 6px;
align-items: flex-start;
}}
.criteria-item:last-child {{ margin-bottom: 0; }}
.criteria-dot {{
flex-shrink: 0;
width: 6px;
height: 6px;
border-radius: 50%;
background: #3a6b4a;
margin-top: 6px;
}}
/* Strengths / weaknesses */
.reasoning-text {{ margin-bottom: 10px; }}
.sw-head {{
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
margin: 8px 0 4px;
}}
.sw-head-pos {{ color: #2d7a50; }}
.sw-head-neg {{ color: #b45309; }}
.sw-list {{ list-style: none; }}
.sw-item {{
position: relative;
padding-left: 14px;
margin-bottom: 4px;
font-size: 0.8rem;
line-height: 1.45;
}}
.sw-item::before {{
content: "";
position: absolute;
left: 0;
top: 7px;
width: 5px;
height: 5px;
border-radius: 50%;
background: currentColor;
}}
/* Output pre */
pre {{
background: #f4faf6;
border: 1px solid #daeee1;
border-radius: 8px;
padding: 10px 12px;
font-family: 'Menlo', 'Consolas', monospace;
font-size: 0.78rem;
line-height: 1.5;
color: #1a2e1f;
white-space: pre-wrap;
word-break: break-word;
overflow-x: auto;
}}
</style>
</head>
<body>
<div class="page-header">
<h1>Prompt Evaluation Report</h1>
<div class="stat-row">
<div class="stat-card">
<div class="stat-label">Test Cases</div>
<div class="stat-value">{total_tests}</div>
</div>
<div class="stat-card">
<div class="stat-label">Average Score</div>
<div class="stat-value">{avg_score:.1f}</div>
<div class="stat-sub">out of 10</div>
</div>
<div class="stat-card">
<div class="stat-label">Pass Rate (≥{pass_threshold})</div>
<div class="stat-value">{pass_rate:.1f}%</div>
</div>
</div>
</div>
<div class="content">
<div class="table-card">
<table>
<thead>
<tr>
<th class="col-scenario">Scenario</th>
<th class="col-inputs">Prompt Inputs</th>
<th class="col-criteria">Criteria</th>
<th class="col-output">Output</th>
<th class="col-score">Score</th>
<th class="col-reasoning">Reasoning</th>
</tr>
</thead>
<tbody>
"""
]
for result in evaluation_results:
prompt_inputs_html = "".join([
f'<div class="input-key">{html_lib.escape(key)}</div>'
f'<div class="input-val">{html_lib.escape(str(value))}</div>'
for key, value in result["test_case"]["prompt_inputs"].items()
])
criteria_html = "".join([
f'<div class="criteria-item"><span class="criteria-dot"></span>'
f'<span>{html_lib.escape(c)}</span></div>'
for c in result["test_case"]["solution_criteria"]
])
score = result["score"]
if score >= pass_threshold:
badge_class = "badge-high"
elif score >= pass_threshold - 2:
badge_class = "badge-medium"
else:
badge_class = "badge-low"
strengths_html = "".join(
f'<li class="sw-item">{html_lib.escape(str(s))}</li>'
for s in result.get("strengths", [])
)
weaknesses_html = "".join(
f'<li class="sw-item">{html_lib.escape(str(w))}</li>'
for w in result.get("weaknesses", [])
)
sw_html = ""
if strengths_html:
sw_html += (
'<div class="sw-head sw-head-pos">Strengths</div>'
f'<ul class="sw-list">{strengths_html}</ul>'
)
if weaknesses_html:
sw_html += (
'<div class="sw-head sw-head-neg">Weaknesses</div>'
f'<ul class="sw-list">{weaknesses_html}</ul>'
)
parts.append(
f""" <tr>
<td class="col-scenario">{html_lib.escape(result["test_case"]["scenario"])}</td>
<td class="col-inputs">{prompt_inputs_html}</td>
<td class="col-criteria">{criteria_html}</td>
<td class="col-output"><pre>{html_lib.escape(result["output"])}</pre></td>
<td class="col-score"><span class="badge {badge_class}">{score}</span></td>
<td class="col-reasoning">
<div class="reasoning-text">{html_lib.escape(result["reasoning"])}</div>
{sw_html}
</td>
</tr>
"""
)
parts.append(
""" </tbody>
</table>
</div>
</div>
</body>
</html>
"""
)
return "".join(parts)