diff --git a/app/MindWork AI Studio/Tools/Services/RustContentStreamJsonContext.cs b/app/MindWork AI Studio/Tools/Services/RustContentStreamJsonContext.cs
new file mode 100644
index 000000000..449593c85
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/RustContentStreamJsonContext.cs
@@ -0,0 +1,24 @@
+using System.Text.Json.Serialization;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// The contracts with which System.Text.Json reads the events of a file extraction from the runtime, built at
+/// compile time.
+///
+///
+/// A converter reads the metadata of an event as the type its JSON tells, so each of these types needs a
+/// contract of its own here. A test fails for one which is missing.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+[JsonSerializable(typeof(ContentStreamSseEvent))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(ContentStreamTextMetadata))]
+[JsonSerializable(typeof(ContentStreamPdfMetadata))]
+[JsonSerializable(typeof(ContentStreamSpreadsheetMetadata))]
+[JsonSerializable(typeof(ContentStreamPresentationMetadata))]
+[JsonSerializable(typeof(ContentStreamImageMetadata))]
+[JsonSerializable(typeof(ContentStreamDocumentMetadata))]
+[JsonSerializable(typeof(ContentStreamErrorMetadata))]
+[JsonSerializable(typeof(ContentStreamPromptInjectionMetadata))]
+internal sealed partial class RustContentStreamJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustJsonContext.cs b/app/MindWork AI Studio/Tools/Services/RustJsonContext.cs
new file mode 100644
index 000000000..c0ec6ae74
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/RustJsonContext.cs
@@ -0,0 +1,89 @@
+using System.Text.Json.Serialization;
+
+using AIStudio.Tools.Databases.VectorStore;
+using AIStudio.Tools.Rust;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// The contracts with which System.Text.Json reads and writes nearly every call to the runtime, built at
+/// compile time.
+///
+///
+/// The calls to the vector store are generic: every query of its client needs the answer type with the type
+/// of its result, e.g., DatabaseQueryResponse<bool?>. A test fails for a request or an answer which is
+/// missing here.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+
+// The requests:
+[JsonSerializable(typeof(SelectSecretRequest))]
+[JsonSerializable(typeof(StoreSecretRequest))]
+[JsonSerializable(typeof(RustService.ShareFileRequest))]
+[JsonSerializable(typeof(PreviousDirectory))]
+[JsonSerializable(typeof(SelectFileOptions))]
+[JsonSerializable(typeof(SaveFileOptions))]
+[JsonSerializable(typeof(OpenPathRequest))]
+[JsonSerializable(typeof(OpenDocumentRequest))]
+[JsonSerializable(typeof(RegisterShortcutRequest))]
+[JsonSerializable(typeof(ValidateShortcutRequest))]
+[JsonSerializable(typeof(SetInterfaceSizeRequest))]
+[JsonSerializable(typeof(CreateMediaJobRequest))]
+[JsonSerializable(typeof(LogEventRequest))]
+[JsonSerializable(typeof(ValidateTokenizerRequest))]
+[JsonSerializable(typeof(StoreTokenizerRequest))]
+[JsonSerializable(typeof(DeleteTokenizerRequest))]
+[JsonSerializable(typeof(CountTokensRequest))]
+[JsonSerializable(typeof(PrepareImageRequest))]
+[JsonSerializable(typeof(PrepareImageForModelRequest))]
+
+// The answers:
+[JsonSerializable(typeof(RequestedSecret))]
+[JsonSerializable(typeof(StoreSecretResponse))]
+[JsonSerializable(typeof(DeleteSecretResponse))]
+[JsonSerializable(typeof(ShareFileResponse))]
+[JsonSerializable(typeof(ImagePrepareResponse))]
+[JsonSerializable(typeof(PreparedModelImage))]
+[JsonSerializable(typeof(TauriEvent))]
+[JsonSerializable(typeof(UpdateResponse))]
+[JsonSerializable(typeof(SetClipboardResponse))]
+[JsonSerializable(typeof(DirectorySelectionResponse))]
+[JsonSerializable(typeof(FileSelectionResponse))]
+[JsonSerializable(typeof(FilesSelectionResponse))]
+[JsonSerializable(typeof(FileSaveResponse))]
+[JsonSerializable(typeof(OpenPathResponse))]
+[JsonSerializable(typeof(OpenDocumentResponse))]
+[JsonSerializable(typeof(ShortcutRegistrationResult))]
+[JsonSerializable(typeof(ShortcutValidationResponse))]
+[JsonSerializable(typeof(ShortcutResponse))]
+[JsonSerializable(typeof(RuntimeInfoResponse))]
+[JsonSerializable(typeof(AppExitResponse))]
+[JsonSerializable(typeof(AppRestartResponse))]
+[JsonSerializable(typeof(InterfaceSizeResponse))]
+[JsonSerializable(typeof(CreateMediaJobResponse))]
+[JsonSerializable(typeof(MediaJobEvent))]
+[JsonSerializable(typeof(List))]
+[JsonSerializable(typeof(GetLogPathsResponse))]
+[JsonSerializable(typeof(TokenizerResponse))]
+
+// The vector store, with the result of every query its client sends:
+[JsonSerializable(typeof(QdrantEdgeInfo))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.EnsureVectorStoreRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.CheckVectorStoreRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.InsertEmbeddingRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.SearchEmbeddingRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.RetrieveVectorsRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteEmbeddingByFileRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.RelocateEmbeddingFileRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.OptimizeVectorStoreRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.DeleteVectorStoreRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.PrepareBackupRequest))]
+[JsonSerializable(typeof(QdrantEdgeClientImplementation.CopyStoreForBackupRequest))]
+[JsonSerializable(typeof(RustService.DatabaseOperationResponse))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse>))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse>))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse))]
+[JsonSerializable(typeof(RustService.DatabaseQueryResponse))]
+internal sealed partial class RustJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustPromptInjectionJsonContext.cs b/app/MindWork AI Studio/Tools/Services/RustPromptInjectionJsonContext.cs
new file mode 100644
index 000000000..4e0a74a4b
--- /dev/null
+++ b/app/MindWork AI Studio/Tools/Services/RustPromptInjectionJsonContext.cs
@@ -0,0 +1,16 @@
+using System.Text.Json.Serialization;
+
+using AIStudio.Tools.Rust;
+
+namespace AIStudio.Tools.Services;
+
+///
+/// The contracts with which System.Text.Json reads and writes the calls to the prompt injection filter of the
+/// runtime, built at compile time.
+///
+[JsonSourceGenerationOptions(GenerationMode = JsonSourceGenerationMode.Metadata)]
+[JsonSerializable(typeof(SanitizePromptInjectionsRequest))]
+[JsonSerializable(typeof(SanitizePromptInjectionsResponse))]
+[JsonSerializable(typeof(SanitizePromptInjectionsBatchRequest))]
+[JsonSerializable(typeof(SanitizePromptInjectionsBatchResponse))]
+internal sealed partial class RustPromptInjectionJsonContext : JsonSerializerContext;
\ No newline at end of file
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
index 220a72105..0954589f0 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Retrieval.cs
@@ -112,7 +112,7 @@ public sealed partial class RustService
try
{
- var sseEvent = JsonSerializer.Deserialize(jsonContent);
+ var sseEvent = JsonSerializer.Deserialize(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
if (sseEvent is null)
continue;
@@ -364,7 +364,7 @@ public sealed partial class RustService
ContentStreamSseEvent? sseEvent = null;
try
{
- sseEvent = JsonSerializer.Deserialize(jsonContent);
+ sseEvent = JsonSerializer.Deserialize(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
}
catch (JsonException)
{
@@ -460,7 +460,7 @@ public sealed partial class RustService
{
try
{
- var errorMessage = JsonSerializer.Deserialize(jsonContent);
+ var errorMessage = JsonSerializer.Deserialize(jsonContent, CONTENT_STREAM_JSON_OPTIONS);
if (string.IsNullOrWhiteSpace(errorMessage))
return false;
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.Security.cs b/app/MindWork AI Studio/Tools/Services/RustService.Security.cs
index 07b829aed..ac831ef76 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.Security.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.Security.cs
@@ -40,7 +40,8 @@ public sealed partial class RustService
using var response = await this.http.PostAsJsonAsync(
"/security/prompt-injection/sanitize",
new SanitizePromptInjectionsRequest(text),
- cancellationToken: timeoutTokenSource.Token);
+ PROMPT_INJECTION_JSON_OPTIONS,
+ timeoutTokenSource.Token);
if (!response.IsSuccessStatusCode)
{
@@ -48,7 +49,7 @@ public sealed partial class RustService
return null;
}
- return await response.Content.ReadFromJsonAsync(timeoutTokenSource.Token);
+ return await response.Content.ReadFromJsonAsync(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
}
catch (Exception exception)
{
@@ -82,7 +83,8 @@ public sealed partial class RustService
using var response = await this.http.PostAsJsonAsync(
"/security/prompt-injection/sanitize-batch",
new SanitizePromptInjectionsBatchRequest(texts),
- cancellationToken: timeoutTokenSource.Token);
+ PROMPT_INJECTION_JSON_OPTIONS,
+ timeoutTokenSource.Token);
if (!response.IsSuccessStatusCode)
{
@@ -90,7 +92,7 @@ public sealed partial class RustService
return null;
}
- var batchResponse = await response.Content.ReadFromJsonAsync(timeoutTokenSource.Token);
+ var batchResponse = await response.Content.ReadFromJsonAsync(PROMPT_INJECTION_JSON_OPTIONS, timeoutTokenSource.Token);
if (batchResponse.Results is null || batchResponse.Results.Count != texts.Count)
{
this.logger?.LogError("The prompt injection filter answered with {ResultCount} result(s) for {TextCount} text(s).", batchResponse.Results?.Count ?? 0, texts.Count);
diff --git a/app/MindWork AI Studio/Tools/Services/RustService.cs b/app/MindWork AI Studio/Tools/Services/RustService.cs
index 8fb76002c..8e849379a 100644
--- a/app/MindWork AI Studio/Tools/Services/RustService.cs
+++ b/app/MindWork AI Studio/Tools/Services/RustService.cs
@@ -37,15 +37,38 @@ public sealed partial class RustService : BackgroundService
///
/// The JSON options of nearly every call to the runtime, which names its fields in snake case.
///
- internal static readonly JsonSerializerOptions JSON_OPTIONS = new()
+ internal static readonly JsonSerializerOptions JSON_OPTIONS = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower,
Converters =
{
new RustEnumConverter(),
},
- };
-
+ TypeInfoResolver = RustJsonContext.Default,
+ }.AsReadOnly();
+
+ ///
+ /// The JSON options of the events of a file extraction, which name their fields themselves.
+ ///
+ ///
+ /// The same as the defaults of System.Text.Json, with which these events were read before.
+ ///
+ internal static readonly JsonSerializerOptions CONTENT_STREAM_JSON_OPTIONS = new JsonSerializerOptions
+ {
+ TypeInfoResolver = RustContentStreamJsonContext.Default,
+ }.AsReadOnly();
+
+ ///
+ /// The JSON options of the prompt injection filter, which names its fields in camel case.
+ ///
+ ///
+ /// The same as the defaults of the JSON calls of HttpClient, with which the filter was called before.
+ ///
+ internal static readonly JsonSerializerOptions PROMPT_INJECTION_JSON_OPTIONS = new JsonSerializerOptions(JsonSerializerDefaults.Web)
+ {
+ TypeInfoResolver = RustPromptInjectionJsonContext.Default,
+ }.AsReadOnly();
+
private ILogger? logger;
private Encryption? encryptor;
private string? cachedUserLanguage;
diff --git a/app/Tests/Serialization/JsonCoverageTests.cs b/app/Tests/Serialization/JsonCoverageTests.cs
index c6329a0ab..e40776b90 100644
--- a/app/Tests/Serialization/JsonCoverageTests.cs
+++ b/app/Tests/Serialization/JsonCoverageTests.cs
@@ -16,6 +16,8 @@ public sealed class JsonCoverageTests
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.Stored))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderRequests))]
[TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.ProviderAnswers))]
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeRequests))]
+ [TestCaseSource(typeof(JsonTestRoots), nameof(JsonTestRoots.RuntimeAnswers))]
public void EveryContractComesFromSourceGeneration(JsonTestRoot root)
{
var typesWithoutGeneratedContract = JsonTypeGraph.Collect(root.Type, root.Options)
diff --git a/app/Tests/Serialization/JsonTestRoots.Runtime.cs b/app/Tests/Serialization/JsonTestRoots.Runtime.cs
index b0d827989..8cc355fa5 100644
--- a/app/Tests/Serialization/JsonTestRoots.Runtime.cs
+++ b/app/Tests/Serialization/JsonTestRoots.Runtime.cs
@@ -1,5 +1,4 @@
using System.Reflection;
-using System.Text.Json;
using AIStudio.Tools;
using AIStudio.Tools.Databases.VectorStore;
@@ -42,9 +41,9 @@ public static partial class JsonTestRoots
foreach (var type in typeof(QdrantEdgeClientImplementation).GetNestedTypes(BindingFlags.NonPublic).Where(type => type.Name.EndsWith("Request", StringComparison.Ordinal)).OrderBy(type => type.Name, StringComparer.Ordinal))
yield return Case(new(type, options));
- // The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
- yield return Case(new(typeof(SanitizePromptInjectionsRequest), JsonSerializerOptions.Web));
- yield return Case(new(typeof(SanitizePromptInjectionsBatchRequest), JsonSerializerOptions.Web));
+ // The prompt injection filter, which names its fields in camel case:
+ yield return Case(new(typeof(SanitizePromptInjectionsRequest), RustService.PROMPT_INJECTION_JSON_OPTIONS));
+ yield return Case(new(typeof(SanitizePromptInjectionsBatchRequest), RustService.PROMPT_INJECTION_JSON_OPTIONS));
}
///
@@ -94,16 +93,16 @@ public static partial class JsonTestRoots
yield return Case(new(typeof(RustService.DatabaseQueryResponse), options));
//
- // The events of a file extraction, which get read with the defaults of System.Text.Json. A converter reads
- // their metadata as the type the JSON tells:
+ // The events of a file extraction, which name their fields themselves. A converter reads their metadata as
+ // the type the JSON tells:
//
- yield return Case(new(typeof(ContentStreamSseEvent), JsonSerializerOptions.Default));
- yield return Case(new(typeof(string), JsonSerializerOptions.Default), "error of a file extraction");
+ yield return Case(new(typeof(ContentStreamSseEvent), RustService.CONTENT_STREAM_JSON_OPTIONS));
+ yield return Case(new(typeof(string), RustService.CONTENT_STREAM_JSON_OPTIONS), "error of a file extraction");
foreach (var type in ImplementationsOf(typeof(ContentStreamSseMetadata)))
- yield return Case(new(type, JsonSerializerOptions.Default));
+ yield return Case(new(type, RustService.CONTENT_STREAM_JSON_OPTIONS));
- // The prompt injection filter, which gets called with the defaults of HttpClient, i.e., with camel case:
- yield return Case(new(typeof(SanitizePromptInjectionsResponse), JsonSerializerOptions.Web));
- yield return Case(new(typeof(SanitizePromptInjectionsBatchResponse), JsonSerializerOptions.Web));
+ // The prompt injection filter, which names its fields in camel case:
+ yield return Case(new(typeof(SanitizePromptInjectionsResponse), RustService.PROMPT_INJECTION_JSON_OPTIONS));
+ yield return Case(new(typeof(SanitizePromptInjectionsBatchResponse), RustService.PROMPT_INJECTION_JSON_OPTIONS));
}
}
\ No newline at end of file