File: Formats\Html\HtmlReportWriter.cs
Web Access
Project: src\src\Libraries\Microsoft.Extensions.AI.Evaluation.Reporting\CSharp\Microsoft.Extensions.AI.Evaluation.Reporting.csproj (Microsoft.Extensions.AI.Evaluation.Reporting)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.AI.Evaluation.Reporting.JsonSerialization;
 
namespace Microsoft.Extensions.AI.Evaluation.Reporting.Formats.Html;
 
/// <summary>
/// An <see cref="IEvaluationReportWriter"/> that generates an HTML report containing all the
/// <see cref="EvaluationMetric"/>s present in the supplied <see cref="ScenarioRunResult"/>s and writes it to the
/// specified <paramref name="reportFilePath"/>.
/// </summary>
/// <param name="reportFilePath">
/// The path to a file where the report will be written. If the file already exists, it will be overwritten.
/// </param>
public sealed class HtmlReportWriter(string reportFilePath) : IEvaluationReportWriter
{
    /// <inheritdoc/>
    public async ValueTask WriteReportAsync(
        IEnumerable<ScenarioRunResult> scenarioRunResults,
        CancellationToken cancellationToken = default)
    {
        var dataset =
            new Dataset(
                scenarioRunResults.ToList(),
                createdAt: DateTime.UtcNow,
                generatorVersion: Constants.Version);
 
        using var stream = new FileStream(reportFilePath, FileMode.Create, FileAccess.Write, FileShare.None);
        using var writer = new StreamWriter(stream, Encoding.UTF8);
 
#if NET
        await writer.WriteAsync(HtmlTemplateBefore.AsMemory(), cancellationToken).ConfigureAwait(false);
        await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
#else
        await writer.WriteAsync(HtmlTemplateBefore).ConfigureAwait(false);
        await writer.FlushAsync().ConfigureAwait(false);
#endif
 
        await JsonSerializer.SerializeAsync(
            stream,
            dataset,
            SerializerContext.Compact.Dataset,
            cancellationToken).ConfigureAwait(false);
 
#if NET
        await writer.WriteAsync(HtmlTemplateAfter.AsMemory(), cancellationToken).ConfigureAwait(false);
        await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
#else
        await writer.WriteAsync(HtmlTemplateAfter).ConfigureAwait(false);
        await writer.FlushAsync().ConfigureAwait(false);
#endif
    }
 
    private static string HtmlTemplateBefore { get; }
    private static string HtmlTemplateAfter { get; }
 
#pragma warning disable CA1065, S3877
    // CA1065, S3877: Do not raise exceptions in static constructors.
    // We disable this warning because the exception is only thrown in catastrophic circumstances where we somehow
    // failed to include the html templates in the assembly as part of the build process. This is highly unlikely to
    // happen in practice. If this does happen somehow, it is better to fail fast and loudly.
    static HtmlReportWriter()
    {
        using Stream resourceStream =
            typeof(HtmlReportWriter).Assembly.GetManifestResourceStream("Reporting.HTML.index.html")
                ?? throw new InvalidOperationException("Failed to load HTML template.");
 
        // TASK: Make this more efficient by scanning the stream rather than reading it all into memory.
        using var reader = new StreamReader(resourceStream);
        string all = reader.ReadToEnd();
 
        // This is the placeholder for the results array in the template.
        const string SearchString = @"{scenarioRunResults:[]}"{scenarioRunResults:[]}";
 
        int start = all.IndexOf(SearchString, StringComparison.Ordinal);
        if (start == -1)
        {
            throw new InvalidOperationException($"Placeholder '{SearchString}' not found in the HTML template.");
        }
 
        HtmlTemplateBefore = all.Substring(0, start);
        HtmlTemplateAfter = all.Substring(start + SearchString.Length);
    }
#pragma warning restore CA1065
}