Skip to content

html

agent_cover.reporting.html

HTML Report Generator.

This module is responsible for rendering the visual representation of the coverage. It combines data from the AgentRegistry with the Business Logic rules from the config.

The report is divided into: 1. Summary Cards: High-level percentages for Prompts and Decisions. 2. File Breakdown: Detailed view of every prompt/tool with line numbers and hit status. 3. Business Logic Table: Visual progress bars showing how many expected values were observed.

Functions

generate_html_report(definitions, executions, output_dir='prompt_html', decision_config=None, decision_hits=None, writer_func=None, timestamp=None)

Generates a static HTML website for coverage results.

It creates an index.html file with embedded CSS, making the report portable (single folder, no external dependencies).

Parameters:

Name Type Description Default
definitions Dict[str, Any]

A dictionary containing the static definitions of prompts and tools found in the codebase.

required
executions Set[str]

A set of identifiers representing the code paths that were executed during the run.

required
output_dir str

The directory where the HTML report should be saved. Defaults to "prompt_html".

'prompt_html'
decision_config Optional[Any]

Configuration object containing business logic decisions (YAML based) to be tracked.

None
decision_hits Optional[Dict[str, Set[str]]]

A dictionary mapping decision IDs to sets of observed values, used to calculate coverage of expected values.

None
writer_func Optional[Callable[[str, str], None]]

An optional callable to handle file writing. If None, defaults to writing to the local disk.

None
timestamp Optional[float]

An optional timestamp (float) to use for the report generation time. If None, the current time is used.

None

Returns:

Type Description
str

The absolute path to the output directory where the report was generated.

Source code in src/agent_cover/reporting/html.py
def generate_html_report(
    definitions: Dict[str, Any],
    executions: Set[str],
    output_dir: str = "prompt_html",
    decision_config: Optional[Any] = None,
    decision_hits: Optional[Dict[str, Set[str]]] = None,
    writer_func: Optional[Callable[[str, str], None]] = None,
    timestamp: Optional[float] = None,
) -> str:
    """Generates a static HTML website for coverage results.

    It creates an `index.html` file with embedded CSS, making the report portable
    (single folder, no external dependencies).

    Args:
        definitions: A dictionary containing the static definitions of prompts
            and tools found in the codebase.
        executions: A set of identifiers representing the code paths that were
            executed during the run.
        output_dir: The directory where the HTML report should be saved.
            Defaults to "prompt_html".
        decision_config: Configuration object containing business logic decisions
            (YAML based) to be tracked.
        decision_hits: A dictionary mapping decision IDs to sets of observed values,
            used to calculate coverage of expected values.
        writer_func: An optional callable to handle file writing. If None,
            defaults to writing to the local disk.
        timestamp: An optional timestamp (float) to use for the report generation time.
            If None, the current time is used.

    Returns:
        The absolute path to the output directory where the report was generated.
    """
    if writer_func is None:
        writer_func = _default_file_writer

    # Create actual directory only if using the default writer
    if writer_func == _default_file_writer:
        if not os.path.exists(output_dir):
            os.makedirs(output_dir)

    # Use injected or current timestamp
    time_str = format_iso_time(timestamp)

    # --- 1. PROMPT DATA PREP ---
    prompts = {k: v for k, v in definitions.items() if v.get("type") == "PROMPT"}

    # --- 2. TOOLS DATA PREP ---
    tools = {k: v for k, v in definitions.items() if v.get("type") == "TOOL"}

    # --- HTML GENERATION ---

    # A. PROMPT SECTION
    prompts_html, prompts_summary = _render_file_table(prompts, executions, "Prompts")

    # B. DECISION SECTION
    yaml_html = ""
    yaml_stats = {"total": 0, "covered": 0}
    if decision_config and decision_config.decisions:
        rows = ""
        for dec in decision_config.decisions:
            hits = decision_hits.get(dec.id, set()) if decision_hits else set()

            val_html = ""
            covered_count = 0
            for val in dec.expected_values:
                is_hit = str(val) in hits
                css = "hit" if is_hit else "miss"
                val_html += f'<span class="tag {css}">{val}</span>'
                if is_hit:
                    covered_count += 1

            yaml_stats["total"] += len(dec.expected_values)
            yaml_stats["covered"] += covered_count

            row_pct = (
                (covered_count / len(dec.expected_values) * 100)
                if dec.expected_values
                else 0
            )
            color = "green" if row_pct == 100 else "orange" if row_pct > 50 else "red"

            rows += f"""
            <tr>
                <td><strong>{dec.id}</strong><br><small style="color:#999">{dec.description}</small></td>
                <td><code>{dec.target_field}</code></td>
                <td>
                    <div class="bar-container"><div class="bar {color}" style="width: {row_pct}%"></div></div>
                    <span class="num">{int(row_pct)}%</span>
                </td>
                <td>{val_html}</td>
            </tr>
            """

        yaml_html = f"""
        <h3>🧠 Business Logic (Configured)</h3>
        <table>
            <thead>
                <tr>
                    <th width="30%">Decision ID</th>
                    <th width="15%">Field</th>
                    <th width="20%">Coverage</th>
                    <th>Expected Values</th>
                </tr>
            </thead>
            <tbody>{rows}</tbody>
        </table>
        """

    # B2. Tools Code Table
    tools_html, tools_summary = _render_file_table(tools, executions, "Internal Tools")

    # B3. Unified Decision Summary
    tot_dec = yaml_stats["total"] + tools_summary["total"]
    cov_dec = yaml_stats["covered"] + tools_summary["executed"]
    dec_pct = (cov_dec / tot_dec * 100) if tot_dec > 0 else 0

    decision_summary_card = f"""
    <div class="summary-card card-decision">
        <strong>🧭 Decision Coverage:</strong>
        <span class="badge {"green" if dec_pct > 80 else "red"}">{dec_pct:.1f}%</span>
        <span style="margin-left: 20px; color:#666;">
            (Business Logic: {yaml_stats["covered"]}/{yaml_stats["total"]} items) +
            (Internal Tools: {tools_summary["executed"]}/{tools_summary["total"]} calls)
        </span>
    </div>
    """

    prompt_pct = (
        (prompts_summary["executed"] / prompts_summary["total"] * 100)
        if prompts_summary["total"] > 0
        else 0
    )
    prompt_summary_card = f"""
    <div class="summary-card card-prompt">
        <strong>📝 Prompt Coverage:</strong>
        <span class="badge {"green" if prompt_pct > 80 else "red"}">{prompt_pct:.1f}%</span>
        <span style="margin-left: 20px; color:#666;">
            {prompts_summary["executed"]} / {prompts_summary["total"]} items covered
        </span>
    </div>
    """

    # --- FINAL ASSEMBLY ---
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8">
        <title>Agent Coverage Report</title>
        <style>{CSS}</style>
    </head>
    <body>
        <h1>🛡️ Agent Coverage Report</h1>
        <div class="meta">Generated on {time_str}</div>

        {prompt_summary_card}
        {decision_summary_card}

        <h2>📝 Prompt Details</h2>
        {prompts_html if prompts_summary["total"] > 0 else "<p>No prompts detected.</p>"}

        <h2>🧭 Decision Details</h2>
        {yaml_html}
        {tools_html if tools_summary["total"] > 0 else ""}

        {"<p>No decisions or tools detected.</p>" if (not yaml_html and tools_summary["total"] == 0) else ""}

    </body>
    </html>
    """

    out_path = os.path.join(output_dir, "index.html")
    writer_func(out_path, html_content)

    return os.path.abspath(output_dir)