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 (&ge;{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)