diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 7dceac52f3..9878d5e791 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -6,7 +6,7 @@ - + diff --git a/dotnet/agent-framework-dotnet.slnx b/dotnet/agent-framework-dotnet.slnx index a38787036d..9833c90209 100644 --- a/dotnet/agent-framework-dotnet.slnx +++ b/dotnet/agent-framework-dotnet.slnx @@ -113,10 +113,9 @@ - + - diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj index 1e565d44d0..e42dca5252 100644 --- a/dotnet/samples/GettingStarted/GettingStarted.csproj +++ b/dotnet/samples/GettingStarted/GettingStarted.csproj @@ -15,6 +15,7 @@ + @@ -29,7 +30,6 @@ - diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj deleted file mode 100644 index caaf31da47..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/Microsoft.Extensions.AI.Agents.AzureAI.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - $(ProjectsTargetFrameworks) - $(ProjectsDebugTargetFrameworks) - alpha - - - - - - - - - - - - Microsoft.Extensions.AI.Agents.AzureAI - Implementation of generative AI abstractions for Azure AI Persistent Agents. - - - diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs deleted file mode 100644 index fad92f7696..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs +++ /dev/null @@ -1,471 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Text.Json.Nodes; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.Agents.Persistent; - -namespace Microsoft.Extensions.AI.AzureAIAgentsPersistent; - -/// Represents an for an Azure.AI.Agents.Persistent . -public sealed partial class PersistentAgentsChatClient : IChatClient -{ - /// The name of the chat client provider. - private const string ProviderName = "azure"; - - /// The underlying . - private readonly PersistentAgentsClient _client; - - /// Metadata for the client. - private readonly ChatClientMetadata _metadata; - - /// The ID of the agent to use. - private readonly string _agentId; - - /// The thread ID to use if none is supplied in . - private readonly string? _defaultThreadId; - - /// List of tools associated with the agent. - private IReadOnlyList? _agentTools; - - /// Initializes a new instance of the class for the specified . - public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? defaultThreadId = null) - { - if (client is null) - { - throw new ArgumentNullException(nameof(client)); - } - - if (string.IsNullOrWhiteSpace(agentId)) - { - throw new ArgumentException("Cannot be null or be comprised of only whitespace.", nameof(agentId)); - } - - this._client = client; - this._agentId = agentId; - this._defaultThreadId = defaultThreadId; - - this._metadata = new(ProviderName); - } - - /// - public object? GetService(Type serviceType, object? serviceKey = null) => - serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) : - serviceKey is not null ? null : - serviceType == typeof(ChatClientMetadata) ? this._metadata : - serviceType == typeof(PersistentAgentsClient) ? this._client : - serviceType.IsInstanceOfType(this) ? this : - null; - - /// - public Task GetResponseAsync( - IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) => - this.GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken); - - /// - public async IAsyncEnumerable GetStreamingResponseAsync( - IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - if (messages is null) - { - throw new ArgumentNullException(nameof(messages)); - } - - // Extract necessary state from messages and options. - (ThreadAndRunOptions runOptions, List? toolResults) = - await this.CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false); - - // Get the thread ID. - string? threadId = options?.ConversationId ?? this._defaultThreadId; - if (threadId is null && toolResults is not null) - { - throw new ArgumentException("No thread ID was provided, but chat messages includes tool results.", nameof(messages)); - } - - // Get any active run ID for this thread. - ThreadRun? threadRun = null; - if (threadId is not null) - { - await foreach (ThreadRun? run in this._client.Runs.GetRunsAsync(threadId, limit: 1, ListSortOrder.Descending, cancellationToken: cancellationToken).ConfigureAwait(false)) - { - if (run.Status != RunStatus.Completed && run.Status != RunStatus.Cancelled && run.Status != RunStatus.Failed && run.Status != RunStatus.Expired) - { - threadRun = run; - break; - } - } - } - - // Submit the request. - IAsyncEnumerable updates; - if (threadRun is not null && - ConvertFunctionResultsToToolOutput(toolResults, out List? toolOutputs) is { } toolRunId && - toolRunId == threadRun.Id) - { - // There's an active run and we have tool results to submit, so submit the results and continue streaming. - // This is going to ignore any additional messages in the run options, as we are only submitting tool outputs, - // but there doesn't appear to be a way to submit additional messages, and having such additional messages is rare. - updates = this._client.Runs.SubmitToolOutputsToStreamAsync(threadRun, toolOutputs, cancellationToken); - } - else - { - if (threadId is null) - { - // No thread ID was provided, so create a new thread. - PersistentAgentThread thread = await this._client.Threads.CreateThreadAsync(runOptions.ThreadOptions.Messages, runOptions.ToolResources, runOptions.Metadata, cancellationToken).ConfigureAwait(false); - runOptions.ThreadOptions.Messages.Clear(); - threadId = thread.Id; - } - else if (threadRun is not null) - { - // There was an active run; we need to cancel it before starting a new run. - await this._client.Runs.CancelRunAsync(threadId, threadRun.Id, cancellationToken).ConfigureAwait(false); - threadRun = null; - } - - // Now create a new run and stream the results. - updates = this._client.Runs.CreateRunStreamingAsync( - threadId: threadId, - agentId: this._agentId, - overrideModelName: runOptions?.OverrideModelName, - overrideInstructions: runOptions?.OverrideInstructions, - additionalInstructions: null, - additionalMessages: runOptions?.ThreadOptions.Messages, - overrideTools: runOptions?.OverrideTools, - temperature: runOptions?.Temperature, - topP: runOptions?.TopP, - maxPromptTokens: runOptions?.MaxPromptTokens, - maxCompletionTokens: runOptions?.MaxCompletionTokens, - truncationStrategy: runOptions?.TruncationStrategy, - toolChoice: runOptions?.ToolChoice, - responseFormat: runOptions?.ResponseFormat, - parallelToolCalls: runOptions?.ParallelToolCalls, - metadata: runOptions?.Metadata, - cancellationToken); - } - - // Process each update. - string? responseId = null; - await foreach (StreamingUpdate? update in updates.ConfigureAwait(false)) - { - switch (update) - { - case ThreadUpdate tu: - threadId ??= tu.Value.Id; - goto default; - - case RunUpdate ru: - threadId ??= ru.Value.ThreadId; - responseId ??= ru.Value.Id; - - ChatResponseUpdate ruUpdate = new() - { - AuthorName = ru.Value.AssistantId, - ConversationId = threadId, - CreatedAt = ru.Value.CreatedAt, - MessageId = responseId, - ModelId = ru.Value.Model, - RawRepresentation = ru, - ResponseId = responseId, - Role = ChatRole.Assistant, - }; - - if (ru.Value.Usage is { } usage) - { - ruUpdate.Contents.Add(new UsageContent(new() - { - InputTokenCount = usage.PromptTokens, - OutputTokenCount = usage.CompletionTokens, - TotalTokenCount = usage.TotalTokens, - })); - } - - if (ru is RequiredActionUpdate rau && rau.ToolCallId is string toolCallId && rau.FunctionName is string functionName) - { - ruUpdate.Contents.Add( - new FunctionCallContent( - JsonSerializer.Serialize([ru.Value.Id, toolCallId], AgentsChatClientJsonContext.Default.StringArray), - functionName, - JsonSerializer.Deserialize(rau.FunctionArguments, AgentsChatClientJsonContext.Default.IDictionaryStringObject)!)); - } - - yield return ruUpdate; - break; - - case MessageContentUpdate mcu: - yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text) - { - ConversationId = threadId, - MessageId = responseId, - RawRepresentation = mcu, - ResponseId = responseId, - }; - break; - - default: - yield return new ChatResponseUpdate - { - ConversationId = threadId, - MessageId = responseId, - RawRepresentation = update, - ResponseId = responseId, - Role = ChatRole.Assistant, - }; - break; - } - } - } - - /// - public void Dispose() { } - - /// - /// Creates the to use for the request and extracts any function result contents - /// that need to be submitted as tool results. - /// - private async ValueTask<(ThreadAndRunOptions RunOptions, List? ToolResults)> CreateRunOptionsAsync( - IEnumerable messages, ChatOptions? options, CancellationToken cancellationToken) - { - // Create the options instance to populate, either a fresh or using one the caller provides. - ThreadAndRunOptions runOptions = - options?.RawRepresentationFactory?.Invoke(this) as ThreadAndRunOptions ?? - new(); - - // Populate the run options from the ChatOptions, if provided. - if (options is not null) - { - runOptions.MaxCompletionTokens ??= options.MaxOutputTokens; - runOptions.OverrideModelName ??= options.ModelId; - runOptions.TopP ??= options.TopP; - runOptions.Temperature ??= options.Temperature; - runOptions.ParallelToolCalls ??= options.AllowMultipleToolCalls; - // Ignored: options.TopK, options.FrequencyPenalty, options.Seed, options.StopSequences - - if (options.Tools is { Count: > 0 } tools) - { - List toolDefinitions = []; - ToolResources? toolResources = null; - - // If the caller has provided any tool overrides, we'll assume they don't want to use the agent's tools. - // But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to - // just add them. To handle that, we'll get all of the agent's tools and add them to the override list - // along with our tools. - if (runOptions.OverrideTools is null || !runOptions.OverrideTools.Any()) - { - if (this._agentTools is null) - { - PersistentAgent agent = await this._client.Administration.GetAgentAsync(this._agentId, cancellationToken).ConfigureAwait(false); - this._agentTools = agent.Tools; - } - - toolDefinitions.AddRange(this._agentTools); - } - - // The caller can provide tools in the supplied ThreadAndRunOptions. - if (runOptions.OverrideTools is not null) - { - toolDefinitions.AddRange(runOptions.OverrideTools); - } - - // Now add the tools from ChatOptions.Tools. - foreach (AITool tool in tools) - { - switch (tool) - { - case AIFunction aiFunction: - toolDefinitions.Add(new FunctionToolDefinition( - aiFunction.Name, - aiFunction.Description, - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AgentsChatClientJsonContext.Default.JsonElement)))); - break; - - case HostedCodeInterpreterTool: - toolDefinitions.Add(new CodeInterpreterToolDefinition()); - - // Once available, HostedCodeInterpreterTool.FileIds property will be used instead of the AdditionalProperties. - if (tool.AdditionalProperties.TryGetValue("fileIds", out object? fileIdsObject) && fileIdsObject is IEnumerable fileIds) - { - foreach (var fileId in fileIds) - { - (toolResources ??= new() { CodeInterpreter = new() }).CodeInterpreter.FileIds.Add(fileId); - } - } - break; - - case HostedWebSearchTool webSearch when webSearch.AdditionalProperties?.TryGetValue("connectionId", out object? connectionId) is true: - toolDefinitions.Add(new BingGroundingToolDefinition(new BingGroundingSearchToolParameters([new BingGroundingSearchConfiguration(connectionId!.ToString())]))); - break; - } - } - - if (toolDefinitions.Count > 0) - { - runOptions.OverrideTools = toolDefinitions; - } - - if (toolResources is not null) - { - runOptions.ToolResources = toolResources; - } - } - - // Store the tool mode, if relevant. - if (runOptions.ToolChoice is null) - { - switch (options.ToolMode) - { - case NoneChatToolMode: - runOptions.ToolChoice = BinaryData.FromString("none"); - break; - - case RequiredChatToolMode required: - runOptions.ToolChoice = required.RequiredFunctionName is string functionName ? - BinaryData.FromString($$"""{"type": "function", "function": {"name": "{{functionName}}"} }""") : - BinaryData.FromString("required"); - break; - } - } - - // Store the response format, if relevant. - if (runOptions.ResponseFormat is null) - { - if (options.ResponseFormat is ChatResponseFormatJson jsonFormat) - { - runOptions.ResponseFormat = jsonFormat.Schema is { } schema ? - BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(new() - { - ["type"] = "json_schema", - ["json_schema"] = JsonSerializer.SerializeToNode(schema, AgentsChatClientJsonContext.Default.JsonNode), - }, AgentsChatClientJsonContext.Default.JsonObject)) : - BinaryData.FromString("""{ "type": "json_object" }"""); - } - } - } - - // Process ChatMessages. System messages are turned into additional instructions. - // All other messages are added 1:1, treating assistant messages as agent messages - // and everything else as user messages. - StringBuilder? instructions = null; - List? functionResults = null; - - runOptions.ThreadOptions ??= new(); - - foreach (ChatMessage chatMessage in messages) - { - List messageContents = []; - - if (chatMessage.Role == ChatRole.System || - chatMessage.Role == new ChatRole("developer")) - { - instructions ??= new(); - foreach (TextContent textContent in chatMessage.Contents.OfType()) - { - _ = instructions.Append(textContent); - } - - continue; - } - - foreach (AIContent content in chatMessage.Contents) - { - switch (content) - { - case TextContent text: - messageContents.Add(new MessageInputTextBlock(text.Text)); - break; - - case DataContent image when image.HasTopLevelMediaType("image"): - messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri))); - break; - - case UriContent image when image.HasTopLevelMediaType("image"): - messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri.AbsoluteUri))); - break; - - case FunctionResultContent result: - (functionResults ??= []).Add(result); - break; - - default: - if (content.RawRepresentation is MessageInputContentBlock rawContent) - { - messageContents.Add(rawContent); - } - break; - } - } - - if (messageContents.Count > 0) - { - runOptions.ThreadOptions.Messages.Add(new ThreadMessageOptions( - chatMessage.Role == ChatRole.Assistant ? MessageRole.Agent : MessageRole.User, - messageContents)); - } - } - - if (instructions is not null) - { - runOptions.OverrideInstructions = instructions.ToString(); - } - - return (runOptions, functionResults); - } - - /// Convert instances to instances. - /// The tool results to process. - /// The generated list of tool outputs, if any could be created. - /// The run ID associated with the corresponding function call requests. - private static string? ConvertFunctionResultsToToolOutput(List? toolResults, out List? toolOutputs) - { - string? runId = null; - toolOutputs = null; - if (toolResults?.Count > 0) - { - foreach (FunctionResultContent frc in toolResults) - { - // When creating the FunctionCallContext, we created it with a CallId == [runId, callId]. - // We need to extract the run ID and ensure that the ToolOutput we send back to Azure - // is only the call ID. - string[]? runAndCallIDs; - try - { - runAndCallIDs = JsonSerializer.Deserialize(frc.CallId, AgentsChatClientJsonContext.Default.StringArray); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch -#pragma warning restore CA1031 // Do not catch general exception types - { - continue; - } - - if (runAndCallIDs is null || - runAndCallIDs.Length != 2 || - string.IsNullOrWhiteSpace(runAndCallIDs[0]) || // run ID - string.IsNullOrWhiteSpace(runAndCallIDs[1]) || // call ID - (runId is not null && runId != runAndCallIDs[0])) - { - continue; - } - - runId = runAndCallIDs[0]; - (toolOutputs ??= []).Add(new(runAndCallIDs[1], frc.Result?.ToString() ?? string.Empty)); - } - } - - return runId; - } - - [JsonSerializable(typeof(JsonElement))] - [JsonSerializable(typeof(JsonNode))] - [JsonSerializable(typeof(JsonObject))] - [JsonSerializable(typeof(string[]))] - [JsonSerializable(typeof(IDictionary))] - private sealed partial class AgentsChatClientJsonContext : JsonSerializerContext; -} diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs deleted file mode 100644 index 9d3402517c..0000000000 --- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsClientExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.AI.Agents.Persistent; -using Microsoft.Extensions.AI.AzureAIAgentsPersistent; - -namespace Microsoft.Extensions.AI; - -/// -/// Provides extension methods for . -/// -public static class PersistentAgentsClientExtensions -{ - /// - /// Creates an for a client for interacting with a specific agent. - /// - /// The instance to be accessed as an . - /// The unique identifier of the agent with which to interact. - /// - /// An optional existing thread identifier for the chat session. This serves as a default, and may be overridden per call to - /// or via the - /// property. If not thread ID is provided via either mechanism, a new thread will be created for the request. - /// - /// An instance configured to interact with the specified agent and thread. - public static IChatClient AsIChatClient(this PersistentAgentsClient client, string agentId, string? threadId = null) => - new PersistentAgentsChatClient(client, agentId, threadId); -} diff --git a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj index 9a0f2f56bf..4c64d6cf15 100644 --- a/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj +++ b/dotnet/tests/AzureAIAgentsPersistent.IntegrationTests/AzureAIAgentsPersistent.IntegrationTests.csproj @@ -7,11 +7,11 @@ - +