Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ internal static void CoalesceContent(IList<AIContent> contents)
Coalesce<CodeInterpreterToolCallContent>(
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];
Expand All @@ -320,9 +320,8 @@ internal static void CoalesceContent(IList<AIContent> contents)
CoalesceContent(inputs);
}

return new()
return new(firstContent.Id)
{
CallId = firstContent.CallId,
Inputs = inputs,
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
};
Expand All @@ -331,7 +330,7 @@ internal static void CoalesceContent(IList<AIContent> contents)
Coalesce<CodeInterpreterToolResultContent>(
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];
Expand All @@ -358,9 +357,8 @@ internal static void CoalesceContent(IList<AIContent> contents)
CoalesceContent(output);
}

return new()
return new(firstContent.Id)
{
CallId = firstContent.CallId,
Outputs = output,
AdditionalProperties = firstContent.AdditionalProperties?.Clone(),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,14 @@ namespace Microsoft.Extensions.AI;
/// It is informational only and represents the call itself, not the result.
/// </remarks>
[Experimental("MEAI001")]
public sealed class CodeInterpreterToolCallContent : AIContent
public sealed class CodeInterpreterToolCallContent : ServiceActionContent
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeInterpreterToolCallContent"/> class.
/// </summary>
public CodeInterpreterToolCallContent()
/// <inheritdoc/>
public CodeInterpreterToolCallContent(string id)
: base(id)
{
}

/// <summary>
/// Gets or sets the tool call ID.
/// </summary>
public string? CallId { get; set; }

/// <summary>
/// Gets or sets the inputs to the code interpreter tool.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,14 @@ namespace Microsoft.Extensions.AI;
/// Represents the result of a code interpreter tool invocation by a hosted service.
/// </summary>
[Experimental("MEAI001")]
public sealed class CodeInterpreterToolResultContent : AIContent
public sealed class CodeInterpreterToolResultContent : ServiceActionContent
{
/// <summary>
/// Initializes a new instance of the <see cref="CodeInterpreterToolResultContent"/> class.
/// </summary>
public CodeInterpreterToolResultContent()
/// <inheritdoc/>
public CodeInterpreterToolResultContent(string id)
: base(id)
{
}

/// <summary>
/// Gets or sets the tool call ID that this result corresponds to.
/// </summary>
public string? CallId { get; set; }

/// <summary>
/// Gets or sets the output of code interpreter tool.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,23 @@ namespace Microsoft.Extensions.AI;
/// It is informational only.
/// </remarks>
[Experimental("MEAI001")]
public sealed class McpServerToolCallContent : AIContent
public sealed class McpServerToolCallContent : ServiceActionContent
{
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolCallContent"/> class.
/// </summary>
/// <param name="callId">The tool call ID.</param>
/// <param name="id">The tool call ID.</param>
/// <param name="toolName">The tool name.</param>
/// <param name="serverName">The MCP server name that hosts the tool.</param>
/// <exception cref="ArgumentNullException"><paramref name="callId"/> or <paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="callId"/> or <paramref name="toolName"/> is empty or composed entirely of whitespace.</exception>
public McpServerToolCallContent(string callId, string toolName, string? serverName)
/// <exception cref="ArgumentNullException"><paramref name="id"/> or <paramref name="toolName"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="id"/> or <paramref name="toolName"/> is empty or composed entirely of whitespace.</exception>
public McpServerToolCallContent(string id, string toolName, string? serverName)
: base(id)
{
CallId = Throw.IfNullOrWhitespace(callId);
ToolName = Throw.IfNullOrWhitespace(toolName);
ServerName = serverName;
}

/// <summary>
/// Gets the tool call ID.
/// </summary>
public string CallId { get; }

/// <summary>
/// Gets the name of the tool called.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -16,24 +14,14 @@ namespace Microsoft.Extensions.AI;
/// It is informational only.
/// </remarks>
[Experimental("MEAI001")]
public sealed class McpServerToolResultContent : AIContent
public sealed class McpServerToolResultContent : ServiceActionContent
{
/// <summary>
/// Initializes a new instance of the <see cref="McpServerToolResultContent"/> class.
/// </summary>
/// <param name="callId">The tool call ID.</param>
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="callId"/> is empty or composed entirely of whitespace.</exception>
public McpServerToolResultContent(string callId)
/// <inheritdoc/>
public McpServerToolResultContent(string id)
: base(id)
{
CallId = Throw.IfNullOrWhitespace(callId);
}

/// <summary>
/// Gets the tool call ID.
/// </summary>
public string CallId { get; }

/// <summary>
/// Gets or sets the output of the tool call.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Represents an action performed by a hosted service.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
[Experimental("MEAI001")]
public class ServiceActionContent : AIContent
{
/// <summary>
/// Initializes a new instance of the <see cref="ServiceActionContent"/> class.
/// </summary>
/// <param name="id">The ID for the service-side action.</param>
/// <exception cref="ArgumentNullException"><paramref name="id"/> is <see langword="null"/>.</exception>
/// <exception cref="ArgumentException"><paramref name="id"/> is empty or composed entirely of whitespace.</exception>
public ServiceActionContent(string id)
{
Id = Throw.IfNullOrWhitespace(id);
}

/// <summary>
/// Gets the ID for the service-side action.
/// </summary>
public string Id { get; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down Expand Up @@ -133,6 +134,7 @@ private static JsonSerializerOptions CreateDefaultOptions()
[JsonSerializable(typeof(McpServerToolApprovalResponseContent))]
[JsonSerializable(typeof(CodeInterpreterToolCallContent))]
[JsonSerializable(typeof(CodeInterpreterToolResultContent))]
[JsonSerializable(typeof(ServiceActionContent))]
[JsonSerializable(typeof(ResponseContinuationToken))]

// IEmbeddingGenerator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,8 @@ public async IAsyncEnumerable<ChatResponseUpdate> 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,
};
Expand All @@ -218,9 +217,8 @@ public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(

if (details.CodeInterpreterOutputs is { Count: > 0 })
{
CodeInterpreterToolResultContent hcitrc = new()
CodeInterpreterToolResultContent hcitrc = new(details.ToolCallId)
{
CallId = details.ToolCallId,
RawRepresentation = details,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1287,19 +1287,17 @@ private static void AddAllMcpFilters(IList<string> toolNames, McpToolFilter filt
/// <summary>Adds new <see cref="AIContent"/> for the specified <paramref name="cicri"/> into <paramref name="contents"/>.</summary>
private static void AddCodeInterpreterContents(CodeInterpreterCallResponseItem cicri, IList<AIContent> 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
// the same CodeInterpreterCallResponseItem being included on two different AIContent instances. When these are roundtripped, we want only one
// 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<CodeInterpreterCallOutput, AIContent?>(o =>
o switch
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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<AIContent> inputs = [new TextContent("print('hello')")];
Expand All @@ -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"),
Expand All @@ -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')"),
Expand All @@ -82,12 +79,19 @@ public void Serialization_Roundtrips()
var deserializedSut = JsonSerializer.Deserialize<CodeInterpreterToolCallContent>(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<TextContent>(deserializedSut.Inputs[0]);
Assert.Equal("print('hello')", ((TextContent)deserializedSut.Inputs[0]).Text);
Assert.IsType<HostedFileContent>(deserializedSut.Inputs[1]);
Assert.Equal("file456", ((HostedFileContent)deserializedSut.Inputs[1]).FileId);
}

[Fact]
public void Constructor_Throws()
{
Assert.Throws<ArgumentNullException>("id", () => new CodeInterpreterToolCallContent(null!));
Assert.Throws<ArgumentException>("id", () => new CodeInterpreterToolCallContent(string.Empty));
}
}
Loading
Loading