-
Notifications
You must be signed in to change notification settings - Fork 588
Fix ToJsonObject serialization failure with anonymous types and add support for custom JsonSerializerOptions #1113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 14 commits
982138e
6a070b3
57b4a93
bcf2e24
7041a89
867d044
bd598ea
32f2549
cb95813
a3cb705
111616b
c1212c6
f46474b
8c94a23
46425b2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,6 +23,7 @@ public static class AIContentExtensions | |
| /// satisfy sampling requests using the specified <see cref="IChatClient"/>. | ||
| /// </summary> | ||
| /// <param name="chatClient">The <see cref="IChatClient"/> with which to satisfy sampling requests.</param> | ||
| /// <param name="serializerOptions">The <see cref="JsonSerializerOptions"/> to use for serializing user-provided objects. If <see langword="null"/>, <see cref="McpJsonUtilities.DefaultOptions"/> is used.</param> | ||
| /// <returns>The created handler delegate that can be assigned to <see cref="McpClientHandlers.SamplingHandler"/>.</returns> | ||
| /// <remarks> | ||
| /// <para> | ||
|
|
@@ -36,10 +37,13 @@ public static class AIContentExtensions | |
| /// </remarks> | ||
| /// <exception cref="ArgumentNullException"><paramref name="chatClient"/> is <see langword="null"/>.</exception> | ||
| public static Func<CreateMessageRequestParams?, IProgress<ProgressNotificationValue>, CancellationToken, ValueTask<CreateMessageResult>> CreateSamplingHandler( | ||
| this IChatClient chatClient) | ||
| this IChatClient chatClient, | ||
| JsonSerializerOptions? serializerOptions = null) | ||
| { | ||
| Throw.IfNull(chatClient); | ||
|
|
||
| serializerOptions ??= McpJsonUtilities.DefaultOptions; | ||
|
|
||
| return async (requestParams, progress, cancellationToken) => | ||
| { | ||
| Throw.IfNull(requestParams); | ||
|
|
@@ -75,7 +79,7 @@ public static class AIContentExtensions | |
| chatResponse.FinishReason == ChatFinishReason.Length ? CreateMessageResult.StopReasonMaxTokens : | ||
| chatResponse.FinishReason == ChatFinishReason.ToolCalls ? CreateMessageResult.StopReasonToolUse : | ||
| chatResponse.FinishReason.ToString(), | ||
| Meta = chatResponse.AdditionalProperties?.ToJsonObject(), | ||
| Meta = chatResponse.AdditionalProperties?.ToJsonObject(serializerOptions), | ||
| Role = lastMessage?.Role == ChatRole.User ? Role.User : Role.Assistant, | ||
| Content = contents, | ||
| }; | ||
|
|
@@ -138,8 +142,10 @@ public static class AIContentExtensions | |
| } | ||
|
|
||
| /// <summary>Converts the specified dictionary to a <see cref="JsonObject"/>.</summary> | ||
| internal static JsonObject? ToJsonObject(this IReadOnlyDictionary<string, object?> properties) => | ||
| JsonSerializer.SerializeToNode(properties, McpJsonUtilities.JsonContext.Default.IReadOnlyDictionaryStringObject) as JsonObject; | ||
| internal static JsonObject? ToJsonObject(this IReadOnlyDictionary<string, object?> properties, JsonSerializerOptions options) | ||
| { | ||
| return JsonSerializer.SerializeToNode(properties, options.GetTypeInfo(typeof(IReadOnlyDictionary<string, object?>))) as JsonObject; | ||
| } | ||
|
|
||
| internal static AdditionalPropertiesDictionary ToAdditionalProperties(this JsonObject obj) | ||
| { | ||
|
|
@@ -181,6 +187,7 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) | |
| /// </summary> | ||
| /// <param name="result">The tool result to convert.</param> | ||
| /// <param name="callId">The identifier for the function call request that triggered the tool invocation.</param> | ||
| /// <param name="options">The <see cref="JsonSerializerOptions"/> to use for serialization. If <see langword="null"/>, <see cref="McpJsonUtilities.DefaultOptions"/> is used.</param> | ||
| /// <returns>A <see cref="ChatMessage"/> object created from the tool result.</returns> | ||
| /// <remarks> | ||
| /// This method transforms a protocol-specific <see cref="CallToolResult"/> from the Model Context Protocol | ||
|
|
@@ -189,12 +196,14 @@ public static ChatMessage ToChatMessage(this PromptMessage promptMessage) | |
| /// serialized <see cref="JsonElement"/>. | ||
| /// </remarks> | ||
| /// <exception cref="ArgumentNullException"><paramref name="result"/> or <paramref name="callId"/> is <see langword="null"/>.</exception> | ||
| public static ChatMessage ToChatMessage(this CallToolResult result, string callId) | ||
| public static ChatMessage ToChatMessage(this CallToolResult result, string callId, JsonSerializerOptions? options = null) | ||
| { | ||
| Throw.IfNull(result); | ||
| Throw.IfNull(callId); | ||
|
|
||
| return new(ChatRole.Tool, [new FunctionResultContent(callId, JsonSerializer.SerializeToElement(result, McpJsonUtilities.JsonContext.Default.CallToolResult)) | ||
| options ??= McpJsonUtilities.DefaultOptions; | ||
|
|
||
| return new(ChatRole.Tool, [new FunctionResultContent(callId, JsonSerializer.SerializeToElement(result, options.GetTypeInfo<CallToolResult>())) | ||
| { | ||
| RawRepresentation = result, | ||
| }]); | ||
|
|
@@ -271,7 +280,7 @@ public static IList<PromptMessage> ToPromptMessages(this ChatMessage chatMessage | |
| EmbeddedResourceBlock resourceContent => resourceContent.Resource.ToAIContent(), | ||
|
|
||
| ToolUseContentBlock toolUse => FunctionCallContent.CreateFromParsedArguments(toolUse.Input, toolUse.Id, toolUse.Name, | ||
| static json => JsonSerializer.Deserialize(json, McpJsonUtilities.JsonContext.Default.IDictionaryStringObject)), | ||
| static json => JsonSerializer.Deserialize(json, McpJsonUtilities.DefaultOptions.GetTypeInfo<IDictionary<string, object?>>())), | ||
|
||
|
|
||
| ToolResultContentBlock toolResult => new FunctionResultContent( | ||
| toolResult.ToolUseId, | ||
|
|
@@ -365,12 +374,15 @@ public static IList<AIContent> ToAIContents(this IEnumerable<ResourceContents> c | |
|
|
||
| /// <summary>Creates a new <see cref="ContentBlock"/> from the content of an <see cref="AIContent"/>.</summary> | ||
| /// <param name="content">The <see cref="AIContent"/> to convert.</param> | ||
| /// <param name="options">The <see cref="JsonSerializerOptions"/> to use for serialization. If <see langword="null"/>, <see cref="McpJsonUtilities.DefaultOptions"/> is used.</param> | ||
| /// <returns>The created <see cref="ContentBlock"/>.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="content"/> is <see langword="null"/>.</exception> | ||
| public static ContentBlock ToContentBlock(this AIContent content) | ||
| public static ContentBlock ToContentBlock(this AIContent content, JsonSerializerOptions? options = null) | ||
stephentoub marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| { | ||
| Throw.IfNull(content); | ||
|
|
||
| options ??= McpJsonUtilities.DefaultOptions; | ||
|
|
||
| ContentBlock contentBlock = content switch | ||
| { | ||
| TextContent textContent => new TextContentBlock | ||
|
|
@@ -404,27 +416,27 @@ public static ContentBlock ToContentBlock(this AIContent content) | |
| { | ||
| Id = callContent.CallId, | ||
| Name = callContent.Name, | ||
| Input = JsonSerializer.SerializeToElement(callContent.Arguments, McpJsonUtilities.DefaultOptions.GetTypeInfo<IDictionary<string, object?>>()!), | ||
| Input = JsonSerializer.SerializeToElement(callContent.Arguments, options.GetTypeInfo<IDictionary<string, object?>>()!), | ||
| }, | ||
|
|
||
| FunctionResultContent resultContent => new ToolResultContentBlock() | ||
| { | ||
| ToolUseId = resultContent.CallId, | ||
| IsError = resultContent.Exception is not null, | ||
| Content = | ||
| resultContent.Result is AIContent c ? [c.ToContentBlock()] : | ||
| resultContent.Result is IEnumerable<AIContent> ec ? [.. ec.Select(c => c.ToContentBlock())] : | ||
| [new TextContentBlock { Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo<object>()) }], | ||
| resultContent.Result is AIContent c ? [c.ToContentBlock(options)] : | ||
| resultContent.Result is IEnumerable<AIContent> ec ? [.. ec.Select(c => c.ToContentBlock(options))] : | ||
| [new TextContentBlock { Text = JsonSerializer.Serialize(content, options.GetTypeInfo<object>()) }], | ||
| StructuredContent = resultContent.Result is JsonElement je ? je : null, | ||
| }, | ||
|
|
||
| _ => new TextContentBlock | ||
| { | ||
| Text = JsonSerializer.Serialize(content, McpJsonUtilities.DefaultOptions.GetTypeInfo(typeof(object))), | ||
| Text = JsonSerializer.Serialize(content, options.GetTypeInfo(typeof(object))), | ||
| } | ||
| }; | ||
|
|
||
| contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(); | ||
| contentBlock.Meta = content.AdditionalProperties?.ToJsonObject(options); | ||
|
|
||
| return contentBlock; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,16 +73,19 @@ public ValueTask<CreateMessageResult> SampleAsync( | |
| /// </summary> | ||
| /// <param name="messages">The messages to send as part of the request.</param> | ||
| /// <param name="chatOptions">The options to use for the request, including model parameters and constraints.</param> | ||
| /// <param name="serializerOptions">The <see cref="JsonSerializerOptions"/> to use for serializing user-provided objects. If <see langword="null"/>, <see cref="McpJsonUtilities.DefaultOptions"/> is used.</param> | ||
| /// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param> | ||
| /// <returns>A task containing the chat response from the model.</returns> | ||
| /// <exception cref="ArgumentNullException"><paramref name="messages"/> is <see langword="null"/>.</exception> | ||
| /// <exception cref="InvalidOperationException">The client does not support sampling.</exception> | ||
| /// <exception cref="McpException">The request failed or the client returned an error response.</exception> | ||
| public async Task<ChatResponse> SampleAsync( | ||
| IEnumerable<ChatMessage> messages, ChatOptions? chatOptions = default, CancellationToken cancellationToken = default) | ||
| IEnumerable<ChatMessage> messages, ChatOptions? chatOptions = default, JsonSerializerOptions? serializerOptions = null, CancellationToken cancellationToken = default) | ||
| { | ||
| Throw.IfNull(messages); | ||
|
|
||
| serializerOptions ??= McpJsonUtilities.DefaultOptions; | ||
|
|
||
| StringBuilder? systemPrompt = null; | ||
|
|
||
| if (chatOptions?.Instructions is { } instructions) | ||
|
|
@@ -148,7 +151,7 @@ public async Task<ChatResponse> SampleAsync( | |
| Name = af.Name, | ||
| Description = af.Description, | ||
| InputSchema = af.JsonSchema, | ||
| Meta = af.AdditionalProperties.ToJsonObject(), | ||
| Meta = af.AdditionalProperties.ToJsonObject(serializerOptions), | ||
| }); | ||
| } | ||
| } | ||
|
|
@@ -172,7 +175,7 @@ public async Task<ChatResponse> SampleAsync( | |
| Temperature = chatOptions?.Temperature, | ||
| ToolChoice = toolChoice, | ||
| Tools = tools, | ||
| Meta = chatOptions?.AdditionalProperties?.ToJsonObject(), | ||
| Meta = chatOptions?.AdditionalProperties?.ToJsonObject(serializerOptions), | ||
| }, cancellationToken).ConfigureAwait(false); | ||
|
|
||
| List<AIContent> responseContents = []; | ||
|
|
@@ -526,7 +529,7 @@ private sealed class SamplingChatClient(McpServer server) : IChatClient | |
|
|
||
| /// <inheritdoc/> | ||
| public Task<ChatResponse> GetResponseAsync(IEnumerable<ChatMessage> messages, ChatOptions? chatOptions = null, CancellationToken cancellationToken = default) => | ||
| _server.SampleAsync(messages, chatOptions, cancellationToken); | ||
| _server.SampleAsync(messages, chatOptions, serializerOptions: null, cancellationToken); | ||
|
||
|
|
||
| /// <inheritdoc/> | ||
| async IAsyncEnumerable<ChatResponseUpdate> IChatClient.GetStreamingResponseAsync( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot, this serializer options needs to be passed into the constructed chart client instance and then used in that instance anywhere a JSO is needed, like the call to sampleasync
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated
CreateSamplingHandlerto pass serializer options through toToChatClientArguments, which now uses them when callingToAIContentin commit 46425b2.