diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs index e7b535e6995..ed94862a77c 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/ChatResponseExtensions.cs @@ -293,7 +293,7 @@ internal static void CoalesceContent(IList contents) Coalesce( contents, mergeSingle: true, - canMerge: static (r1, r2) => r1.CallId == r2.CallId, + canMerge: static (r1, r2) => r1.Id == r2.Id, static (contents, start, end) => { var firstContent = (CodeInterpreterToolCallContent)contents[start]; @@ -320,9 +320,8 @@ internal static void CoalesceContent(IList contents) CoalesceContent(inputs); } - return new() + return new(firstContent.Id) { - CallId = firstContent.CallId, Inputs = inputs, AdditionalProperties = firstContent.AdditionalProperties?.Clone(), }; @@ -331,7 +330,7 @@ internal static void CoalesceContent(IList contents) Coalesce( contents, mergeSingle: true, - canMerge: static (r1, r2) => r1.CallId is not null && r2.CallId is not null && r1.CallId == r2.CallId, + canMerge: static (r1, r2) => r1.Id == r2.Id, static (contents, start, end) => { var firstContent = (CodeInterpreterToolResultContent)contents[start]; @@ -358,9 +357,8 @@ internal static void CoalesceContent(IList contents) CoalesceContent(output); } - return new() + return new(firstContent.Id) { - CallId = firstContent.CallId, Outputs = output, AdditionalProperties = firstContent.AdditionalProperties?.Clone(), }; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index af8b19c8d84..dcce8b52e8d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -32,6 +32,7 @@ namespace Microsoft.Extensions.AI; // [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] // [JsonDerivedType(typeof(CodeInterpreterToolCallContent), typeDiscriminator: "codeInterpreterToolCall")] // [JsonDerivedType(typeof(CodeInterpreterToolResultContent), typeDiscriminator: "codeInterpreterToolResult")] +// [JsonDerivedType(typeof(ServiceActionContent), typeDiscriminator: "serviceAction")] public class AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs index 31681b171be..be6eb004b33 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolCallContent.cs @@ -14,20 +14,14 @@ namespace Microsoft.Extensions.AI; /// It is informational only and represents the call itself, not the result. /// [Experimental("MEAI001")] -public sealed class CodeInterpreterToolCallContent : AIContent +public sealed class CodeInterpreterToolCallContent : ServiceActionContent { - /// - /// Initializes a new instance of the class. - /// - public CodeInterpreterToolCallContent() + /// + public CodeInterpreterToolCallContent(string id) + : base(id) { } - /// - /// Gets or sets the tool call ID. - /// - public string? CallId { get; set; } - /// /// Gets or sets the inputs to the code interpreter tool. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs index 486ee7072ea..e428808fc52 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/CodeInterpreterToolResultContent.cs @@ -10,20 +10,14 @@ namespace Microsoft.Extensions.AI; /// Represents the result of a code interpreter tool invocation by a hosted service. /// [Experimental("MEAI001")] -public sealed class CodeInterpreterToolResultContent : AIContent +public sealed class CodeInterpreterToolResultContent : ServiceActionContent { - /// - /// Initializes a new instance of the class. - /// - public CodeInterpreterToolResultContent() + /// + public CodeInterpreterToolResultContent(string id) + : base(id) { } - /// - /// Gets or sets the tool call ID that this result corresponds to. - /// - public string? CallId { get; set; } - /// /// Gets or sets the output of code interpreter tool. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs index 3283c09a7ee..54d485a0fe5 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -16,28 +16,23 @@ namespace Microsoft.Extensions.AI; /// It is informational only. /// [Experimental("MEAI001")] -public sealed class McpServerToolCallContent : AIContent +public sealed class McpServerToolCallContent : ServiceActionContent { /// /// Initializes a new instance of the class. /// - /// The tool call ID. + /// The tool call ID. /// The tool name. /// The MCP server name that hosts the tool. - /// or is . - /// or is empty or composed entirely of whitespace. - public McpServerToolCallContent(string callId, string toolName, string? serverName) + /// or is . + /// or is empty or composed entirely of whitespace. + public McpServerToolCallContent(string id, string toolName, string? serverName) + : base(id) { - CallId = Throw.IfNullOrWhitespace(callId); ToolName = Throw.IfNullOrWhitespace(toolName); ServerName = serverName; } - /// - /// Gets the tool call ID. - /// - public string CallId { get; } - /// /// Gets the name of the tool called. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs index b8329c74d99..279fce57c64 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -1,10 +1,8 @@ // 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.Diagnostics.CodeAnalysis; -using Microsoft.Shared.Diagnostics; namespace Microsoft.Extensions.AI; @@ -16,24 +14,14 @@ namespace Microsoft.Extensions.AI; /// It is informational only. /// [Experimental("MEAI001")] -public sealed class McpServerToolResultContent : AIContent +public sealed class McpServerToolResultContent : ServiceActionContent { - /// - /// Initializes a new instance of the class. - /// - /// The tool call ID. - /// is . - /// is empty or composed entirely of whitespace. - public McpServerToolResultContent(string callId) + /// + public McpServerToolResultContent(string id) + : base(id) { - CallId = Throw.IfNullOrWhitespace(callId); } - /// - /// Gets the tool call ID. - /// - public string CallId { get; } - /// /// Gets or sets the output of the tool call. /// diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ServiceActionContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ServiceActionContent.cs new file mode 100644 index 00000000000..24fe5ba5ef2 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/ServiceActionContent.cs @@ -0,0 +1,35 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.Shared.Diagnostics; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents an action performed by a hosted service. +/// +/// +/// This content type is used to represent actions performed by the service such as calls to other services or invocation of service tools. +/// It is informational only. +/// +[Experimental("MEAI001")] +public class ServiceActionContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID for the service-side action. + /// is . + /// is empty or composed entirely of whitespace. + public ServiceActionContent(string id) + { + Id = Throw.IfNullOrWhitespace(id); + } + + /// + /// Gets the ID for the service-side action. + /// + public string Id { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs index d01294836bc..3a78f4fee14 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Utilities/AIJsonUtilities.Defaults.cs @@ -59,6 +59,7 @@ private static JsonSerializerOptions CreateDefaultOptions() AddAIContentType(options, typeof(McpServerToolApprovalResponseContent), typeDiscriminatorId: "mcpServerToolApprovalResponse", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolCallContent), typeDiscriminatorId: "codeInterpreterToolCall", checkBuiltIn: false); AddAIContentType(options, typeof(CodeInterpreterToolResultContent), typeDiscriminatorId: "codeInterpreterToolResult", checkBuiltIn: false); + AddAIContentType(options, typeof(ServiceActionContent), typeDiscriminatorId: "serviceAction", checkBuiltIn: false); if (JsonSerializer.IsReflectionEnabledByDefault) { @@ -133,6 +134,7 @@ private static JsonSerializerOptions CreateDefaultOptions() [JsonSerializable(typeof(McpServerToolApprovalResponseContent))] [JsonSerializable(typeof(CodeInterpreterToolCallContent))] [JsonSerializable(typeof(CodeInterpreterToolResultContent))] + [JsonSerializable(typeof(ServiceActionContent))] [JsonSerializable(typeof(ResponseContinuationToken))] // IEmbeddingGenerator diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs index 065ad80d23a..b5657befccb 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantsChatClient.cs @@ -199,9 +199,8 @@ public async IAsyncEnumerable GetStreamingResponseAsync( case RunStepDetailsUpdate details: if (!string.IsNullOrEmpty(details.CodeInterpreterInput)) { - CodeInterpreterToolCallContent hcitcc = new() + CodeInterpreterToolCallContent hcitcc = new(details.ToolCallId) { - CallId = details.ToolCallId, Inputs = [new DataContent(Encoding.UTF8.GetBytes(details.CodeInterpreterInput), "text/x-python")], RawRepresentation = details, }; @@ -218,9 +217,8 @@ public async IAsyncEnumerable GetStreamingResponseAsync( if (details.CodeInterpreterOutputs is { Count: > 0 }) { - CodeInterpreterToolResultContent hcitrc = new() + CodeInterpreterToolResultContent hcitrc = new(details.ToolCallId) { - CallId = details.ToolCallId, RawRepresentation = details, }; diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index eb39754d5fd..afcaf441060 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -1099,14 +1099,14 @@ static FunctionCallOutputResponseItem SerializeAIContent(string callId, IEnumera break; case McpServerToolCallContent mstcc: - (idToContentMapping ??= [])[mstcc.CallId] = mstcc; + (idToContentMapping ??= [])[mstcc.Id] = mstcc; break; case McpServerToolResultContent mstrc: - if (idToContentMapping?.TryGetValue(mstrc.CallId, out AIContent? callContentFromMapping) is true && + if (idToContentMapping?.TryGetValue(mstrc.Id, out AIContent? callContentFromMapping) is true && callContentFromMapping is McpServerToolCallContent associatedCall) { - _ = idToContentMapping.Remove(mstrc.CallId); + _ = idToContentMapping.Remove(mstrc.Id); McpToolCallItem mtci = ResponseItem.CreateMcpToolCallItem( associatedCall.ServerName, associatedCall.ToolName, @@ -1287,9 +1287,8 @@ private static void AddAllMcpFilters(IList toolNames, McpToolFilter filt /// Adds new for the specified into . private static void AddCodeInterpreterContents(CodeInterpreterCallResponseItem cicri, IList contents) { - contents.Add(new CodeInterpreterToolCallContent + contents.Add(new CodeInterpreterToolCallContent(cicri.Id) { - CallId = cicri.Id, Inputs = !string.IsNullOrWhiteSpace(cicri.Code) ? [new DataContent(Encoding.UTF8.GetBytes(cicri.Code), "text/x-python")] : null, // We purposefully do not set the RawRepresentation on the HostedCodeInterpreterToolCallContent, only on the HostedCodeInterpreterToolResultContent, to avoid @@ -1297,9 +1296,8 @@ private static void AddCodeInterpreterContents(CodeInterpreterCallResponseItem c // CodeInterpreterCallResponseItem sent back for the pair. }); - contents.Add(new CodeInterpreterToolResultContent + contents.Add(new CodeInterpreterToolResultContent(cicri.Id) { - CallId = cicri.Id, Outputs = cicri.Outputs is { Count: > 0 } outputs ? outputs.Select(o => o switch { diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs index e5734ccd7cf..b9543515c01 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/AIContentTests.cs @@ -75,7 +75,8 @@ public void Serialization_DerivedTypes_Roundtrips() new McpServerToolCallContent("call123", "myTool", "myServer"), new McpServerToolResultContent("call123"), new McpServerToolApprovalRequestContent("request123", new McpServerToolCallContent("call123", "myTool", "myServer")), - new McpServerToolApprovalResponseContent("request123", approved: true) + new McpServerToolApprovalResponseContent("request123", approved: true), + new ServiceActionContent("action123") ]); var serialized = JsonSerializer.Serialize(message, AIJsonUtilities.DefaultOptions); diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs index 1807f4a169a..16ac72c8416 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolCallContentTests.cs @@ -1,6 +1,7 @@ // 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.Text.Json; using Xunit; @@ -12,21 +13,19 @@ public class CodeInterpreterToolCallContentTests [Fact] public void Constructor_PropsDefault() { - CodeInterpreterToolCallContent c = new(); + CodeInterpreterToolCallContent c = new("call123"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.CallId); + Assert.Equal("call123", c.Id); Assert.Null(c.Inputs); } [Fact] public void Properties_Roundtrip() { - CodeInterpreterToolCallContent c = new(); + CodeInterpreterToolCallContent c = new("call123"); - Assert.Null(c.CallId); - c.CallId = "call123"; - Assert.Equal("call123", c.CallId); + Assert.Equal("call123", c.Id); Assert.Null(c.Inputs); IList inputs = [new TextContent("print('hello')")]; @@ -47,9 +46,8 @@ public void Properties_Roundtrip() [Fact] public void Inputs_SupportsMultipleContentTypes() { - CodeInterpreterToolCallContent c = new() + CodeInterpreterToolCallContent c = new("call456") { - CallId = "call456", Inputs = [ new TextContent("import numpy as np"), @@ -68,9 +66,8 @@ public void Inputs_SupportsMultipleContentTypes() [Fact] public void Serialization_Roundtrips() { - CodeInterpreterToolCallContent content = new() + CodeInterpreterToolCallContent content = new("call123") { - CallId = "call123", Inputs = [ new TextContent("print('hello')"), @@ -82,7 +79,7 @@ public void Serialization_Roundtrips() var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedSut); - Assert.Equal("call123", deserializedSut.CallId); + Assert.Equal("call123", deserializedSut.Id); Assert.NotNull(deserializedSut.Inputs); Assert.Equal(2, deserializedSut.Inputs.Count); Assert.IsType(deserializedSut.Inputs[0]); @@ -90,4 +87,11 @@ public void Serialization_Roundtrips() Assert.IsType(deserializedSut.Inputs[1]); Assert.Equal("file456", ((HostedFileContent)deserializedSut.Inputs[1]).FileId); } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws("id", () => new CodeInterpreterToolCallContent(null!)); + Assert.Throws("id", () => new CodeInterpreterToolCallContent(string.Empty)); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs index 6fb1303be53..de4ba73851c 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/CodeInterpreterToolResultContentTests.cs @@ -1,6 +1,7 @@ // 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.Text.Json; using Xunit; @@ -12,21 +13,19 @@ public class CodeInterpreterToolResultContentTests [Fact] public void Constructor_PropsDefault() { - CodeInterpreterToolResultContent c = new(); + CodeInterpreterToolResultContent c = new("call123"); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Null(c.CallId); + Assert.Equal("call123", c.Id); Assert.Null(c.Outputs); } [Fact] public void Properties_Roundtrip() { - CodeInterpreterToolResultContent c = new(); + CodeInterpreterToolResultContent c = new("call123"); - Assert.Null(c.CallId); - c.CallId = "call123"; - Assert.Equal("call123", c.CallId); + Assert.Equal("call123", c.Id); Assert.Null(c.Outputs); IList output = [new TextContent("Hello, World!")]; @@ -47,9 +46,8 @@ public void Properties_Roundtrip() [Fact] public void Output_SupportsMultipleContentTypes() { - CodeInterpreterToolResultContent c = new() + CodeInterpreterToolResultContent c = new("call789") { - CallId = "call789", Outputs = [ new TextContent("Execution completed"), @@ -70,9 +68,8 @@ public void Output_SupportsMultipleContentTypes() [Fact] public void Serialization_Roundtrips() { - CodeInterpreterToolResultContent content = new() + CodeInterpreterToolResultContent content = new("call123") { - CallId = "call123", Outputs = [ new TextContent("Hello, World!"), @@ -84,7 +81,7 @@ public void Serialization_Roundtrips() var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedSut); - Assert.Equal("call123", deserializedSut.CallId); + Assert.Equal("call123", deserializedSut.Id); Assert.NotNull(deserializedSut.Outputs); Assert.Equal(2, deserializedSut.Outputs.Count); Assert.IsType(deserializedSut.Outputs[0]); @@ -92,4 +89,11 @@ public void Serialization_Roundtrips() Assert.IsType(deserializedSut.Outputs[1]); Assert.Equal("result.txt", ((HostedFileContent)deserializedSut.Outputs[1]).FileId); } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws("id", () => new CodeInterpreterToolResultContent(null!)); + Assert.Throws("id", () => new CodeInterpreterToolResultContent(string.Empty)); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs index d5c5b43ed0a..e6831017221 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Text.Json; using Xunit; namespace Microsoft.Extensions.AI; @@ -12,12 +13,12 @@ public class McpServerToolCallContentTests [Fact] public void Constructor_PropsDefault() { - McpServerToolCallContent c = new("callId1", "toolName", null); + McpServerToolCallContent c = new("call123", "toolName", null); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); - Assert.Equal("callId1", c.CallId); + Assert.Equal("call123", c.Id); Assert.Equal("toolName", c.ToolName); Assert.Null(c.ServerName); Assert.Null(c.Arguments); @@ -26,7 +27,7 @@ public void Constructor_PropsDefault() [Fact] public void Constructor_PropsRoundtrip() { - McpServerToolCallContent c = new("callId1", "toolName", "serverName"); + McpServerToolCallContent c = new("call123", "toolName", "serverName"); Assert.Null(c.RawRepresentation); object raw = new(); @@ -43,7 +44,7 @@ public void Constructor_PropsRoundtrip() c.Arguments = args; Assert.Same(args, c.Arguments); - Assert.Equal("callId1", c.CallId); + Assert.Equal("call123", c.Id); Assert.Equal("toolName", c.ToolName); Assert.Equal("serverName", c.ServerName); } @@ -51,10 +52,44 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { - Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", null)); + Assert.Throws("id", () => new McpServerToolCallContent(string.Empty, "name", null)); Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, null)); - Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", null)); + Assert.Throws("id", () => new McpServerToolCallContent(null!, "name", null)); Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, null)); } + + [Fact] + public void Serialization_Roundtrips() + { + var content = new McpServerToolCallContent("call123", "toolName", "serverName") + { + Arguments = new Dictionary + { + { "arg1", 123 }, + { "arg2", "456" } + } + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedContent); + Assert.Equal(content.Id, deserializedContent.Id); + Assert.Equal(content.ToolName, deserializedContent.ToolName); + Assert.Equal(content.ServerName, deserializedContent.ServerName); + Assert.NotNull(deserializedContent.Arguments); + Assert.Equal(2, deserializedContent.Arguments.Count); + Assert.Collection(deserializedContent.Arguments, + kvp => + { + Assert.Equal("arg1", kvp.Key); + Assert.True(kvp.Value is JsonElement { ValueKind: JsonValueKind.Number }); + }, + kvp => + { + Assert.Equal("arg2", kvp.Key); + Assert.True(kvp.Value is JsonElement { ValueKind: JsonValueKind.String }); + }); + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs index 8fa6cc8a381..a2bc4f7ddc1 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -13,8 +13,8 @@ public class McpServerToolResultContentTests [Fact] public void Constructor_PropsDefault() { - McpServerToolResultContent c = new("callId"); - Assert.Equal("callId", c.CallId); + McpServerToolResultContent c = new("call123"); + Assert.Equal("call123", c.Id); Assert.Null(c.RawRepresentation); Assert.Null(c.AdditionalProperties); Assert.Null(c.Output); @@ -23,7 +23,7 @@ public void Constructor_PropsDefault() [Fact] public void Constructor_PropsRoundtrip() { - McpServerToolResultContent c = new("callId"); + McpServerToolResultContent c = new("call123"); Assert.Null(c.RawRepresentation); object raw = new(); @@ -35,7 +35,7 @@ public void Constructor_PropsRoundtrip() c.AdditionalProperties = props; Assert.Same(props, c.AdditionalProperties); - Assert.Equal("callId", c.CallId); + Assert.Equal("call123", c.Id); Assert.Null(c.Output); IList output = []; @@ -46,8 +46,8 @@ public void Constructor_PropsRoundtrip() [Fact] public void Constructor_Throws() { - Assert.Throws("callId", () => new McpServerToolResultContent(string.Empty)); - Assert.Throws("callId", () => new McpServerToolResultContent(null!)); + Assert.Throws("id", () => new McpServerToolResultContent(string.Empty)); + Assert.Throws("id", () => new McpServerToolResultContent(null!)); } [Fact] @@ -62,7 +62,9 @@ public void Serialization_Roundtrips() var deserializedContent = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); Assert.NotNull(deserializedContent); - Assert.Equal(content.CallId, deserializedContent.CallId); + Assert.Equal(content.Id, deserializedContent.Id); Assert.NotNull(deserializedContent.Output); + Assert.IsType(deserializedContent.Output[0]); + Assert.Equal("result", ((TextContent)deserializedContent.Output[0]).Text); } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ServiceActionContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ServiceActionContentTests.cs new file mode 100644 index 00000000000..48e3660b434 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/ServiceActionContentTests.cs @@ -0,0 +1,63 @@ +// 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.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class ServiceActionContentTests +{ + [Fact] + public void Constructor_PropsDefault() + { + ServiceActionContent c = new("action123"); + Assert.Null(c.RawRepresentation); + Assert.Null(c.AdditionalProperties); + Assert.Equal("action123", c.Id); + } + + [Fact] + public void Properties_Roundtrip() + { + ServiceActionContent c = new("action123"); + + Assert.Equal("action123", c.Id); + + Assert.Null(c.RawRepresentation); + object raw = new(); + c.RawRepresentation = raw; + Assert.Same(raw, c.RawRepresentation); + + Assert.Null(c.AdditionalProperties); + AdditionalPropertiesDictionary props = new() { { "key", "value" } }; + c.AdditionalProperties = props; + Assert.Same(props, c.AdditionalProperties); + } + + [Fact] + public void Serialization_Roundtrips() + { + ServiceActionContent content = new("action123") + { + AdditionalProperties = new AdditionalPropertiesDictionary { { "key", "value" } } + }; + + var json = JsonSerializer.Serialize(content, AIJsonUtilities.DefaultOptions); + var deserializedSut = JsonSerializer.Deserialize(json, AIJsonUtilities.DefaultOptions); + + Assert.NotNull(deserializedSut); + Assert.Equal("action123", deserializedSut.Id); + Assert.NotNull(deserializedSut.AdditionalProperties); + Assert.Single(deserializedSut.AdditionalProperties); + Assert.Equal("value", deserializedSut.AdditionalProperties["key"]?.ToString()); + } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws("id", () => new ServiceActionContent(null!)); + Assert.Throws("id", () => new ServiceActionContent(string.Empty)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs index ef9d6063ddd..920b2722065 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIAssistantChatClientIntegrationTests.cs @@ -64,10 +64,7 @@ public async Task UseCodeInterpreter_ProducesCodeExecutionResults() // Validate CodeInterpreterToolCallContent var toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType().SingleOrDefault(); Assert.NotNull(toolCallContent); - if (toolCallContent.CallId is not null) - { - Assert.NotEmpty(toolCallContent.CallId); - } + Assert.NotEmpty(toolCallContent.Id); if (toolCallContent.Inputs is not null) { @@ -83,10 +80,7 @@ public async Task UseCodeInterpreter_ProducesCodeExecutionResults() var toolResultContents = response.Messages.SelectMany(m => m.Contents).OfType().ToList(); foreach (var toolResultContent in toolResultContents) { - if (toolResultContent.CallId is not null) - { - Assert.NotEmpty(toolResultContent.CallId); - } + Assert.NotEmpty(toolResultContent.Id); if (toolResultContent.Outputs is not null) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 830563a60e1..d9ed15e2879 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -40,8 +40,8 @@ public async Task UseCodeInterpreter_ProducesCodeExecutionResults() // Validate CodeInterpreterToolCallContent var toolCallContent = response.Messages.SelectMany(m => m.Contents).OfType().SingleOrDefault(); Assert.NotNull(toolCallContent); - Assert.NotNull(toolCallContent.CallId); - Assert.NotEmpty(toolCallContent.CallId); + Assert.NotNull(toolCallContent.Id); + Assert.NotEmpty(toolCallContent.Id); Assert.NotNull(toolCallContent.Inputs); Assert.NotEmpty(toolCallContent.Inputs); @@ -53,8 +53,8 @@ public async Task UseCodeInterpreter_ProducesCodeExecutionResults() // Validate CodeInterpreterToolResultContent var toolResultContent = response.Messages.SelectMany(m => m.Contents).OfType().FirstOrDefault(); Assert.NotNull(toolResultContent); - Assert.NotNull(toolResultContent.CallId); - Assert.NotEmpty(toolResultContent.CallId); + Assert.NotNull(toolResultContent.Id); + Assert.NotEmpty(toolResultContent.Id); if (toolResultContent.Outputs is not null) { @@ -199,7 +199,7 @@ await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() response.Messages .SelectMany(m => m.Contents) .OfType() - .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true)) + .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.Id, true)) .ToArray()); if (approvalResponse.Contents.Count == 0) { diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 94d767f67d4..fda6a6bb11d 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -1438,7 +1438,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) Assert.Equal(3, message.Contents.Count); var call = Assert.IsType(message.Contents[0]); - Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", call.CallId); + Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", call.Id); Assert.Equal("deepwiki", call.ServerName); Assert.Equal("ask_question", call.ToolName); Assert.NotNull(call.Arguments); @@ -1447,7 +1447,7 @@ public async Task McpToolCall_ApprovalRequired_NonStreaming(string role) Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)call.Arguments["question"]!).GetString()); var result = Assert.IsType(message.Contents[1]); - Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.CallId); + Assert.Equal("mcp_06ee3b1962eeb8470068e6b21cbaa081a3b5aa2a6c989f4c6f", result.Id); Assert.NotNull(result.Output); Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(result.Output)).Text); @@ -1694,7 +1694,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) Assert.Equal(6, message.Contents.Count); var firstCall = Assert.IsType(message.Contents[1]); - Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstCall.CallId); + Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstCall.Id); Assert.Equal("deepwiki", firstCall.ServerName); Assert.Equal("read_wiki_structure", firstCall.ToolName); Assert.NotNull(firstCall.Arguments); @@ -1702,12 +1702,12 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); var firstResult = Assert.IsType(message.Contents[2]); - Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); + Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.Id); Assert.NotNull(firstResult.Output); Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); var secondCall = Assert.IsType(message.Contents[3]); - Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.CallId); + Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondCall.Id); Assert.Equal("deepwiki", secondCall.ServerName); Assert.Equal("ask_question", secondCall.ToolName); Assert.NotNull(secondCall.Arguments); @@ -1715,7 +1715,7 @@ public async Task McpToolCall_ApprovalNotRequired_NonStreaming(bool rawTool) Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString()); var secondResult = Assert.IsType(message.Contents[4]); - Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.CallId); + Assert.Equal("mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", secondResult.Id); Assert.NotNull(secondResult.Output); Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text); @@ -2108,7 +2108,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal(6, message.Contents.Count); var firstCall = Assert.IsType(message.Contents[1]); - Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstCall.CallId); + Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstCall.Id); Assert.Equal("deepwiki", firstCall.ServerName); Assert.Equal("read_wiki_structure", firstCall.ToolName); Assert.NotNull(firstCall.Arguments); @@ -2116,12 +2116,12 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); var firstResult = Assert.IsType(message.Contents[2]); - Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); + Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.Id); Assert.NotNull(firstResult.Output); Assert.StartsWith("Available pages for dotnet/extensions", Assert.IsType(Assert.Single(firstResult.Output)).Text); var secondCall = Assert.IsType(message.Contents[3]); - Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.CallId); + Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondCall.Id); Assert.Equal("deepwiki", secondCall.ServerName); Assert.Equal("ask_question", secondCall.ToolName); Assert.NotNull(secondCall.Arguments); @@ -2129,7 +2129,7 @@ public async Task McpToolCall_ApprovalNotRequired_Streaming() Assert.Equal("What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?", ((JsonElement)secondCall.Arguments["question"]!).GetString()); var secondResult = Assert.IsType(message.Contents[4]); - Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.CallId); + Assert.Equal("mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54", secondResult.Id); Assert.NotNull(secondResult.Output); Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text); @@ -2722,7 +2722,7 @@ public async Task CodeInterpreterTool_NonStreaming() Assert.Equal("text/x-python", codeInput.MediaType); var codeResult = Assert.IsType(message.Contents[1]); - Assert.Equal(codeCall.CallId, codeResult.CallId); + Assert.Equal(codeCall.Id, codeResult.Id); var textContent = Assert.IsType(message.Contents[2]); Assert.Equal("15", textContent.Text); @@ -2943,7 +2943,7 @@ public async Task CodeInterpreterTool_Streaming() Assert.Contains("sum_of_numbers", Encoding.UTF8.GetString(codeInput.Data.ToArray())); var codeResult = Assert.IsType(message.Contents[1]); - Assert.Equal(codeCall.CallId, codeResult.CallId); + Assert.Equal(codeCall.Id, codeResult.Id); var textContent = Assert.IsType(message.Contents[2]); Assert.Equal("The sum of numbers from 1 to 10 is 55.", textContent.Text);