Generated the JSON contracts of the model answers and the Assistant Builder at compile time

This commit is contained in:
Thorsten Sommer committed 2026-11-03 08:57:02 +01:00
1 parent 319612c7c3
commit 4253ca7eab
21 files changed
+150 -18

No files matched your search

+3 -2
View File
@@ -19,10 +19,11 @@ public abstract class AgentBase(ILogger<AgentBase> logger, SettingsManager setti
Time = DateTimeOffset.UtcNow, Time = DateTimeOffset.UtcNow,
}; };
internal static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
}; TypeInfoResolver = AgentJsonContext.Default,
}.AsReadOnly();
protected DataSourceService DataSourceService { get; init; } = dataSourceService; protected DataSourceService DataSourceService { get; init; } = dataSourceService;
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
using AIStudio.Agents.AssistantAudit;
namespace AIStudio.Agents;
/// <summary>
/// The contracts with which System.Text.Json reads the answers of the agents, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(List<SelectedDataSource>))]
[JsonSerializable(typeof(RetrievalContextValidationResult))]
[JsonSerializable(typeof(AssistantAuditResult))]
internal sealed partial class AgentJsonContext : JsonSerializerContext;
@@ -14,13 +14,14 @@ internal sealed class LauncherTextsResponse
{ {
public const string SCHEMA_VERSION_VALUE = "assistant_builder_launcher_texts_v1"; public const string SCHEMA_VERSION_VALUE = "assistant_builder_launcher_texts_v1";
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
AllowTrailingCommas = false, AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow, ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8, MaxDepth = 8,
}; TypeInfoResolver = LauncherTextsResponseJsonContext.Default,
}.AsReadOnly();
public string SchemaVersion { get; set; } = string.Empty; public string SchemaVersion { get; set; } = string.Empty;
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.Builder;
/// <summary>
/// The contracts with which System.Text.Json reads the answer of a model which names a direct chat launcher, built
/// at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(LauncherTextsResponse))]
internal sealed partial class LauncherTextsResponseJsonContext : JsonSerializerContext;
@@ -4,13 +4,14 @@ namespace AIStudio.Assistants.Builder;
internal sealed partial class LuaResponse internal sealed partial class LuaResponse
{ {
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
AllowTrailingCommas = false, AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow, ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 32, MaxDepth = 32,
}; TypeInfoResolver = LuaResponseJsonContext.Default,
}.AsReadOnly();
public static bool TryParse(string modelResponse, out LuaResponse response, out LuaResponseParseError error, out string technicalDetails) public static bool TryParse(string modelResponse, out LuaResponse response, out LuaResponseParseError error, out string technicalDetails)
{ {
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.Builder;
/// <summary>
/// The contracts with which System.Text.Json reads the answer of a model which writes an assistant plugin, built at
/// compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(LuaResponse))]
internal sealed partial class LuaResponseJsonContext : JsonSerializerContext;
@@ -13,13 +13,14 @@ namespace AIStudio.Assistants.ERI;
/// </remarks> /// </remarks>
internal sealed class ERIServerFileList internal sealed class ERIServerFileList
{ {
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.CamelCase, PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
AllowTrailingCommas = false, AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow, ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8, MaxDepth = 8,
}; TypeInfoResolver = ERIServerFileListJsonContext.Default,
}.AsReadOnly();
/// <summary> /// <summary>
/// The paths of the files, relative to the base directory. /// The paths of the files, relative to the base directory.
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.ERI;
/// <summary>
/// The contracts with which System.Text.Json reads the files a model plans for an ERI server, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(ERIServerFileList))]
internal sealed partial class ERIServerFileListJsonContext : JsonSerializerContext;
@@ -17,10 +17,11 @@ namespace AIStudio.Assistants.PromptOptimizer;
public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialogPromptOptimizer> public partial class AssistantPromptOptimizer : AssistantBaseCore<SettingsDialogPromptOptimizer>
{ {
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
}; TypeInfoResolver = PromptOptimizationJsonContext.Default,
}.AsReadOnly();
[Inject] [Inject]
private IDialogService DialogService { get; init; } = null!; private IDialogService DialogService { get; init; } = null!;
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.PromptOptimizer;
/// <summary>
/// The contracts with which System.Text.Json reads the answer of the prompt optimizer, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(PromptOptimizationResult))]
internal sealed partial class PromptOptimizationJsonContext : JsonSerializerContext;
@@ -16,10 +16,11 @@ namespace AIStudio.Assistants.SWOTAnalysis;
internal static class SwotAnalysisModeration internal static class SwotAnalysisModeration
{ {
// Internal rather than private, so that a test can check the JSON the moderator gets: // Internal rather than private, so that a test can check the JSON the moderator gets:
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
WriteIndented = true, WriteIndented = true,
}; TypeInfoResolver = SwotModerationJsonContext.Default,
}.AsReadOnly();
public static string BuildSystemPrompt(string outputLanguageInstruction) => public static string BuildSystemPrompt(string outputLanguageInstruction) =>
$""" $"""
@@ -8,13 +8,14 @@ namespace AIStudio.Assistants.SWOTAnalysis;
/// </summary> /// </summary>
internal static class SwotModelAnswer internal static class SwotModelAnswer
{ {
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true, AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip, ReadCommentHandling = JsonCommentHandling.Skip,
RespectNullableAnnotations = true, RespectNullableAnnotations = true,
}; TypeInfoResolver = SwotModelAnswerJsonContext.Default,
}.AsReadOnly();
/// <summary> /// <summary>
/// Reads the JSON object of a model answer into the given type, without checking its content. /// Reads the JSON object of a model answer into the given type, without checking its content.
@@ -0,0 +1,12 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.SWOTAnalysis;
/// <summary>
/// The contracts with which System.Text.Json reads the answers of the models which work on a SWOT analysis, built at
/// compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(SwotCategoryAnalysisResult))]
[JsonSerializable(typeof(SwotModerationResult))]
internal sealed partial class SwotModelAnswerJsonContext : JsonSerializerContext;
@@ -0,0 +1,11 @@
using System.Text.Json.Serialization;
namespace AIStudio.Assistants.SWOTAnalysis;
/// <summary>
/// The contracts with which System.Text.Json writes the preliminary results the moderator of a SWOT analysis gets,
/// built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Dictionary<string, SwotModerationCategoryInput>))]
internal sealed partial class SwotModerationJsonContext : JsonSerializerContext;
@@ -0,0 +1,10 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.PowerPoint;
/// <summary>
/// The contracts with which System.Text.Json reads the slides a model plans, built at compile time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(SlideDeckSpec.DeckDto))]
internal sealed partial class SlideDeckJsonContext : JsonSerializerContext;
@@ -7,7 +7,7 @@ namespace AIStudio.Tools.PowerPoint;
internal sealed partial record SlideDeckSpec internal sealed partial record SlideDeckSpec
{ {
internal static readonly JsonSerializerOptions JSON_OPTIONS = new() internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true, PropertyNameCaseInsensitive = true,
@@ -15,7 +15,8 @@ internal sealed partial record SlideDeckSpec
AllowTrailingCommas = true, AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip, ReadCommentHandling = JsonCommentHandling.Skip,
MaxDepth = 16, MaxDepth = 16,
}; TypeInfoResolver = SlideDeckJsonContext.Default,
}.AsReadOnly();
/// <summary> /// <summary>
/// The deepest list level a paragraph may have; PowerPoint knows nine, starting at 0. /// The deepest list level a paragraph may have; PowerPoint knows nine, starting at 0.
@@ -0,0 +1,14 @@
using System.Text.Json.Serialization;
namespace AIStudio.Tools.Services;
/// <summary>
/// The contracts with which System.Text.Json writes what the Assistant Builder hands to its models, built at compile
/// time.
/// </summary>
[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(AssistantBuilderDraftPromptData))]
[JsonSerializable(typeof(AssistantBuilderGenerationPromptData))]
[JsonSerializable(typeof(AssistantBuilderLauncherPromptData))]
[JsonSerializable(typeof(AssistantBuilderRevisionPromptData))]
internal sealed partial class AssistantBuilderPromptJsonContext : JsonSerializerContext;
@@ -19,11 +19,12 @@ public sealed class AssistantPluginGenerationService(ToolRegistry toolRegistry,
private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginGenerationService).Namespace, nameof(AssistantPluginGenerationService)); private static string TB(string fallbackEN) => I18N.I.T(fallbackEN, typeof(AssistantPluginGenerationService).Namespace, nameof(AssistantPluginGenerationService));
// Internal rather than private, so that a test can check the JSON the models get: // Internal rather than private, so that a test can check the JSON the models get:
internal static readonly JsonSerializerOptions UNTRUSTED_PROMPT_JSON_OPTIONS = new() internal static readonly JsonSerializerOptions UNTRUSTED_PROMPT_JSON_OPTIONS = new JsonSerializerOptions
{ {
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
WriteIndented = true, WriteIndented = true,
}; TypeInfoResolver = AssistantBuilderPromptJsonContext.Default,
}.AsReadOnly();
private const string LUA_RESPONSE_SCHEMA_PATH = "Assistants/Builder/AssistantBuilderLuaResponse.schema.json"; private const string LUA_RESPONSE_SCHEMA_PATH = "Assistants/Builder/AssistantBuilderLuaResponse.schema.json";
private const string DEFAULT_VERSION = "1.0.0"; private const string DEFAULT_VERSION = "1.0.0";
@@ -22,6 +22,7 @@ public sealed class JsonContractTests
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelPrompts))]
public void TheContractsMatchThoseOfReflection(JsonTestRoot root) public void TheContractsMatchThoseOfReflection(JsonTestRoot root)
{ {
var baseline = JsonReflectionBaseline.Of(root.Options); var baseline = JsonReflectionBaseline.Of(root.Options);
@@ -23,6 +23,8 @@ public sealed class JsonCoverageTests
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingStored))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingStored))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingHashed))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))] [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelPrompts))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root) public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{ {
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options) var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
@@ -5,6 +5,7 @@ using AIStudio.Assistants.ERI;
using AIStudio.Assistants.PromptOptimizer; using AIStudio.Assistants.PromptOptimizer;
using AIStudio.Assistants.SWOTAnalysis; using AIStudio.Assistants.SWOTAnalysis;
using AIStudio.Tools.PowerPoint; using AIStudio.Tools.PowerPoint;
using AIStudio.Tools.Services;
namespace AIStudio.Tests.Serialization; namespace AIStudio.Tests.Serialization;
@@ -33,4 +34,20 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(ERIServerFileList), ERIServerFileList.JSON_OPTIONS)); yield return Case(new(typeof(ERIServerFileList), ERIServerFileList.JSON_OPTIONS));
yield return Case(new(typeof(SlideDeckSpec.DeckDto), SlideDeckSpec.JSON_OPTIONS)); yield return Case(new(typeof(SlideDeckSpec.DeckDto), SlideDeckSpec.JSON_OPTIONS));
} }
/// <summary>
/// The JSON which the Assistant Builder and the moderation of a SWOT analysis put into their prompts.
/// </summary>
public static IEnumerable<TestCaseData> ModelPrompts()
{
TestHost.PrepareStaticState();
var untrusted = AssistantPluginGenerationService.UNTRUSTED_PROMPT_JSON_OPTIONS;
yield return Case(new(typeof(AssistantBuilderDraftPromptData), untrusted));
yield return Case(new(typeof(AssistantBuilderGenerationPromptData), untrusted));
yield return Case(new(typeof(AssistantBuilderLauncherPromptData), untrusted));
yield return Case(new(typeof(AssistantBuilderRevisionPromptData), untrusted));
yield return Case(new(typeof(Dictionary<string, SwotModerationCategoryInput>), SwotAnalysisModeration.JSON_OPTIONS));
}
} }