| 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.Net; 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); // Serialize the dataset to JSON, then HTML-encode it for safe embedding in a data attribute. // This pattern avoids XSS vulnerabilities that can occur when embedding JSON directly in script blocks. string json = JsonSerializer.Serialize(dataset, JsonUtilities.Compact.DatasetTypeInfo); string htmlEncodedJson = WebUtility.HtmlEncode(json); using var stream = new FileStream( reportFilePath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: 4096, useAsync: true); using var writer = new StreamWriter(stream, Encoding.UTF8); #if NET await writer.WriteAsync(HtmlTemplateBefore.AsMemory(), cancellationToken).ConfigureAwait(false); await writer.WriteAsync(htmlEncodedJson.AsMemory(), cancellationToken).ConfigureAwait(false); await writer.WriteAsync(HtmlTemplateAfter.AsMemory(), cancellationToken).ConfigureAwait(false); await writer.FlushAsync(cancellationToken).ConfigureAwait(false); #else await writer.WriteAsync(HtmlTemplateBefore).ConfigureAwait(false); await writer.WriteAsync(htmlEncodedJson).ConfigureAwait(false); await writer.WriteAsync(HtmlTemplateAfter).ConfigureAwait(false); await writer.FlushAsync().ConfigureAwait(false); #endif } internal static string HtmlTemplateBefore { get; } internal 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 in the data-dataset attribute on the root div. const string SearchString = "##DATASETS##"; 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 }