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,
};
internal static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_SERIALIZER_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
};
TypeInfoResolver = AgentJsonContext.Default,
}.AsReadOnly();
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";
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8,
};
TypeInfoResolver = LauncherTextsResponseJsonContext.Default,
}.AsReadOnly();
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 static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 32,
};
TypeInfoResolver = LuaResponseJsonContext.Default,
}.AsReadOnly();
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>
internal sealed class ERIServerFileList
{
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
AllowTrailingCommas = false,
ReadCommentHandling = JsonCommentHandling.Disallow,
MaxDepth = 8,
};
TypeInfoResolver = ERIServerFileListJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// 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>
{
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
};
TypeInfoResolver = PromptOptimizationJsonContext.Default,
}.AsReadOnly();
[Inject]
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 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,
};
TypeInfoResolver = SwotModerationJsonContext.Default,
}.AsReadOnly();
public static string BuildSystemPrompt(string outputLanguageInstruction) =>
$"""
@@ -8,13 +8,14 @@ namespace AIStudio.Assistants.SWOTAnalysis;
/// </summary>
internal static class SwotModelAnswer
{
internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true,
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
RespectNullableAnnotations = true,
};
TypeInfoResolver = SwotModelAnswerJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// 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 static readonly JsonSerializerOptions JSON_OPTIONS = new()
internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
PropertyNameCaseInsensitive = true,
@@ -15,7 +15,8 @@ internal sealed partial record SlideDeckSpec
AllowTrailingCommas = true,
ReadCommentHandling = JsonCommentHandling.Skip,
MaxDepth = 16,
};
TypeInfoResolver = SlideDeckJsonContext.Default,
}.AsReadOnly();
/// <summary>
/// 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));
// 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,
WriteIndented = true,
};
TypeInfoResolver = AssistantBuilderPromptJsonContext.Default,
}.AsReadOnly();
private const string LUA_RESPONSE_SCHEMA_PATH = "Assistants/Builder/AssistantBuilderLuaResponse.schema.json";
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.VisualBriefingAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelPrompts))]
public void TheContractsMatchThoseOfReflection(JsonTestRoot root)
{
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.VisualBriefingHashed))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.VisualBriefingAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelAnswers))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ModelPrompts))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
@@ -5,6 +5,7 @@ using AIStudio.Assistants.ERI;
using AIStudio.Assistants.PromptOptimizer;
using AIStudio.Assistants.SWOTAnalysis;
using AIStudio.Tools.PowerPoint;
using AIStudio.Tools.Services;
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(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));
}
}