diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs index 59ce51e7ef3..899ba04251e 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/ChatCompletion/RequiredChatToolMode.cs @@ -41,12 +41,6 @@ public RequiredChatToolMode(string? requiredFunctionName) RequiredFunctionName = requiredFunctionName; } - // The reason for not overriding Equals/GetHashCode (e.g., so two instances are equal if they - // have the same RequiredFunctionName) is to leave open the option to unseal the type in the - // future. If we did define equality based on RequiredFunctionName but a subclass added further - // fields, this would lead to wrong behavior unless the subclass author remembers to re-override - // Equals/GetHashCode as well, which they likely won't. - /// Gets a string representing this instance to display in the debugger. [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"Required: {RequiredFunctionName ?? "Any"}"; diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs index e3ee10ad50a..a0e240be991 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/AIContent.cs @@ -24,6 +24,10 @@ namespace Microsoft.Extensions.AI; // experimental types in its source generated files. // [JsonDerivedType(typeof(FunctionApprovalRequestContent), typeDiscriminator: "functionApprovalRequest")] // [JsonDerivedType(typeof(FunctionApprovalResponseContent), typeDiscriminator: "functionApprovalResponse")] +// [JsonDerivedType(typeof(McpServerToolCallContent), typeDiscriminator: "mcpServerToolCall")] +// [JsonDerivedType(typeof(McpServerToolResultContent), typeDiscriminator: "mcpServerToolResult")] +// [JsonDerivedType(typeof(McpServerToolApprovalRequestContent), typeDiscriminator: "mcpServerToolApprovalRequest")] +// [JsonDerivedType(typeof(McpServerToolApprovalResponseContent), typeDiscriminator: "mcpServerToolApprovalResponse")] public class AIContent { diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs new file mode 100644 index 00000000000..8f302d901b4 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalRequestContent.cs @@ -0,0 +1,41 @@ +// 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 a request for user approval of an MCP server tool call. +/// +[Experimental("MEAI001")] +public sealed class McpServerToolApprovalRequestContent : UserInputRequestContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID that uniquely identifies the MCP server tool approval request/response pair. + /// The tool call that requires user approval. + /// is . + /// is empty or composed entirely of whitespace. + /// is . + public McpServerToolApprovalRequestContent(string id, McpServerToolCallContent toolCall) + : base(id) + { + ToolCall = Throw.IfNull(toolCall); + } + + /// + /// Gets the tool call that pre-invoke approval is required for. + /// + public McpServerToolCallContent ToolCall { get; } + + /// + /// Creates a to indicate whether the function call is approved or rejected based on the value of . + /// + /// if the function call is approved; otherwise, . + /// The representing the approval response. + public McpServerToolApprovalResponseContent CreateResponse(bool approved) => new(Id, approved); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs new file mode 100644 index 00000000000..0e239a79d7f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolApprovalResponseContent.cs @@ -0,0 +1,32 @@ +// 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; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a response to an MCP server tool approval request. +/// +[Experimental("MEAI001")] +public sealed class McpServerToolApprovalResponseContent : UserInputResponseContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The ID that uniquely identifies the MCP server tool approval request/response pair. + /// if the MCP server tool call is approved; otherwise, . + /// is . + /// is empty or composed entirely of whitespace. + public McpServerToolApprovalResponseContent(string id, bool approved) + : base(id) + { + Approved = approved; + } + + /// + /// Gets a value indicating whether the user approved the request. + /// + public bool Approved { get; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs new file mode 100644 index 00000000000..5ed6385789c --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolCallContent.cs @@ -0,0 +1,55 @@ +// 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; + +/// +/// Represents a tool call request to a MCP server. +/// +/// +/// This content type is used to represent an invocation of an MCP server tool by a hosted service. +/// It is informational only. +/// +[Experimental("MEAI001")] +public sealed class McpServerToolCallContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID. + /// The tool name. + /// The MCP server name. + /// , , or are . + /// , , or are empty or composed entirely of whitespace. + public McpServerToolCallContent(string callId, string toolName, string serverName) + { + CallId = Throw.IfNullOrWhitespace(callId); + ToolName = Throw.IfNullOrWhitespace(toolName); + ServerName = Throw.IfNullOrWhitespace(serverName); + } + + /// + /// Gets the tool call ID. + /// + public string CallId { get; } + + /// + /// Gets the name of the tool called. + /// + public string ToolName { get; } + + /// + /// Gets the name of the MCP server. + /// + public string ServerName { get; } + + /// + /// Gets or sets the arguments used for the tool call. + /// + public IReadOnlyDictionary? Arguments { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs new file mode 100644 index 00000000000..b8329c74d99 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Contents/McpServerToolResultContent.cs @@ -0,0 +1,41 @@ +// 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; + +/// +/// Represents the result of a MCP server tool call. +/// +/// +/// This content type is used to represent the result of an invocation of an MCP server tool by a hosted service. +/// It is informational only. +/// +[Experimental("MEAI001")] +public sealed class McpServerToolResultContent : AIContent +{ + /// + /// Initializes a new instance of the class. + /// + /// The tool call ID. + /// is . + /// is empty or composed entirely of whitespace. + public McpServerToolResultContent(string callId) + { + CallId = Throw.IfNullOrWhitespace(callId); + } + + /// + /// Gets the tool call ID. + /// + public string CallId { get; } + + /// + /// Gets or sets the output of the tool call. + /// + public IList? Output { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs new file mode 100644 index 00000000000..388ffbc2f7f --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolAlwaysRequireApprovalMode.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// +/// Indicates that approval is always required for tool calls to a hosted MCP server. +/// +/// +/// Use to get an instance of . +/// +[Experimental("MEAI001")] +[DebuggerDisplay(nameof(AlwaysRequire))] +public sealed class HostedMcpServerToolAlwaysRequireApprovalMode : HostedMcpServerToolApprovalMode +{ + /// Initializes a new instance of the class. + /// Use to get an instance of . + public HostedMcpServerToolAlwaysRequireApprovalMode() + { + } + + /// + public override bool Equals(object? obj) => obj is HostedMcpServerToolAlwaysRequireApprovalMode; + + /// + public override int GetHashCode() => typeof(HostedMcpServerToolAlwaysRequireApprovalMode).GetHashCode(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs new file mode 100644 index 00000000000..47c3e3e48db --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolApprovalMode.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.Extensions.AI; + +/// +/// Describes how approval is required for tool calls to a hosted MCP server. +/// +/// +/// The predefined values , and are provided to specify handling for all tools. +/// To specify approval behavior for individual tool names, use . +/// +[Experimental("MEAI001")] +[JsonPolymorphic(TypeDiscriminatorPropertyName = "$type")] +[JsonDerivedType(typeof(HostedMcpServerToolNeverRequireApprovalMode), typeDiscriminator: "never")] +[JsonDerivedType(typeof(HostedMcpServerToolAlwaysRequireApprovalMode), typeDiscriminator: "always")] +[JsonDerivedType(typeof(HostedMcpServerToolRequireSpecificApprovalMode), typeDiscriminator: "requireSpecific")] +#pragma warning disable CA1052 // Static holder types should be Static or NotInheritable +public class HostedMcpServerToolApprovalMode +#pragma warning restore CA1052 +{ + /// + /// Gets a predefined indicating that all tool calls to a hosted MCP server always require approval. + /// + public static HostedMcpServerToolAlwaysRequireApprovalMode AlwaysRequire { get; } = new(); + + /// + /// Gets a predefined indicating that all tool calls to a hosted MCP server never require approval. + /// + public static HostedMcpServerToolNeverRequireApprovalMode NeverRequire { get; } = new(); + + private protected HostedMcpServerToolApprovalMode() + { + } + + /// + /// Instantiates a that specifies approval behavior for individual tool names. + /// + /// The list of tools names that always require approval. + /// The list of tools names that never require approval. + /// An instance of for the specified tool names. + public static HostedMcpServerToolRequireSpecificApprovalMode RequireSpecific(IList? alwaysRequireApprovalToolNames, IList? neverRequireApprovalToolNames) + => new(alwaysRequireApprovalToolNames, neverRequireApprovalToolNames); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs new file mode 100644 index 00000000000..bca80649f0d --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolNeverRequireApprovalMode.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.AI; + +/// +/// Indicates that approval is never required for tool calls to a hosted MCP server. +/// +/// +/// Use to get an instance of . +/// +[Experimental("MEAI001")] +[DebuggerDisplay(nameof(NeverRequire))] +public sealed class HostedMcpServerToolNeverRequireApprovalMode : HostedMcpServerToolApprovalMode +{ + /// Initializes a new instance of the class. + /// Use to get an instance of . + public HostedMcpServerToolNeverRequireApprovalMode() + { + } + + /// + public override bool Equals(object? obj) => obj is HostedMcpServerToolNeverRequireApprovalMode; + + /// + public override int GetHashCode() => typeof(HostedMcpServerToolNeverRequireApprovalMode).GetHashCode(); +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs new file mode 100644 index 00000000000..a5e870af7e7 --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedMcpServerToolRequireSpecificApprovalMode.cs @@ -0,0 +1,66 @@ +// 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 System.Linq; + +namespace Microsoft.Extensions.AI; + +/// +/// Represents a mode where approval behavior is specified for individual tool names. +/// +[Experimental("MEAI001")] +public sealed class HostedMcpServerToolRequireSpecificApprovalMode : HostedMcpServerToolApprovalMode +{ + /// + /// Initializes a new instance of the class that specifies approval behavior for individual tool names. + /// + /// The list of tools names that always require approval. + /// The list of tools names that never require approval. + public HostedMcpServerToolRequireSpecificApprovalMode(IList? alwaysRequireApprovalToolNames, IList? neverRequireApprovalToolNames) + { + AlwaysRequireApprovalToolNames = alwaysRequireApprovalToolNames; + NeverRequireApprovalToolNames = neverRequireApprovalToolNames; + } + + /// + /// Gets or sets the list of tool names that always require approval. + /// + public IList? AlwaysRequireApprovalToolNames { get; set; } + + /// + /// Gets or sets the list of tool names that never require approval. + /// + public IList? NeverRequireApprovalToolNames { get; set; } + + /// + public override bool Equals(object? obj) => obj is HostedMcpServerToolRequireSpecificApprovalMode other && + ListEquals(AlwaysRequireApprovalToolNames, other.AlwaysRequireApprovalToolNames) && + ListEquals(NeverRequireApprovalToolNames, other.NeverRequireApprovalToolNames); + + /// + public override int GetHashCode() => + HashCode.Combine(GetListHashCode(AlwaysRequireApprovalToolNames), GetListHashCode(NeverRequireApprovalToolNames)); + + private static bool ListEquals(IList? list1, IList? list2) => + ReferenceEquals(list1, list2) || + (list1 is not null && list2 is not null && list1.SequenceEqual(list2)); + + private static int GetListHashCode(IList? list) + { + if (list is null) + { + return 0; + } + + HashCode hc = default; + foreach (string item in list) + { + hc.Add(item); + } + + return hc.ToHashCode(); + } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj index f5472854def..25aa2f4c49d 100644 --- a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Microsoft.Extensions.AI.Abstractions.csproj @@ -32,6 +32,7 @@ + diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/AITool.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/AITool.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedCodeInterpreterTool.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedCodeInterpreterTool.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedFileSearchTool.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedFileSearchTool.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs new file mode 100644 index 00000000000..54e760c78dd --- /dev/null +++ b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedMcpServerTool.cs @@ -0,0 +1,89 @@ +// 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; + +/// +/// Represents a hosted MCP server tool that can be specified to an AI service. +/// +[Experimental("MEAI001")] +public class HostedMcpServerTool : AITool +{ + /// + /// Initializes a new instance of the class. + /// + /// The name of the remote MCP server. + /// The URL of the remote MCP server. + /// or are . + /// is empty or composed entirely of whitespace. + public HostedMcpServerTool(string serverName, [StringSyntax(StringSyntaxAttribute.Uri)] string url) + : this(serverName, new Uri(Throw.IfNull(url))) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the remote MCP server. + /// The URL of the remote MCP server. + /// or are . + /// is empty or composed entirely of whitespace. + public HostedMcpServerTool(string serverName, Uri url) + { + ServerName = Throw.IfNullOrWhitespace(serverName); + Url = Throw.IfNull(url); + } + + /// + /// Gets the name of the remote MCP server that is used to identify it. + /// + public string ServerName { get; } + + /// + /// Gets the URL of the remote MCP server. + /// + public Uri Url { get; } + + /// + /// Gets or sets the description of the remote MCP server, used to provide more context to the AI service. + /// + public string? ServerDescription { get; set; } + + /// + /// Gets or sets the list of tools allowed to be used by the AI service. + /// + /// + /// The default value is , which allows any tool to be used. + /// + public IList? AllowedTools { get; set; } + + /// + /// Gets or sets the approval mode that indicates when the AI service should require user approval for tool calls to the remote MCP server. + /// + /// + /// + /// You can set this property to to require approval for all tool calls, + /// or to to never require approval. + /// + /// + /// The default value is , which some providers may treat the same as . + /// + /// + /// The underlying provider is not guaranteed to support or honor the approval mode. + /// + /// + public HostedMcpServerToolApprovalMode? ApprovalMode { get; set; } + + /// + /// Gets or sets the HTTP headers that the AI service should use when calling the remote MCP server. + /// + /// + /// This property is useful for specifying the authentication header or other headers required by the MCP server. + /// + public IDictionary? Headers { get; set; } +} diff --git a/src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedWebSearchTool.cs b/src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs similarity index 100% rename from src/Libraries/Microsoft.Extensions.AI.Abstractions/HostedWebSearchTool.cs rename to src/Libraries/Microsoft.Extensions.AI.Abstractions/Tools/HostedWebSearchTool.cs diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs index b4f75fe2c94..ab770568f69 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIJsonContext.cs @@ -14,6 +14,7 @@ namespace Microsoft.Extensions.AI; WriteIndented = true)] [JsonSerializable(typeof(OpenAIClientExtensions.ToolJson))] [JsonSerializable(typeof(IDictionary))] +[JsonSerializable(typeof(IReadOnlyDictionary))] [JsonSerializable(typeof(string[]))] [JsonSerializable(typeof(IEnumerable))] [JsonSerializable(typeof(JsonElement))] diff --git a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs index 0acfef52eb0..c025ae14d8a 100644 --- a/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs +++ b/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIResponsesChatClient.cs @@ -159,6 +159,25 @@ internal static IEnumerable ToChatMessages(IEnumerable FromOpenAIStreamingRe string? modelId = null; string? lastMessageId = null; ChatRole? lastRole = null; - Dictionary outputIndexToMessages = []; - Dictionary? functionCallItems = null; + bool anyFunctions = false; await foreach (var streamingUpdate in streamingResponseUpdates.WithCancellation(cancellationToken).ConfigureAwait(false)) { @@ -229,7 +247,7 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => var update = CreateUpdate(ToUsageDetails(completedUpdate.Response) is { } usage ? new UsageContent(usage) : null); update.FinishReason = ToFinishReason(completedUpdate.Response?.IncompleteStatusDetails?.Reason) ?? - (functionCallItems is not null ? ChatFinishReason.ToolCalls : + (anyFunctions ? ChatFinishReason.ToolCalls : ChatFinishReason.Stop); yield return update; break; @@ -239,65 +257,59 @@ ChatResponseUpdate CreateUpdate(AIContent? content = null) => switch (outputItemAddedUpdate.Item) { case MessageResponseItem mri: - outputIndexToMessages[outputItemAddedUpdate.OutputIndex] = mri; + lastMessageId = outputItemAddedUpdate.Item.Id; + lastRole = ToChatRole(mri.Role); break; case FunctionCallResponseItem fcri: - (functionCallItems ??= [])[outputItemAddedUpdate.OutputIndex] = fcri; + anyFunctions = true; + lastRole = ChatRole.Assistant; break; } goto default; - case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate: - _ = outputIndexToMessages.Remove(outputItemDoneUpdate.OutputIndex); - - if (outputItemDoneUpdate.Item is MessageResponseItem item && - item.Content is { Count: > 0 } content && - content.Any(c => c.OutputTextAnnotations is { Count: > 0 })) - { - AIContent annotatedContent = new(); - foreach (var c in content) - { - PopulateAnnotations(c, annotatedContent); - } - - yield return CreateUpdate(annotatedContent); - break; - } - - goto default; - case StreamingResponseOutputTextDeltaUpdate outputTextDeltaUpdate: - { - _ = outputIndexToMessages.TryGetValue(outputTextDeltaUpdate.OutputIndex, out MessageResponseItem? messageItem); - lastMessageId = messageItem?.Id; - lastRole = ToChatRole(messageItem?.Role); - yield return CreateUpdate(new TextContent(outputTextDeltaUpdate.Delta)); break; - } - case StreamingResponseFunctionCallArgumentsDoneUpdate functionCallOutputDoneUpdate: - { - if (functionCallItems?.TryGetValue(functionCallOutputDoneUpdate.OutputIndex, out FunctionCallResponseItem? callInfo) is true) - { - _ = functionCallItems.Remove(functionCallOutputDoneUpdate.OutputIndex); + case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is FunctionCallResponseItem fcri: + yield return CreateUpdate(OpenAIClientExtensions.ParseCallContent(fcri.FunctionArguments.ToString(), fcri.CallId, fcri.FunctionName)); + break; - var fcc = OpenAIClientExtensions.ParseCallContent( - functionCallOutputDoneUpdate.FunctionArguments.ToString(), - callInfo.CallId, - callInfo.FunctionName); + case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallItem mtci: + var mcpUpdate = CreateUpdate(); + AddMcpToolCallContent(mtci, mcpUpdate.Contents); + yield return mcpUpdate; + break; - lastMessageId = callInfo.Id; - lastRole = ChatRole.Assistant; + case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolDefinitionListItem mtdli: + yield return CreateUpdate(new AIContent { RawRepresentation = mtdli }); + break; - yield return CreateUpdate(fcc); - break; + case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when outputItemDoneUpdate.Item is McpToolCallApprovalRequestItem mtcari: + yield return CreateUpdate(new McpServerToolApprovalRequestContent(mtcari.Id, new(mtcari.Id, mtcari.ToolName, mtcari.ServerLabel) + { + Arguments = JsonSerializer.Deserialize(mtcari.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + RawRepresentation = mtcari, + }) + { + RawRepresentation = mtcari, + }); + break; + + case StreamingResponseOutputItemDoneUpdate outputItemDoneUpdate when + outputItemDoneUpdate.Item is MessageResponseItem mri && + mri.Content is { Count: > 0 } content && + content.Any(c => c.OutputTextAnnotations is { Count: > 0 }): + AIContent annotatedContent = new(); + foreach (var c in content) + { + PopulateAnnotations(c, annotatedContent); } - goto default; - } + yield return CreateUpdate(annotatedContent); + break; case StreamingResponseErrorUpdate errorUpdate: yield return CreateUpdate(new ErrorContent(errorUpdate.Message) @@ -440,6 +452,49 @@ private ResponseCreationOptions ToOpenAIResponseCreationOptions(ChatOptions? opt result.Tools.Add(ModelReaderWriter.Read(BinaryData.FromString(json))); break; + + case HostedMcpServerTool mcpTool: + McpTool responsesMcpTool = ResponseTool.CreateMcpTool( + mcpTool.ServerName, + mcpTool.Url, + mcpTool.Headers); + + if (mcpTool.AllowedTools is not null) + { + responsesMcpTool.AllowedTools = new(); + AddAllMcpFilters(mcpTool.AllowedTools, responsesMcpTool.AllowedTools); + } + + switch (mcpTool.ApprovalMode) + { + case HostedMcpServerToolAlwaysRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.AlwaysRequireApproval); + break; + + case HostedMcpServerToolNeverRequireApprovalMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(GlobalMcpToolCallApprovalPolicy.NeverRequireApproval); + break; + + case HostedMcpServerToolRequireSpecificApprovalMode specificMode: + responsesMcpTool.ToolCallApprovalPolicy = new McpToolCallApprovalPolicy(new CustomMcpToolCallApprovalPolicy()); + + if (specificMode.AlwaysRequireApprovalToolNames is { Count: > 0 } alwaysRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval = new(); + AddAllMcpFilters(alwaysRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsAlwaysRequiringApproval); + } + + if (specificMode.NeverRequireApprovalToolNames is { Count: > 0 } neverRequireToolNames) + { + responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval = new(); + AddAllMcpFilters(neverRequireToolNames, responsesMcpTool.ToolCallApprovalPolicy.CustomPolicy.ToolsNeverRequiringApproval); + } + + break; + } + + result.Tools.Add(responsesMcpTool); + break; } } @@ -497,6 +552,8 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable? idToContentMapping = null; + foreach (ChatMessage input in inputs) { if (input.Role == ChatRole.System || @@ -545,6 +602,10 @@ internal static IEnumerable ToOpenAIResponseItems(IEnumerable ToOpenAIResponseItems(IEnumerable))))); break; + + case McpServerToolApprovalRequestContent mcpApprovalRequestContent: + // BUG https://github.com/openai/openai-dotnet/issues/664: Needs to be able to set an approvalRequestId + yield return ResponseItem.CreateMcpApprovalRequestItem( + mcpApprovalRequestContent.ToolCall.ServerName, + mcpApprovalRequestContent.ToolCall.ToolName, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(mcpApprovalRequestContent.ToolCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject))); + break; + + case McpServerToolCallContent mstcc: + (idToContentMapping ??= [])[mstcc.CallId] = mstcc; + break; + + case McpServerToolResultContent mstrc: + if (idToContentMapping?.TryGetValue(mstrc.CallId, out AIContent? callContentFromMapping) is true && + callContentFromMapping is McpServerToolCallContent associatedCall) + { + _ = idToContentMapping.Remove(mstrc.CallId); + McpToolCallItem mtci = ResponseItem.CreateMcpToolCallItem( + associatedCall.ServerName, + associatedCall.ToolName, + BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(associatedCall.Arguments!, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject))); + if (mstrc.Output?.OfType().FirstOrDefault() is ErrorContent errorContent) + { + mtci.Error = BinaryData.FromString(errorContent.Message); + } + else + { + mtci.ToolOutput = string.Concat(mstrc.Output?.OfType() ?? []); + } + + yield return mtci; + } + + break; } } @@ -745,4 +841,34 @@ private static List ToResponseContentParts(IList return parts; } + + /// Adds new for the specified into . + private static void AddMcpToolCallContent(McpToolCallItem mtci, IList contents) + { + contents.Add(new McpServerToolCallContent(mtci.Id, mtci.ToolName, mtci.ServerLabel) + { + Arguments = JsonSerializer.Deserialize(mtci.ToolArguments.ToMemory().Span, OpenAIJsonContext.Default.IReadOnlyDictionaryStringObject)!, + + // We purposefully do not set the RawRepresentation on the McpServerToolCallContent, only on the McpServerToolResultContent, to avoid + // the same McpToolCallItem being included on two different AIContent instances. When these are roundtripped, we want only one + // McpToolCallItem sent back for the pair. + }); + + contents.Add(new McpServerToolResultContent(mtci.Id) + { + RawRepresentation = mtci, + Output = [mtci.Error is not null ? + new ErrorContent(mtci.Error.ToString()) : + new TextContent(mtci.ToolOutput)], + }); + } + + /// Adds all of the tool names from to . + private static void AddAllMcpFilters(IList toolNames, McpToolFilter filter) + { + foreach (var toolName in toolNames) + { + filter.ToolNames.Add(toolName); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests..cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/FunctionCallContentTests.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.cs new file mode 100644 index 00000000000..ce6516124cd --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolCallContentTests.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.Collections.Generic; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class McpServerToolCallContentTests +{ + [Fact] + public void Constructor_PropsDefault() + { + McpServerToolCallContent c = new("callId1", "toolName", "serverName"); + + Assert.Null(c.RawRepresentation); + Assert.Null(c.AdditionalProperties); + + Assert.Equal("callId1", c.CallId); + Assert.Equal("toolName", c.ToolName); + Assert.Equal("serverName", c.ServerName); + + Assert.Null(c.Arguments); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + McpServerToolCallContent c = new("callId1", "toolName", "serverName"); + + 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); + + Assert.Null(c.Arguments); + IReadOnlyDictionary args = new Dictionary(); + c.Arguments = args; + Assert.Same(args, c.Arguments); + + Assert.Equal("callId1", c.CallId); + Assert.Equal("toolName", c.ToolName); + Assert.Equal("serverName", c.ServerName); + } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws("callId", () => new McpServerToolCallContent(string.Empty, "name", "serverName")); + Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", string.Empty, "serverName")); + Assert.Throws("serverName", () => new McpServerToolCallContent("callId1", "name", string.Empty)); + + Assert.Throws("callId", () => new McpServerToolCallContent(null!, "name", "serverName")); + Assert.Throws("toolName", () => new McpServerToolCallContent("callId1", null!, "serverName")); + Assert.Throws("serverName", () => new McpServerToolCallContent("callId1", "name", null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs new file mode 100644 index 00000000000..ec20b37c9e2 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Contents/McpServerToolResultContentTests.cs @@ -0,0 +1,51 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.AI; + +public class McpServerToolResultContentTests +{ + [Fact] + public void Constructor_PropsDefault() + { + McpServerToolResultContent c = new("callId"); + Assert.Equal("callId", c.CallId); + Assert.Null(c.RawRepresentation); + Assert.Null(c.AdditionalProperties); + Assert.Null(c.Output); + } + + [Fact] + public void Constructor_PropsRoundtrip() + { + McpServerToolResultContent c = new("callId"); + + 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); + + Assert.Equal("callId", c.CallId); + + Assert.Null(c.Output); + IList output = []; + c.Output = output; + Assert.Same(output, c.Output); + } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws("callId", () => new McpServerToolResultContent(string.Empty)); + Assert.Throws("callId", () => new McpServerToolResultContent(null!)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs new file mode 100644 index 00000000000..3ad4690130a --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedMcpServerToolApprovalModeTests.cs @@ -0,0 +1,48 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Xunit; + +namespace Microsoft.Extensions.AI; + +public class HostedMcpServerToolApprovalModeTests +{ + [Fact] + public void Singletons_Idempotent() + { + Assert.Same(HostedMcpServerToolApprovalMode.AlwaysRequire, HostedMcpServerToolApprovalMode.AlwaysRequire); + Assert.Same(HostedMcpServerToolApprovalMode.NeverRequire, HostedMcpServerToolApprovalMode.NeverRequire); + } + + [Fact] + public void Serialization_NeverRequire_Roundtrips() + { + string json = JsonSerializer.Serialize(HostedMcpServerToolApprovalMode.NeverRequire, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal("""{"$type":"never"}""", json); + + HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal(HostedMcpServerToolApprovalMode.NeverRequire, result); + } + + [Fact] + public void Serialization_AlwaysRequire_Roundtrips() + { + string json = JsonSerializer.Serialize(HostedMcpServerToolApprovalMode.AlwaysRequire, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal("""{"$type":"always"}""", json); + + HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal(HostedMcpServerToolApprovalMode.AlwaysRequire, result); + } + + [Fact] + public void Serialization_RequireSpecific_Roundtrips() + { + var requireSpecific = HostedMcpServerToolApprovalMode.RequireSpecific(["ToolA", "ToolB"], ["ToolC"]); + string json = JsonSerializer.Serialize(requireSpecific, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal("""{"$type":"requireSpecific","alwaysRequireApprovalToolNames":["ToolA","ToolB"],"neverRequireApprovalToolNames":["ToolC"]}""", json); + + HostedMcpServerToolApprovalMode? result = JsonSerializer.Deserialize(json, TestJsonSerializerContext.Default.HostedMcpServerToolApprovalMode); + Assert.Equal(requireSpecific, result); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs index 609dac264eb..01de984d949 100644 --- a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/TestJsonSerializerContext.cs @@ -35,4 +35,5 @@ namespace Microsoft.Extensions.AI; [JsonSerializable(typeof(DayOfWeek[]))] // Used in Content tests [JsonSerializable(typeof(Guid))] // Used in Content tests [JsonSerializable(typeof(decimal))] // Used in Content tests +[JsonSerializable(typeof(HostedMcpServerToolApprovalMode))] internal sealed partial class TestJsonSerializerContext : JsonSerializerContext; diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/AIToolTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/AIToolTests.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedCodeInterpreterToolTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedCodeInterpreterToolTests.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedFileSearchToolTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedFileSearchToolTests.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs new file mode 100644 index 00000000000..c77e59e3307 --- /dev/null +++ b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedMcpServerToolTests.cs @@ -0,0 +1,76 @@ +// 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 Xunit; + +namespace Microsoft.Extensions.AI; + +public class HostedMcpServerToolTests +{ + [Fact] + public void Constructor_PropsDefault() + { + HostedMcpServerTool tool = new("serverName", "https://localhost/"); + + Assert.Empty(tool.AdditionalProperties); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("https://localhost/", tool.Url.ToString()); + + Assert.Empty(tool.Description); + Assert.Null(tool.AllowedTools); + Assert.Null(tool.ApprovalMode); + } + + [Fact] + public void Constructor_Roundtrips() + { + HostedMcpServerTool tool = new("serverName", "https://localhost/"); + + Assert.Empty(tool.AdditionalProperties); + Assert.Empty(tool.Description); + Assert.Equal(nameof(HostedMcpServerTool), tool.Name); + + Assert.Equal("serverName", tool.ServerName); + Assert.Equal("https://localhost/", tool.Url.ToString()); + Assert.Empty(tool.Description); + + Assert.Null(tool.ServerDescription); + string serverDescription = "This is a test server"; + tool.ServerDescription = serverDescription; + Assert.Equal(serverDescription, tool.ServerDescription); + + Assert.Null(tool.AllowedTools); + List allowedTools = ["tool1", "tool2"]; + tool.AllowedTools = allowedTools; + Assert.Same(allowedTools, tool.AllowedTools); + + Assert.Null(tool.ApprovalMode); + tool.ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire; + Assert.Same(HostedMcpServerToolApprovalMode.NeverRequire, tool.ApprovalMode); + + tool.ApprovalMode = HostedMcpServerToolApprovalMode.AlwaysRequire; + Assert.Same(HostedMcpServerToolApprovalMode.AlwaysRequire, tool.ApprovalMode); + + var customApprovalMode = new HostedMcpServerToolRequireSpecificApprovalMode(["tool1"], ["tool2"]); + tool.ApprovalMode = customApprovalMode; + Assert.Same(customApprovalMode, tool.ApprovalMode); + + Assert.Null(tool.Headers); + Dictionary headers = []; + tool.Headers = headers; + Assert.Same(headers, tool.Headers); + } + + [Fact] + public void Constructor_Throws() + { + Assert.Throws(() => new HostedMcpServerTool(string.Empty, new Uri("https://localhost/"))); + Assert.Throws(() => new HostedMcpServerTool(null!, new Uri("https://localhost/"))); + Assert.Throws(() => new HostedMcpServerTool("name", (Uri)null!)); + Assert.Throws(() => new HostedMcpServerTool("name", (string)null!)); + Assert.Throws(() => new HostedMcpServerTool("name", string.Empty)); + } +} diff --git a/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedWebSearchToolTests.cs b/test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs similarity index 100% rename from test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/HostedWebSearchToolTests.cs rename to test/Libraries/Microsoft.Extensions.AI.Abstractions.Tests/Tools/HostedWebSearchToolTests.cs diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs index 007e86288f5..a46a06bb770 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientIntegrationTests.cs @@ -18,7 +18,7 @@ public class OpenAIResponseClientIntegrationTests : ChatClientIntegrationTests public override bool FunctionInvokingChatClientSetsConversationId => true; - // Test structure doesn't make sense with Respones. + // Test structure doesn't make sense with Responses. public override Task Caching_AfterFunctionInvocation_FunctionOutputUnchangedAsync() => Task.CompletedTask; [ConditionalFact] @@ -49,4 +49,128 @@ public async Task UseWebSearch_AnnotationsReflectResults() Assert.NotEmpty(ca.Title); }); } + + [ConditionalFact] + public async Task RemoteMCP_ListTools() + { + SkipIfNotEnabled(); + + ChatOptions chatOptions = new() + { + Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") { ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire }], + }; + + ChatResponse response = await CreateChatClient()!.GetResponseAsync("Which tools are available on the wiki_tools MCP server?", chatOptions); + + Assert.Contains("read_wiki_structure", response.Text); + Assert.Contains("read_wiki_contents", response.Text); + Assert.Contains("ask_question", response.Text); + } + + [ConditionalFact] + public async Task RemoteMCP_CallTool_ApprovalNeverRequired() + { + SkipIfNotEnabled(); + + await RunAsync(false, false); + await RunAsync(true, true); + + async Task RunAsync(bool streaming, bool requireSpecific) + { + ChatOptions chatOptions = new() + { + Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + { + ApprovalMode = requireSpecific ? + HostedMcpServerToolApprovalMode.RequireSpecific(null, ["read_wiki_structure", "ask_question"]) : + HostedMcpServerToolApprovalMode.NeverRequire, + } + ], + }; + + using var client = CreateChatClient()!; + + const string Prompt = "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository"; + + ChatResponse response = streaming ? + await client.GetStreamingResponseAsync(Prompt, chatOptions).ToChatResponseAsync() : + await client.GetResponseAsync(Prompt, chatOptions); + + Assert.NotNull(response); + Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); + Assert.NotEmpty(response.Messages.SelectMany(m => m.Contents).OfType()); + Assert.Empty(response.Messages.SelectMany(m => m.Contents).OfType()); + + Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text); + } + } + + [ConditionalFact] + public async Task RemoteMCP_CallTool_ApprovalRequired() + { + SkipIfNotEnabled(); + + await RunAsync(false, false, false); + await RunAsync(true, true, false); + await RunAsync(false, false, true); + await RunAsync(true, true, true); + + async Task RunAsync(bool streaming, bool requireSpecific, bool useConversationId) + { + ChatOptions chatOptions = new() + { + Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + { + ApprovalMode = requireSpecific ? + HostedMcpServerToolApprovalMode.RequireSpecific(["read_wiki_structure", "ask_question"], null) : + HostedMcpServerToolApprovalMode.AlwaysRequire, + } + ], + }; + + using var client = CreateChatClient()!; + + // Initial request + List input = [new ChatMessage(ChatRole.User, "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository")]; + ChatResponse response = streaming ? + await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() : + await client.GetResponseAsync(input, chatOptions); + + // Handle approvals of up to two rounds of tool calls + int approvalsCount = 0; + for (int i = 0; i < 2; i++) + { + if (useConversationId) + { + chatOptions.ConversationId = response.ConversationId; + input.Clear(); + } + else + { + input.AddRange(response.Messages); + } + + var approvalResponse = new ChatMessage(ChatRole.Tool, + response.Messages + .SelectMany(m => m.Contents) + .OfType() + .Select(c => new McpServerToolApprovalResponseContent(c.ToolCall.CallId, true)) + .ToArray()); + if (approvalResponse.Contents.Count == 0) + { + break; + } + + approvalsCount += approvalResponse.Contents.Count; + input.Add(approvalResponse); + response = streaming ? + await client.GetStreamingResponseAsync(input, chatOptions).ToChatResponseAsync() : + await client.GetResponseAsync(input, chatOptions); + } + + // Validate final response + Assert.Equal(2, approvalsCount); + Assert.Contains("src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md", response.Text); + } + } } diff --git a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs index 014fae0d39f..9175b6afd57 100644 --- a/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs +++ b/test/Libraries/Microsoft.Extensions.AI.OpenAI.Tests/OpenAIResponseClientTests.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Linq; using System.Net.Http; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Distributed; using Microsoft.Extensions.Caching.Memory; @@ -824,6 +825,682 @@ public async Task MultipleOutputItems_NonStreaming() Assert.Equal(36, response.Usage.TotalTokenCount); } + [Fact] + public async Task McpToolCall_ApprovalNotRequired_NonStreaming() + { + const string Input = """ + { + "model": "gpt-4o-mini", + "tools": [ + { + "type": "mcp", + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never" + } + ], + "tool_choice": "auto", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository" + } + ] + } + ] + } + """; + + const string Output = """ + { + "id": "resp_68be416397ec81918c48ef286530b8140384f747588fc3f5", + "object": "response", + "created_at": 1757299043, + "status": "completed", + "background": false, + "error": null, + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "model": "gpt-4o-mini-2024-07-18", + "output": [ + { + "id": "mcpl_68be4163aa80819185e792abdcde71670384f747588fc3f5", + "type": "mcp_list_tools", + "server_label": "deepwiki", + "tools": [ + { + "annotations": { + "read_only": false + }, + "description": "Get a list of documentation topics for a GitHub repository", + "input_schema": { + "type": "object", + "properties": { + "repoName": { + "type": "string", + "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")" + } + }, + "required": [ + "repoName" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "name": "read_wiki_structure" + }, + { + "annotations": { + "read_only": false + }, + "description": "View documentation about a GitHub repository", + "input_schema": { + "type": "object", + "properties": { + "repoName": { + "type": "string", + "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")" + } + }, + "required": [ + "repoName" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "name": "read_wiki_contents" + }, + { + "annotations": { + "read_only": false + }, + "description": "Ask any question about a GitHub repository", + "input_schema": { + "type": "object", + "properties": { + "repoName": { + "type": "string", + "description": "GitHub repository: owner/repo (e.g. \"facebook/react\")" + }, + "question": { + "type": "string", + "description": "The question to ask about the repository" + } + }, + "required": [ + "repoName", + "question" + ], + "additionalProperties": false, + "$schema": "http://json-schema.org/draft-07/schema#" + }, + "name": "ask_question" + } + ] + }, + { + "id": "mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", + "type": "mcp_call", + "approval_request_id": null, + "arguments": "{\"repoName\":\"dotnet/extensions\"}", + "error": null, + "name": "read_wiki_structure", + "output": "Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing", + "server_label": "deepwiki" + }, + { + "id": "mcp_68be416900f88191837ae0718339a4ce0384f747588fc3f5", + "type": "mcp_call", + "approval_request_id": null, + "arguments": "{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}", + "error": null, + "name": "ask_question", + "output": "The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` within the `dotnet/extensions` repository. This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` package, including installation instructions and usage examples for its core interfaces like `IChatClient` and `IEmbeddingGenerator`. \n\n## Path to README.md\n\nThe specific path to the `README.md` file for the `Microsoft.Extensions.AI.Abstractions` project is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md`. This path is also referenced in the `AI Extensions Framework` wiki page as a relevant source file. \n\n## Notes\n\nThe `Packaging.targets` file in the `eng/MSBuild` directory indicates that `README.md` files are included in packages when `IsPackable` and `IsShipping` properties are true. This suggests that the `README.md` file located at `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` is intended to be part of the distributed NuGet package for `Microsoft.Extensions.AI.Abstractions`. \n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_315595bd-9b39-4f04-9fa3-42dc778fa9f3\n", + "server_label": "deepwiki" + }, + { + "id": "msg_68be416fb43c819194a1d4ace2643a7e0384f747588fc3f5", + "type": "message", + "status": "completed", + "content": [ + { + "type": "output_text", + "annotations": [], + "logprobs": [], + "text": "The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file includes an overview, installation instructions, and usage examples related to the package." + } + ], + "role": "assistant" + } + ], + "parallel_tool_calls": true, + "previous_response_id": null, + "prompt_cache_key": null, + "reasoning": { + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "store": true, + "temperature": 1, + "text": { + "format": { + "type": "text" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tools": [ + { + "type": "mcp", + "allowed_tools": null, + "headers": null, + "require_approval": "never", + "server_description": null, + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/" + } + ], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 1329, + "input_tokens_details": { + "cached_tokens": 0 + }, + "output_tokens": 123, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 1452 + }, + "user": null, + "metadata": {} + } + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + ChatOptions chatOptions = new() + { + Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + } + ], + }; + + var response = await client.GetResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions); + Assert.NotNull(response); + + Assert.Equal("resp_68be416397ec81918c48ef286530b8140384f747588fc3f5", response.ResponseId); + Assert.Equal("resp_68be416397ec81918c48ef286530b8140384f747588fc3f5", response.ConversationId); + Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_043), response.CreatedAt); + Assert.Null(response.FinishReason); + + var message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.Assistant, response.Messages[0].Role); + Assert.Equal("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file includes an overview, installation instructions, and usage examples related to the package.", response.Messages[0].Text); + + Assert.Equal(6, message.Contents.Count); + + var firstCall = Assert.IsType(message.Contents[1]); + Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstCall.CallId); + Assert.Equal("deepwiki", firstCall.ServerName); + Assert.Equal("read_wiki_structure", firstCall.ToolName); + Assert.NotNull(firstCall.Arguments); + Assert.Single(firstCall.Arguments); + Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); + + var firstResult = Assert.IsType(message.Contents[2]); + Assert.Equal("mcp_68be4166acfc8191bc5e0a751eed358b0384f747588fc3f5", firstResult.CallId); + 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("deepwiki", secondCall.ServerName); + Assert.Equal("ask_question", secondCall.ToolName); + Assert.NotNull(secondCall.Arguments); + Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString()); + 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.NotNull(secondResult.Output); + Assert.StartsWith("The `README.md` file for `Microsoft.Extensions.AI.Abstractions` is located at", Assert.IsType(Assert.Single(secondResult.Output)).Text); + + Assert.NotNull(response.Usage); + Assert.Equal(1329, response.Usage.InputTokenCount); + Assert.Equal(123, response.Usage.OutputTokenCount); + Assert.Equal(1452, response.Usage.TotalTokenCount); + } + + [Fact] + public async Task McpToolCall_ApprovalNotRequired_Streaming() + { + const string Input = """ + { + "model": "gpt-4o-mini", + "tools": [ + { + "type": "mcp", + "server_label": "deepwiki", + "server_url": "https://mcp.deepwiki.com/mcp", + "require_approval": "never" + } + ], + "tool_choice": "auto", + "input": [ + { + "type": "message", + "role": "user", + "content": [ + { + "type": "input_text", + "text": "Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository" + } + ] + } + ], + "stream": true + } + """; + + const string Output = """ + event: response.created + data: {"type":"response.created","sequence_number":0,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"mcp","allowed_tools":null,"headers":null,"require_approval":"never","server_description":null,"server_label":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.in_progress + data: {"type":"response.in_progress","sequence_number":1,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"status":"in_progress","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"auto","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"mcp","allowed_tools":null,"headers":null,"require_approval":"never","server_description":null,"server_label":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":null,"user":null,"metadata":{}}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":2,"output_index":0,"item":{"id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[]}} + + event: response.mcp_list_tools.in_progress + data: {"type":"response.mcp_list_tools.in_progress","sequence_number":3,"output_index":0,"item_id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54"} + + event: response.mcp_list_tools.completed + data: {"type":"response.mcp_list_tools.completed","sequence_number":4,"output_index":0,"item_id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54"} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":5,"output_index":0,"item":{"id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[{"annotations":{"read_only":false},"description":"Get a list of documentation topics for a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_structure"},{"annotations":{"read_only":false},"description":"View documentation about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_contents"},{"annotations":{"read_only":false},"description":"Ask any question about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"},"question":{"type":"string","description":"The question to ask about the repository"}},"required":["repoName","question"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"ask_question"}]}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":6,"output_index":1,"item":{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"","error":null,"name":"read_wiki_structure","output":null,"server_label":"deepwiki"}} + + event: response.mcp_call.in_progress + data: {"type":"response.mcp_call.in_progress","sequence_number":7,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54"} + + event: response.mcp_call_arguments.delta + data: {"type":"response.mcp_call_arguments.delta","sequence_number":8,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","delta":"{\"repoName\":\"dotnet/extensions\"}","obfuscation":""} + + event: response.mcp_call_arguments.done + data: {"type":"response.mcp_call_arguments.done","sequence_number":9,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","arguments":"{\"repoName\":\"dotnet/extensions\"}"} + + event: response.mcp_call.completed + data: {"type":"response.mcp_call.completed","sequence_number":10,"output_index":1,"item_id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54"} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":11,"output_index":1,"item":{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\"}","error":null,"name":"read_wiki_structure","output":"Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing","server_label":"deepwiki"}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":12,"output_index":2,"item":{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"","error":null,"name":"ask_question","output":null,"server_label":"deepwiki"}} + + event: response.mcp_call.in_progress + data: {"type":"response.mcp_call.in_progress","sequence_number":13,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54"} + + event: response.mcp_call_arguments.delta + data: {"type":"response.mcp_call_arguments.delta","sequence_number":14,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","delta":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","obfuscation":"IT"} + + event: response.mcp_call_arguments.done + data: {"type":"response.mcp_call_arguments.done","sequence_number":15,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}"} + + event: response.mcp_call.completed + data: {"type":"response.mcp_call.completed","sequence_number":16,"output_index":2,"item_id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54"} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":17,"output_index":2,"item":{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","error":null,"name":"ask_question","output":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` . This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` library, including installation instructions and usage examples for its core components like `IChatClient` and `IEmbeddingGenerator` .\n\n## README.md Content Overview\nThe `README.md` file for `Microsoft.Extensions.AI.Abstractions` details the purpose of the library, which is to provide abstractions for generative AI components . It includes instructions on how to install the NuGet package `Microsoft.Extensions.AI.Abstractions` .\n\nThe document also provides usage examples for the `IChatClient` interface, which defines methods for interacting with AI services that offer \"chat\" capabilities . This includes examples for requesting both complete and streaming chat responses .\n\nFurthermore, the `README.md` explains the `IEmbeddingGenerator` interface, which is used for generating vector embeddings from input values . It demonstrates how to use `GenerateAsync` to create embeddings . The file also discusses how both `IChatClient` and `IEmbeddingGenerator` implementations can be layered to create pipelines of functionality, incorporating features like caching and telemetry .\n\nNotes:\nThe user's query specifically asked for the path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions`. The provided codebase context, particularly the wiki page for \"AI Extensions Framework\", directly lists this file as a relevant source file . The content of the `README.md` file itself further confirms its relevance to the `Microsoft.Extensions.AI.Abstractions` library.\n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_bb6bee43-3136-4b21-bc5d-02ca1611d857\n","server_label":"deepwiki"}} + + event: response.output_item.added + data: {"type":"response.output_item.added","sequence_number":18,"output_index":3,"item":{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"in_progress","content":[],"role":"assistant"}} + + event: response.content_part.added + data: {"type":"response.content_part.added","sequence_number":19,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":""}} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":20,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"The","logprobs":[],"obfuscation":"a5sNdjeWpJXIK"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":21,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" path","logprobs":[],"obfuscation":"2oWbALsHrtv"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":22,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"K8lRBCaiusvjP"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":23,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"LP7Xp4jDWA5w"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":24,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"2rUNEj0h3wLlee"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":25,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"README","logprobs":[],"obfuscation":"PSbOrCj8y6"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":26,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".md","logprobs":[],"obfuscation":"Do0BCY4kJ6wQW"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":27,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"3fTPkjHu1Oq83DT"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":28,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" file","logprobs":[],"obfuscation":"CI9PXx3sH06"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":29,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"fJuaoSPsMge8"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":30,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"O1h4Q0T72OM4e7"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":31,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"Microsoft","logprobs":[],"obfuscation":"E2YPgfE"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":32,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".Extensions","logprobs":[],"obfuscation":"vfVX8"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":33,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"EwDmSMHqymBRl1"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":34,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"QQfjze1z7QhvcJE"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":35,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"7fLbFXKbxOMkBi"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":36,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"bst","logprobs":[],"obfuscation":"3p1svK7Jd1N7C"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":37,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ractions","logprobs":[],"obfuscation":"Cl2xCwTC"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":38,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"ObDOKE72QOlXSx9"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":39,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" in","logprobs":[],"obfuscation":"FJwPbDYgh4XjL"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":40,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"e8cV5qt7hEsz"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":41,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" `","logprobs":[],"obfuscation":"Hf8ZQDFLfImh3e"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":42,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"dot","logprobs":[],"obfuscation":"0lh2vLiYye2JI"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":43,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"net","logprobs":[],"obfuscation":"g5fzb2qtk4Piz"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":44,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/extensions","logprobs":[],"obfuscation":"egpos"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":45,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`","logprobs":[],"obfuscation":"gXw3bKveEVIKXux"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":46,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" repository","logprobs":[],"obfuscation":"rqhlC"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":47,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" is","logprobs":[],"obfuscation":"YZq9zsRja0g2M"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":48,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":":\n\n","logprobs":[],"obfuscation":"mhDAmaHJUvLGl"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":49,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"``","logprobs":[],"obfuscation":"3XmO5YTsWjzHHf"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":50,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`\n","logprobs":[],"obfuscation":"4fmXZmdkPxNn8K"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":51,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"src","logprobs":[],"obfuscation":"ifGf4yLEg5pMZ"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":52,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/L","logprobs":[],"obfuscation":"C1k1toBElpgxyW"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":53,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ibraries","logprobs":[],"obfuscation":"fdOTYTyp"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":54,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/M","logprobs":[],"obfuscation":"DyscJIQYaPJugC"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":55,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"icrosoft","logprobs":[],"obfuscation":"PQxU7muP"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":56,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".Extensions","logprobs":[],"obfuscation":"RCJB8"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":57,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"i92CWxnAkwS4C9"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":58,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"I","logprobs":[],"obfuscation":"qfH8wVJN74vCfBM"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":59,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".A","logprobs":[],"obfuscation":"LcuBP89lZVCCH9"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":60,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"bst","logprobs":[],"obfuscation":"I8rKDbKN0zylv"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":61,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"ractions","logprobs":[],"obfuscation":"tOgiCPs5"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":62,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"/","logprobs":[],"obfuscation":"jgJjLruTbFJGDhU"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":63,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"README","logprobs":[],"obfuscation":"D5VSEFNde7"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":64,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".md","logprobs":[],"obfuscation":"7ZGJO5sZOTPBs"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":65,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"\n","logprobs":[],"obfuscation":"7Sv80haKTTwfEWj"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":66,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"``","logprobs":[],"obfuscation":"m1JSvZ8rrpJnH5"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":67,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"`\n\n","logprobs":[],"obfuscation":"U93PMKtCB5Pb5"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":68,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"This","logprobs":[],"obfuscation":"f5veTGedo9nM"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":69,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" file","logprobs":[],"obfuscation":"oEBwvP5FnPK"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":70,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" provides","logprobs":[],"obfuscation":"IVNCYwr"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":71,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" an","logprobs":[],"obfuscation":"3x6WquURIJ3ld"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":72,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" overview","logprobs":[],"obfuscation":"VR9yeiD"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":73,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" of","logprobs":[],"obfuscation":"z46dC1o2FC8Rs"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":74,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" the","logprobs":[],"obfuscation":"YfZGabvmgyoI"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":75,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" library","logprobs":[],"obfuscation":"TamElgEp"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":76,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"VfVfqbnHAfsJyJn"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":77,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" installation","logprobs":[],"obfuscation":"CGR"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":78,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" instructions","logprobs":[],"obfuscation":"xst"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":79,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"3u5wqRA2RXh2QP8"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":80,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" and","logprobs":[],"obfuscation":"tD4WZmOhepzQ"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":81,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" usage","logprobs":[],"obfuscation":"SadOK826mZ"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":82,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" examples","logprobs":[],"obfuscation":"5VpLKav"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":83,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" for","logprobs":[],"obfuscation":"xPvtjDSUic9E"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":84,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" its","logprobs":[],"obfuscation":"6duK61DX14vx"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":85,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" core","logprobs":[],"obfuscation":"Cz8trPLsCWu"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":86,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" components","logprobs":[],"obfuscation":"Gexuy"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":87,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":".","logprobs":[],"obfuscation":"HVeWkHoX1cc6hVh"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":88,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" If","logprobs":[],"obfuscation":"G1TOxxwvSEq4L"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":89,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" you","logprobs":[],"obfuscation":"xQlKeOixd1hv"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":90,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" have","logprobs":[],"obfuscation":"bX6P0qgFPnR"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":91,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" any","logprobs":[],"obfuscation":"KxH8EiMzXa1N"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":92,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" more","logprobs":[],"obfuscation":"kA0kxRPPqru"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":93,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" questions","logprobs":[],"obfuscation":"9HRCyD"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":94,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" about","logprobs":[],"obfuscation":"yYFZhtsSfc"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":95,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" it","logprobs":[],"obfuscation":"zpyEAwPWl8Ozh"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":96,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":",","logprobs":[],"obfuscation":"ivjn00lbmzDHiFU"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":97,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" feel","logprobs":[],"obfuscation":"O2edXDmkBqt"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":98,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" free","logprobs":[],"obfuscation":"MlpWh7p0P1F"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":99,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" to","logprobs":[],"obfuscation":"uMNfozGkKe6xW"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":100,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":" ask","logprobs":[],"obfuscation":"6rMOxwXhR8RY"} + + event: response.output_text.delta + data: {"type":"response.output_text.delta","sequence_number":101,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"delta":"!","logprobs":[],"obfuscation":"QPZMdhS0e5vYuRl"} + + event: response.output_text.done + data: {"type":"response.output_text.done","sequence_number":102,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!","logprobs":[]} + + event: response.content_part.done + data: {"type":"response.content_part.done","sequence_number":103,"item_id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","output_index":3,"content_index":0,"part":{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}} + + event: response.output_item.done + data: {"type":"response.output_item.done","sequence_number":104,"output_index":3,"item":{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}],"role":"assistant"}} + + event: response.completed + data: {"type":"response.completed","sequence_number":105,"response":{"id":"resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54","object":"response","created_at":1757299965,"status":"completed","background":false,"error":null,"incomplete_details":null,"instructions":null,"max_output_tokens":null,"max_tool_calls":null,"model":"gpt-4o-mini-2024-07-18","output":[{"id":"mcpl_68be44fd8f68819eba7a74a2f6d27a5a03a2537be0e84a54","type":"mcp_list_tools","server_label":"deepwiki","tools":[{"annotations":{"read_only":false},"description":"Get a list of documentation topics for a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_structure"},{"annotations":{"read_only":false},"description":"View documentation about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"}},"required":["repoName"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"read_wiki_contents"},{"annotations":{"read_only":false},"description":"Ask any question about a GitHub repository","input_schema":{"type":"object","properties":{"repoName":{"type":"string","description":"GitHub repository: owner/repo (e.g. \"facebook/react\")"},"question":{"type":"string","description":"The question to ask about the repository"}},"required":["repoName","question"],"additionalProperties":false,"$schema":"http://json-schema.org/draft-07/schema#"},"name":"ask_question"}]},{"id":"mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\"}","error":null,"name":"read_wiki_structure","output":"Available pages for dotnet/extensions:\n\n- 1 Overview\n- 2 Build System and CI/CD\n- 3 AI Extensions Framework\n - 3.1 Core Abstractions\n - 3.2 AI Function System\n - 3.3 Chat Completion\n - 3.4 Caching System\n - 3.5 Evaluation and Reporting\n- 4 HTTP Resilience and Diagnostics\n - 4.1 Standard Resilience\n - 4.2 Hedging Strategies\n- 5 Telemetry and Compliance\n- 6 Testing Infrastructure\n - 6.1 AI Service Integration Testing\n - 6.2 Time Provider Testing","server_label":"deepwiki"},{"id":"mcp_68be4505f134819e806c002f27cce0c303a2537be0e84a54","type":"mcp_call","approval_request_id":null,"arguments":"{\"repoName\":\"dotnet/extensions\",\"question\":\"What is the path to the README.md file for Microsoft.Extensions.AI.Abstractions?\"}","error":null,"name":"ask_question","output":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` is `src/Libraries/Microsoft.Extensions.AI.Abstractions/README.md` . This file provides an overview of the `Microsoft.Extensions.AI.Abstractions` library, including installation instructions and usage examples for its core components like `IChatClient` and `IEmbeddingGenerator` .\n\n## README.md Content Overview\nThe `README.md` file for `Microsoft.Extensions.AI.Abstractions` details the purpose of the library, which is to provide abstractions for generative AI components . It includes instructions on how to install the NuGet package `Microsoft.Extensions.AI.Abstractions` .\n\nThe document also provides usage examples for the `IChatClient` interface, which defines methods for interacting with AI services that offer \"chat\" capabilities . This includes examples for requesting both complete and streaming chat responses .\n\nFurthermore, the `README.md` explains the `IEmbeddingGenerator` interface, which is used for generating vector embeddings from input values . It demonstrates how to use `GenerateAsync` to create embeddings . The file also discusses how both `IChatClient` and `IEmbeddingGenerator` implementations can be layered to create pipelines of functionality, incorporating features like caching and telemetry .\n\nNotes:\nThe user's query specifically asked for the path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions`. The provided codebase context, particularly the wiki page for \"AI Extensions Framework\", directly lists this file as a relevant source file . The content of the `README.md` file itself further confirms its relevance to the `Microsoft.Extensions.AI.Abstractions` library.\n\nWiki pages you might want to explore:\n- [AI Extensions Framework (dotnet/extensions)](/wiki/dotnet/extensions#3)\n- [Chat Completion (dotnet/extensions)](/wiki/dotnet/extensions#3.3)\n\nView this search on DeepWiki: https://deepwiki.com/search/what-is-the-path-to-the-readme_bb6bee43-3136-4b21-bc5d-02ca1611d857\n","server_label":"deepwiki"},{"id":"msg_68be450c39e8819eb9bf6fcb9fd16ecb03a2537be0e84a54","type":"message","status":"completed","content":[{"type":"output_text","annotations":[],"logprobs":[],"text":"The path to the `README.md` file for `Microsoft.Extensions.AI.Abstractions` in the `dotnet/extensions` repository is:\n\n```\nsrc/Libraries/Microsoft.Extensions.AI.Abstractions/README.md\n```\n\nThis file provides an overview of the library, installation instructions, and usage examples for its core components. If you have any more questions about it, feel free to ask!"}],"role":"assistant"}],"parallel_tool_calls":true,"previous_response_id":null,"prompt_cache_key":null,"reasoning":{"effort":null,"summary":null},"safety_identifier":null,"service_tier":"default","store":true,"temperature":1.0,"text":{"format":{"type":"text"},"verbosity":"medium"},"tool_choice":"auto","tools":[{"type":"mcp","allowed_tools":null,"headers":null,"require_approval":"never","server_description":null,"server_label":"deepwiki","server_url":"https://mcp.deepwiki.com/"}],"top_logprobs":0,"top_p":1.0,"truncation":"disabled","usage":{"input_tokens":1420,"input_tokens_details":{"cached_tokens":0},"output_tokens":149,"output_tokens_details":{"reasoning_tokens":0},"total_tokens":1569},"user":null,"metadata":{}}} + + + """; + + using VerbatimHttpHandler handler = new(Input, Output); + using HttpClient httpClient = new(handler); + using IChatClient client = CreateResponseClient(httpClient, "gpt-4o-mini"); + + ChatOptions chatOptions = new() + { + Tools = [new HostedMcpServerTool("deepwiki", "https://mcp.deepwiki.com/mcp") + { + ApprovalMode = HostedMcpServerToolApprovalMode.NeverRequire, + } + ], + }; + + var response = await client.GetStreamingResponseAsync("Tell me the path to the README.md file for Microsoft.Extensions.AI.Abstractions in the dotnet/extensions repository", chatOptions) + .ToChatResponseAsync(); + Assert.NotNull(response); + + Assert.Equal("resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54", response.ResponseId); + Assert.Equal("resp_68be44fd7298819e82fd82c8516e970d03a2537be0e84a54", response.ConversationId); + Assert.Equal("gpt-4o-mini-2024-07-18", response.ModelId); + Assert.Equal(DateTimeOffset.FromUnixTimeSeconds(1_757_299_965), response.CreatedAt); + Assert.Equal(ChatFinishReason.Stop, response.FinishReason); + + var message = Assert.Single(response.Messages); + Assert.Equal(ChatRole.Assistant, response.Messages[0].Role); + Assert.StartsWith("The path to the `README.md` file", response.Messages[0].Text); + + Assert.Equal(6, message.Contents.Count); + + var firstCall = Assert.IsType(message.Contents[1]); + Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstCall.CallId); + Assert.Equal("deepwiki", firstCall.ServerName); + Assert.Equal("read_wiki_structure", firstCall.ToolName); + Assert.NotNull(firstCall.Arguments); + Assert.Single(firstCall.Arguments); + Assert.Equal("dotnet/extensions", ((JsonElement)firstCall.Arguments["repoName"]!).GetString()); + + var firstResult = Assert.IsType(message.Contents[2]); + Assert.Equal("mcp_68be4503d45c819e89cb574361c8eba003a2537be0e84a54", firstResult.CallId); + 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("deepwiki", secondCall.ServerName); + Assert.Equal("ask_question", secondCall.ToolName); + Assert.NotNull(secondCall.Arguments); + Assert.Equal("dotnet/extensions", ((JsonElement)secondCall.Arguments["repoName"]!).GetString()); + 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.NotNull(secondResult.Output); + Assert.StartsWith("The path to the `README.md` file", Assert.IsType(Assert.Single(secondResult.Output)).Text); + + Assert.NotNull(response.Usage); + Assert.Equal(1420, response.Usage.InputTokenCount); + Assert.Equal(149, response.Usage.OutputTokenCount); + Assert.Equal(1569, response.Usage.TotalTokenCount); + } + private static IChatClient CreateResponseClient(HttpClient httpClient, string modelId) => new OpenAIClient( new ApiKeyCredential("apikey"),