diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 38ed7f9884..7277c0c761 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -23,10 +23,10 @@
-
+
-
+
diff --git a/dotnet/samples/GettingStarted/AgentSample.cs b/dotnet/samples/GettingStarted/AgentSample.cs
index 620c395d51..686f6bb383 100644
--- a/dotnet/samples/GettingStarted/AgentSample.cs
+++ b/dotnet/samples/GettingStarted/AgentSample.cs
@@ -7,8 +7,8 @@ using Azure.Identity;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.AI.Agents;
using Microsoft.Shared.Samples;
-using OpenAI;
using OpenAI.Assistants;
+using OpenAI.Chat;
using OpenAI.Responses;
#pragma warning disable OPENAI001
@@ -53,10 +53,6 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
_ => null
};
- protected OpenAIClient OpenAIClient => new(TestConfiguration.OpenAI.ApiKey);
-
- protected PersistentAgentsClient AzureAIPersistentAgentsClient => new(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
-
///
/// For providers that store the agent and the thread on the server side, this will clean and delete
/// any sample agent and thread that was created during this execution.
@@ -83,8 +79,7 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
private Task GetOpenAIChatClientAsync()
=> Task.FromResult(
- OpenAIClient
- .GetChatClient(TestConfiguration.OpenAI.ChatModelId)
+ new ChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.AsIChatClient());
private Task GetAzureOpenAIChatClientAsync()
@@ -98,39 +93,52 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
private Task GetOpenAIResponsesClientAsync()
=> Task.FromResult(
- OpenAIClient
- .GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
+ new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.AsIChatClient());
private async Task GetAzureAIAgentPersistentClientAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
{
- // Create a server side agent to work with.
- var persistentAgentResponse = await AzureAIPersistentAgentsClient.Administration.CreateAgentAsync(
- model: TestConfiguration.AzureAI.DeploymentName,
- name: options.Name,
- instructions: options.Instructions,
- cancellationToken: cancellationToken);
+ var persistentAgentsClient = new PersistentAgentsClient(
+ TestConfiguration.AzureAI.Endpoint,
+ new AzureCliCredential());
- var persistentAgent = persistentAgentResponse.Value;
+ // If the Id is not provided, create a new agent.
+ if (options.Id is null)
+ {
+ var persistentAgentResponse = await persistentAgentsClient.Administration.CreateAgentAsync(
+ model: TestConfiguration.AzureAI.DeploymentName,
+ name: options.Name,
+ instructions: options.Instructions,
+ cancellationToken: cancellationToken);
- // Get the chat client to use for the agent.
- return AzureAIPersistentAgentsClient.AsIChatClient(persistentAgent.Id);
+ var persistentAgent = persistentAgentResponse.Value;
+
+ return persistentAgentsClient.AsIChatClient(persistentAgent.Id);
+ }
+
+ return persistentAgentsClient.AsIChatClient(options.Id);
}
private async Task GetOpenAIAssistantChatClientAsync(ChatClientAgentOptions options, CancellationToken cancellationToken)
{
- var assistantClient = OpenAIClient.GetAssistantClient();
+ var assistantClient = new AssistantClient(TestConfiguration.OpenAI.ApiKey);
- Assistant assistant = await assistantClient.CreateAssistantAsync(
- TestConfiguration.OpenAI.ChatModelId,
- new()
- {
- Name = options.Name,
- Instructions = options.Instructions
- },
- cancellationToken);
+ // If the Id is not provided, create a new assistant.
+ if (options.Id is null)
+ {
+ Assistant assistant = await assistantClient.CreateAssistantAsync(
+ TestConfiguration.OpenAI.ChatModelId,
+ new()
+ {
+ Name = options.Name,
+ Instructions = options.Instructions
+ },
+ cancellationToken);
- return assistantClient.AsIChatClient(assistant.Id);
+ return new NewOpenAIAssistantChatClient(assistantClient, assistant.Id, null);
+ }
+
+ return new NewOpenAIAssistantChatClient(assistantClient, options.Id, null);
}
#endregion
diff --git a/dotnet/samples/GettingStarted/External/MEAI.Abstractions/NewHostedCodeInterpreterTool.cs b/dotnet/samples/GettingStarted/External/MEAI.Abstractions/NewHostedCodeInterpreterTool.cs
new file mode 100644
index 0000000000..1c018593b8
--- /dev/null
+++ b/dotnet/samples/GettingStarted/External/MEAI.Abstractions/NewHostedCodeInterpreterTool.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using Microsoft.Extensions.AI;
+
+namespace OpenAI.Assistants;
+
+///
+/// Proposal for abstraction updates based on the common code interpreter tool properties.
+/// Based on the decision, the abstraction can be updated in M.E.AI directly.
+///
+public class NewHostedCodeInterpreterTool : HostedCodeInterpreterTool
+{
+ // Usage of an internal dictionary is temporary and only used here because the MEAI.Abstractions does not have this specialization yet and the
+ // ChatClients must rely on the AdditionalProperties to check and set correctly the Code Interpreter Resource avoiding a customized RawRepresentationFactory implementation.
+ private readonly Dictionary _additionalProperties = [];
+
+ /// Gets or sets the list of file IDs that the code interpreter tool can access.
+ public IList FileIds
+ {
+ get
+ {
+ // Only create the property in the dictionary when it is actually used
+ if (!this._additionalProperties.TryGetValue("fileIds", out var value) || value is null)
+ {
+ value = new List();
+ this._additionalProperties["fileIds"] = value;
+ }
+
+ return (IList)value;
+ }
+ }
+
+ ///
+ public override IReadOnlyDictionary AdditionalProperties => this._additionalProperties;
+}
diff --git a/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs b/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs
new file mode 100644
index 0000000000..9a747ea620
--- /dev/null
+++ b/dotnet/samples/GettingStarted/External/MEAI.OpenAI/NewOpenAIAssistantChatClient.cs
@@ -0,0 +1,644 @@
+#pragma warning disable IDE0005 // Using directive is unnecessary.
+#pragma warning disable IDE0073 // The file header does not match the required text
+#pragma warning disable CS0436 // Type conflicts with imported type
+#pragma warning disable CA1063 // Implement IDisposable Correctly
+
+// 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;
+using System.Reflection;
+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 Microsoft.Shared.Diagnostics;
+using OpenAI;
+using OpenAI.Assistants;
+using OpenAI.Audio;
+using OpenAI.Chat;
+using OpenAI.Embeddings;
+
+#pragma warning disable CA1031 // Do not catch general exception types
+#pragma warning disable SA1005 // Single line comments should begin with single space
+#pragma warning disable SA1204 // Static elements should appear before instance elements
+#pragma warning disable S125 // Sections of code should not be commented out
+#pragma warning disable S907 // "goto" statement should not be used
+#pragma warning disable S1067 // Expressions should not be too complex
+#pragma warning disable S1751 // Loops with at most one iteration should be refactored
+#pragma warning disable S3011 // Reflection should not be used to increase accessibility of classes, methods, or fields
+#pragma warning disable S4456 // Parameter validation in yielding methods should be wrapped
+#pragma warning disable S4457 // Parameter validation in "async"/"await" methods should be wrapped
+
+namespace Microsoft.Extensions.AI;
+
+/// Represents an for an Azure.AI.Agents.Persistent .
+public sealed class NewOpenAIAssistantChatClient : IChatClient
+{
+ /// The underlying .
+#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ private readonly AssistantClient _client;
+
+ /// Metadata for the client.
+ private readonly ChatClientMetadata _metadata;
+
+ /// The ID of the agent to use.
+ private readonly string _assistantId;
+
+ /// The thread ID to use if none is supplied in .
+ private readonly string? _defaultThreadId;
+
+ /// List of tools associated with the assistant.
+ private IReadOnlyList? _assistantTools;
+
+ /// Initializes a new instance of the class for the specified .
+ public NewOpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId)
+ {
+ _client = Throw.IfNull(assistantClient);
+ _assistantId = Throw.IfNullOrWhitespace(assistantId);
+
+ _defaultThreadId = defaultThreadId;
+
+ // https://github.com/openai/openai-dotnet/issues/215
+ // The endpoint isn't currently exposed, so use reflection to get at it, temporarily. Once packages
+ // implement the abstractions directly rather than providing adapters on top of the public APIs,
+ // the package can provide such implementations separate from what's exposed in the public API.
+ Uri providerUrl = typeof(AssistantClient).GetField("_endpoint", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance)
+ ?.GetValue(assistantClient) as Uri ?? OpenAIClientExtensions2.DefaultOpenAIEndpoint;
+
+ _metadata = new("openai", providerUrl);
+ }
+
+ ///
+ public object? GetService(Type serviceType, object? serviceKey = null) =>
+ serviceType is null ? throw new ArgumentNullException(nameof(serviceType)) :
+ serviceKey is not null ? null :
+ serviceType == typeof(ChatClientMetadata) ? _metadata :
+ serviceType == typeof(AssistantClient) ? _client :
+ serviceType.IsInstanceOfType(this) ? this :
+ null;
+
+ ///
+ public Task GetResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
+ GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken);
+
+ ///
+ public async IAsyncEnumerable GetStreamingResponseAsync(
+ IEnumerable messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ _ = Throw.IfNull(messages);
+
+ // Extract necessary state from messages and options.
+ (RunCreationOptions runOptions, List? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false);
+
+ // Get the thread ID.
+ string? threadId = options?.ConversationId ?? _defaultThreadId;
+ if (threadId is null && toolResults is not null)
+ {
+ Throw.ArgumentException(nameof(messages), "No thread ID was provided, but chat messages includes tool results.");
+ }
+
+ // Get any active run ID for this thread. This is necessary in case a thread has been left with an
+ // active run, in which all attempts other than submitting tools will fail. We thus need to cancel
+ // any active run on the thread.
+ ThreadRun? threadRun = null;
+ if (threadId is not null)
+ {
+ await foreach (var run in _client.GetRunsAsync(
+ threadId,
+ new RunCollectionOptions { Order = RunCollectionOrder.Descending, PageSizeLimit = 1 },
+ 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 = _client.SubmitToolOutputsToRunStreamingAsync(threadRun.ThreadId, threadRun.Id, toolOutputs, cancellationToken);
+ }
+ else
+ {
+ if (threadId is null)
+ {
+ // No thread ID was provided, so create a new thread.
+ ThreadCreationOptions threadCreationOptions = new();
+ foreach (var message in runOptions.AdditionalMessages)
+ {
+ threadCreationOptions.InitialMessages.Add(message);
+ }
+
+ runOptions.AdditionalMessages.Clear();
+
+ var thread = await _client.CreateThreadAsync(threadCreationOptions, cancellationToken).ConfigureAwait(false);
+ threadId = thread.Value.Id;
+ }
+ else if (threadRun is not null)
+ {
+ // There was an active run; we need to cancel it before starting a new run.
+ _ = await _client.CancelRunAsync(threadId, threadRun.Id, cancellationToken).ConfigureAwait(false);
+ threadRun = null;
+ }
+
+ // Now create a new run and stream the results.
+ updates = _client.CreateRunStreamingAsync(
+ threadId: threadId,
+ _assistantId,
+ runOptions,
+ cancellationToken);
+ }
+
+ // Process each update.
+ string? responseId = null;
+ await foreach (var 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 = _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.InputTokenCount,
+ OutputTokenCount = usage.OutputTokenCount,
+ TotalTokenCount = usage.TotalTokenCount,
+ }));
+ }
+
+ 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], OpenAIJsonContext.Default.StringArray),
+ functionName,
+ JsonSerializer.Deserialize(rau.FunctionArguments, OpenAIJsonContext.Default.IDictionaryStringObject)!));
+ }
+
+ yield return ruUpdate;
+ break;
+
+ case MessageContentUpdate mcu:
+ yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text)
+ {
+ AuthorName = _assistantId,
+ ConversationId = threadId,
+ MessageId = responseId,
+ RawRepresentation = mcu,
+ ResponseId = responseId,
+ };
+ break;
+
+ default:
+ yield return new ChatResponseUpdate
+ {
+ AuthorName = _assistantId,
+ ConversationId = threadId,
+ MessageId = responseId,
+ RawRepresentation = update,
+ ResponseId = responseId,
+ Role = ChatRole.Assistant,
+ };
+ break;
+ }
+ }
+ }
+
+ ///
+ void IDisposable.Dispose()
+ {
+ // nop
+ }
+
+ /// Converts an Extensions function to an OpenAI assistants function tool.
+ internal static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null)
+ {
+ bool? strict =
+ OpenAIClientExtensions2.HasStrict(aiFunction.AdditionalProperties) ??
+ OpenAIClientExtensions2.HasStrict(options?.AdditionalProperties);
+
+ return new FunctionToolDefinition(aiFunction.Name)
+ {
+ Description = aiFunction.Description,
+ Parameters = OpenAIClientExtensions2.ToOpenAIFunctionParameters(aiFunction, strict),
+ StrictParameterSchemaEnabled = strict,
+ };
+ }
+
+ ///
+ /// Creates the to use for the request and extracts any function result contents
+ /// that need to be submitted as tool results.
+ ///
+ private async ValueTask<(RunCreationOptions 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.
+ RunCreationOptions runOptions =
+ options?.RawRepresentationFactory?.Invoke(this) as RunCreationOptions ??
+ new();
+
+ // Populate the run options from the ChatOptions, if provided.
+ if (options is not null)
+ {
+ runOptions.MaxOutputTokenCount ??= options.MaxOutputTokens;
+ runOptions.ModelOverride ??= options.ModelId;
+ runOptions.NucleusSamplingFactor ??= options.TopP;
+ runOptions.Temperature ??= options.Temperature;
+ runOptions.AllowParallelToolCalls ??= options.AllowMultipleToolCalls;
+
+ if (options.Tools is { Count: > 0 } tools)
+ {
+ // If the caller has provided any tool overrides, we'll assume they don't want to use the assistant'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 assistant's tools and add them to the override list
+ // along with our tools.
+ if (runOptions.ToolsOverride.Count == 0)
+ {
+ if (_assistantTools is null)
+ {
+ var assistant = await _client.GetAssistantAsync(_assistantId, cancellationToken).ConfigureAwait(false);
+ _assistantTools = assistant.Value.Tools;
+ }
+
+ foreach (var tool in _assistantTools)
+ {
+ runOptions.ToolsOverride.Add(tool);
+ }
+ }
+
+ // The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools.
+ foreach (AITool tool in tools)
+ {
+ switch (tool)
+ {
+ case AIFunction aiFunction:
+ runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
+ break;
+
+ case HostedCodeInterpreterTool:
+ var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
+ runOptions.ToolsOverride.Add(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)
+ {
+ var threadInitializationMessage = new ThreadInitializationMessage(OpenAI.Assistants.MessageRole.User, [OpenAI.Assistants.MessageContent.FromText("attachments")]);
+
+ foreach (var fileId in fileIds)
+ {
+ threadInitializationMessage.Attachments.Add(new(fileId, [codeInterpreterToolDefinition]));
+ }
+
+ runOptions.AdditionalMessages.Add(threadInitializationMessage);
+ }
+
+ break;
+ }
+ }
+ }
+
+ // Store the tool mode, if relevant.
+ if (runOptions.ToolConstraint is null)
+ {
+ switch (options.ToolMode)
+ {
+ case NoneChatToolMode:
+ runOptions.ToolConstraint = ToolConstraint.None;
+ break;
+
+ case AutoChatToolMode:
+ runOptions.ToolConstraint = ToolConstraint.Auto;
+ break;
+
+ case RequiredChatToolMode required when required.RequiredFunctionName is { } functionName:
+ runOptions.ToolConstraint = new ToolConstraint(ToolDefinition.CreateFunction(functionName));
+ break;
+
+ case RequiredChatToolMode required:
+ runOptions.ToolConstraint = ToolConstraint.Required;
+ break;
+ }
+ }
+
+ // Store the response format, if relevant.
+ if (runOptions.ResponseFormat is null)
+ {
+ switch (options.ResponseFormat)
+ {
+ case ChatResponseFormatText:
+ runOptions.ResponseFormat = AssistantResponseFormat.CreateTextFormat();
+ break;
+
+ case ChatResponseFormatJson jsonFormat when OpenAIClientExtensions2.StrictSchemaTransformCache.GetOrCreateTransformedSchema(jsonFormat) is { } jsonSchema:
+ runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonSchemaFormat(
+ jsonFormat.SchemaName,
+ BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(jsonSchema, OpenAIJsonContext.Default.JsonElement)),
+ jsonFormat.SchemaDescription,
+ OpenAIClientExtensions2.HasStrict(options.AdditionalProperties));
+ break;
+
+ case ChatResponseFormatJson jsonFormat:
+ runOptions.ResponseFormat = AssistantResponseFormat.CreateJsonObjectFormat();
+ break;
+ }
+ }
+ }
+
+ // Configure system instructions.
+ StringBuilder? instructions = null;
+ void AppendSystemInstructions(string? toAppend)
+ {
+ if (!string.IsNullOrEmpty(toAppend))
+ {
+ if (instructions is null)
+ {
+ instructions = new(toAppend);
+ }
+ else
+ {
+ _ = instructions.AppendLine().AppendLine(toAppend);
+ }
+ }
+ }
+
+ AppendSystemInstructions(runOptions.AdditionalInstructions);
+ AppendSystemInstructions(options?.Instructions);
+
+ // Process ChatMessages.
+ List? functionResults = null;
+ foreach (var chatMessage in messages)
+ {
+ List messageContents = [];
+
+ // Assistants doesn't support system/developer messages directly. It does support transient per-request instructions,
+ // so we can use the system/developer messages to build up a set of instructions that will be passed to the assistant
+ // as part of this request. However, in doing so, on a subsequent request that information will be lost, as there's no
+ // way to store per-thread instructions in the OpenAI Assistants API. We don't want to convert these to user messages,
+ // however, as that would then expose the system/developer messages in a way that might make the model more likely
+ // to include that information in its responses. System messages should ideally be instead done as instructions to
+ // the assistant when the assistant is created.
+ if (chatMessage.Role == ChatRole.System ||
+ chatMessage.Role == OpenAIClientExtensions2.ChatRoleDeveloper)
+ {
+ foreach (var textContent in chatMessage.Contents.OfType())
+ {
+ AppendSystemInstructions(textContent.Text);
+ }
+
+ continue;
+ }
+
+ foreach (AIContent content in chatMessage.Contents)
+ {
+ switch (content)
+ {
+ case TextContent text:
+ messageContents.Add(MessageContent.FromText(text.Text));
+ break;
+
+ case UriContent image when image.HasTopLevelMediaType("image"):
+ messageContents.Add(MessageContent.FromImageUri(image.Uri));
+ break;
+
+ // Assistants doesn't support data URIs.
+ //case DataContent image when image.HasTopLevelMediaType("image"):
+ // messageContents.Add(MessageContent.FromImageUri(new Uri(image.Uri)));
+ // break;
+
+ case FunctionResultContent result:
+ (functionResults ??= []).Add(result);
+ break;
+
+ case AIContent when content.RawRepresentation is MessageContent rawRep:
+ messageContents.Add(rawRep);
+ break;
+ }
+ }
+
+ if (messageContents.Count > 0)
+ {
+ runOptions.AdditionalMessages.Add(new ThreadInitializationMessage(
+ chatMessage.Role == ChatRole.Assistant ? MessageRole.Assistant : MessageRole.User,
+ messageContents));
+ }
+ }
+
+ runOptions.AdditionalInstructions = 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 (var 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, OpenAIJsonContext.Default.StringArray);
+ }
+ catch
+ {
+ 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;
+ }
+}
+
+/// Provides extension methods for working with s.
+internal static class OpenAIClientExtensions2
+{
+ /// Key into AdditionalProperties used to store a strict option.
+ private const string StrictKey = "strictJsonSchema";
+
+ /// Gets the default OpenAI endpoint.
+ internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
+
+ /// Gets a for "developer".
+ internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer");
+
+ ///
+ /// Gets the JSON schema transformer cache conforming to OpenAI strict / structured output restrictions per
+ /// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas.
+ ///
+ internal static AIJsonSchemaTransformCache StrictSchemaTransformCache { get; } = new(new()
+ {
+ DisallowAdditionalProperties = true,
+ ConvertBooleanSchemas = true,
+ MoveDefaultKeywordToDescription = true,
+ RequireAllProperties = true,
+ TransformSchemaNode = (ctx, node) =>
+ {
+ // Move content from common but unsupported properties to description. In particular, we focus on properties that
+ // the AIJsonUtilities schema generator might produce and/or that are explicitly mentioned in the OpenAI documentation.
+
+ if (node is JsonObject schemaObj)
+ {
+ StringBuilder? additionalDescription = null;
+
+ ReadOnlySpan unsupportedProperties =
+ [
+ // Produced by AIJsonUtilities but not in allow list at https://platform.openai.com/docs/guides/structured-outputs#supported-properties:
+ "contentEncoding", "contentMediaType", "not",
+
+ // Explicitly mentioned at https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#key-ordering as being unsupported with some models:
+ "minLength", "maxLength", "pattern", "format",
+ "minimum", "maximum", "multipleOf",
+ "patternProperties",
+ "minItems", "maxItems",
+
+ // Explicitly mentioned at https://learn.microsoft.com/azure/ai-services/openai/how-to/structured-outputs?pivots=programming-language-csharp&tabs=python-secure%2Cdotnet-entra-id#unsupported-type-specific-keywords
+ // as being unsupported with Azure OpenAI:
+ "unevaluatedProperties", "propertyNames", "minProperties", "maxProperties",
+ "unevaluatedItems", "contains", "minContains", "maxContains", "uniqueItems",
+ ];
+
+ foreach (string propName in unsupportedProperties)
+ {
+ if (schemaObj[propName] is { } propNode)
+ {
+ _ = schemaObj.Remove(propName);
+ AppendLine(ref additionalDescription, propName, propNode);
+ }
+ }
+
+ if (additionalDescription is not null)
+ {
+ schemaObj["description"] = schemaObj["description"] is { } descriptionNode && descriptionNode.GetValueKind() == JsonValueKind.String ?
+ $"{descriptionNode.GetValue()}{Environment.NewLine}{additionalDescription}" :
+ additionalDescription.ToString();
+ }
+
+ return node;
+
+ static void AppendLine(ref StringBuilder? sb, string propName, JsonNode propNode)
+ {
+ sb ??= new();
+
+ if (sb.Length > 0)
+ {
+ _ = sb.AppendLine();
+ }
+
+ _ = sb.Append(propName).Append(": ").Append(propNode);
+ }
+ }
+
+ return node;
+ },
+ });
+
+ // TODO: Once we're ready to rely on C# 14 features, add an extension property ChatOptions.Strict.
+
+ /// Gets whether the properties specify that strict schema handling is desired.
+ internal static bool? HasStrict(IReadOnlyDictionary? additionalProperties) =>
+ additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true &&
+ strictObj is bool strictValue ?
+ strictValue : null;
+
+ /// Extracts from an the parameters and strictness setting for use with OpenAI's APIs.
+ internal static BinaryData ToOpenAIFunctionParameters(AIFunction aiFunction, bool? strict)
+ {
+ // Perform any desirable transformations on the function's JSON schema, if it'll be used in a strict setting.
+ JsonElement jsonSchema = strict is true ?
+ StrictSchemaTransformCache.GetOrCreateTransformedSchema(aiFunction) :
+ aiFunction.JsonSchema;
+
+ // Roundtrip the schema through the ToolJson model type to remove extra properties
+ // and force missing ones into existence, then return the serialized UTF8 bytes as BinaryData.
+ var tool = JsonSerializer.Deserialize(jsonSchema, OpenAIJsonContext.Default.ToolJson)!;
+ var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson));
+
+ return functionParameters;
+ }
+
+ /// Used to create the JSON payload for an OpenAI tool description.
+ internal sealed class ToolJson
+ {
+ [JsonPropertyName("type")]
+ public string Type { get; set; } = "object";
+
+ [JsonPropertyName("required")]
+ public HashSet Required { get; set; } = [];
+
+ [JsonPropertyName("properties")]
+ public Dictionary Properties { get; set; } = [];
+
+ [JsonPropertyName("additionalProperties")]
+ public bool AdditionalProperties { get; set; }
+ }
+}
+
+/// Source-generated JSON type information for use by all OpenAI implementations.
+[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
+ UseStringEnumConverter = true,
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
+ WriteIndented = true)]
+[JsonSerializable(typeof(OpenAIClientExtensions2.ToolJson))]
+[JsonSerializable(typeof(IDictionary))]
+[JsonSerializable(typeof(string[]))]
+[JsonSerializable(typeof(JsonElement))]
+internal sealed partial class OpenAIJsonContext : JsonSerializerContext;
diff --git a/dotnet/samples/GettingStarted/External/README.md b/dotnet/samples/GettingStarted/External/README.md
new file mode 100644
index 0000000000..7e1ab9d811
--- /dev/null
+++ b/dotnet/samples/GettingStarted/External/README.md
@@ -0,0 +1,3 @@
+# Proposed External references
+
+This directory contains proposed external references for the Agent Framework that are not yet available and may be considered and added in the future.
\ No newline at end of file
diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj
index 5e096b6b48..3389787893 100644
--- a/dotnet/samples/GettingStarted/GettingStarted.csproj
+++ b/dotnet/samples/GettingStarted/GettingStarted.csproj
@@ -36,13 +36,15 @@
+
+ true
+
+
+
Always
-
- Always
-
diff --git a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs
index d454ab1a09..231f9a2d7a 100644
--- a/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs
+++ b/dotnet/samples/GettingStarted/Providers/ChatClientAgent_With_OpenAIResponseChatCompletion.cs
@@ -24,6 +24,7 @@ public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutp
public async Task RunWithChatCompletion(bool useConversationIdThread)
{
// Get the chat client to use for the agent.
+#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
using var chatClient = new OpenAIClient(TestConfiguration.OpenAI.ApiKey)
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
.AsIChatClient();
@@ -39,6 +40,7 @@ public sealed class ChatClientAgent_With_OpenAIResponsesChatCompletion(ITestOutp
RawRepresentationFactory = (_) => new ResponseCreationOptions() { StoredOutputEnabled = useConversationIdThread }
}
});
+#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
// Start a new thread for the agent conversation based on the type.
AgentThread thread = agent.GetNewThread();
diff --git a/dotnet/samples/GettingStarted/Tools/Files/groceries.txt b/dotnet/samples/GettingStarted/Resources/groceries.txt
similarity index 100%
rename from dotnet/samples/GettingStarted/Tools/Files/groceries.txt
rename to dotnet/samples/GettingStarted/Resources/groceries.txt
diff --git a/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs b/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs
similarity index 94%
rename from dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs
rename to dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs
index 603476503e..c54b0bf07a 100644
--- a/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingTools.cs
+++ b/dotnet/samples/GettingStarted/Steps/Step02_ChatClientAgent_UsingFunctionTools.cs
@@ -6,7 +6,11 @@ using Microsoft.Extensions.AI.Agents;
namespace Steps;
-public sealed class Step02_ChatClientAgent_UsingTools(ITestOutputHelper output) : AgentSample(output)
+///
+/// This sample demonstrates how to use a with function tools.
+/// It includes examples of both streaming and non-streaming agent interactions.
+///
+public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper output) : AgentSample(output)
{
[Theory]
[InlineData(ChatClientProviders.AzureOpenAI)]
diff --git a/dotnet/samples/GettingStarted/Steps/Step03_ChatClientAgent_UsingCodeInterpreterTools.cs b/dotnet/samples/GettingStarted/Steps/Step03_ChatClientAgent_UsingCodeInterpreterTools.cs
new file mode 100644
index 0000000000..d87219d53b
--- /dev/null
+++ b/dotnet/samples/GettingStarted/Steps/Step03_ChatClientAgent_UsingCodeInterpreterTools.cs
@@ -0,0 +1,126 @@
+// Copyright (c) Microsoft. All rights reserved.
+
+using System.Text;
+using Azure.AI.Agents.Persistent;
+using Azure.Identity;
+using Microsoft.Extensions.AI.Agents;
+using Microsoft.Shared.Samples;
+using OpenAI.Assistants;
+using OpenAI.Files;
+
+namespace Steps;
+
+///
+/// Demonstrates how to use with code interpreter tools and file references.
+/// Shows uploading files to different providers and using them with code interpreter capabilities to analyze data and generate responses.
+///
+public sealed class Step03_ChatClientAgent_UsingCodeInterpreterTools(ITestOutputHelper output) : AgentSample(output)
+{
+ [Theory]
+ [InlineData(ChatClientProviders.OpenAIAssistant)]
+ [InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
+ public async Task RunningWithFileReferenceAsync(ChatClientProviders provider)
+ {
+ var codeInterpreterTool = new NewHostedCodeInterpreterTool();
+ codeInterpreterTool.FileIds.Add(await UploadFileAsync("Resources/groceries.txt", provider));
+
+ var agentOptions = new ChatClientAgentOptions
+ {
+ Name = "HelpfulAssistant",
+ Instructions = "You are a helpful assistant.",
+ ChatOptions = new() { Tools = [codeInterpreterTool] }
+ };
+
+ using var chatClient = await base.GetChatClientAsync(provider, agentOptions);
+
+ ChatClientAgent agent = new(chatClient, agentOptions);
+
+ var thread = agent.GetNewThread();
+
+ // Prompt which allows to verify that the data was processed from file correctly and current datetime is returned.
+ const string Prompt = "Calculate the total number of items, identify the most frequently purchased item and return the result with today's datetime.";
+
+ var assistantOutput = new StringBuilder();
+ var codeInterpreterOutput = new StringBuilder();
+
+ await foreach (var update in agent.RunStreamingAsync(Prompt, thread))
+ {
+ if (!string.IsNullOrWhiteSpace(update.Text))
+ {
+ assistantOutput.Append(update.Text);
+ }
+
+ if (update.RawRepresentation is not null)
+ {
+ codeInterpreterOutput.Append(GetCodeInterpreterOutput(update.RawRepresentation, provider));
+ }
+ }
+
+ Console.WriteLine("Assistant Output:");
+ Console.WriteLine(assistantOutput.ToString());
+
+ Console.WriteLine("Code interpreter Output:");
+ Console.WriteLine(codeInterpreterOutput.ToString());
+ }
+
+ #region private
+
+ ///
+ /// Uploads a file to the specified chat client provider and returns the file ID.
+ ///
+ /// Path to the file to be uploaded.
+ /// The chat client provider to use for uploading the file.
+ /// The ID of the uploaded file.
+ ///
+ private async Task UploadFileAsync(string filePath, ChatClientProviders provider)
+ {
+ switch (provider)
+ {
+ case ChatClientProviders.OpenAIAssistant:
+ var fileClient = new OpenAIFileClient(TestConfiguration.OpenAI.ApiKey);
+ OpenAIFile openAIFileInfo = await fileClient.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
+
+ return openAIFileInfo.Id;
+ case ChatClientProviders.AzureAIAgentsPersistent:
+ var persistentAgentsClient = new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential());
+ PersistentAgentFileInfo persistentAgentFileInfo = await persistentAgentsClient.Files.UploadFileAsync(filePath, PersistentAgentFilePurpose.Agents);
+
+ return persistentAgentFileInfo.Id;
+
+ default:
+ throw new NotSupportedException($"Client provider {provider} is not supported.");
+ }
+ }
+
+ ///
+ /// Depending on the provider, different strategies are used to extract the code interpreter output from the response raw representation.
+ ///
+ /// Raw representation of the response containing code interpreter output.
+ /// Provider of the chat client that is used to determine how to extract the output.
+ /// The code interpreter output as a string.
+ private static string? GetCodeInterpreterOutput(object rawRepresentation, ChatClientProviders provider)
+ {
+#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+ switch (provider)
+ {
+ case ChatClientProviders.OpenAIAssistant
+ when rawRepresentation is OpenAI.Assistants.RunStepDetailsUpdate stepDetails:
+ return $"{stepDetails.CodeInterpreterInput}{string.Join(
+ string.Empty,
+ stepDetails.CodeInterpreterOutputs.SelectMany(l => l.Logs)
+ )}";
+#pragma warning restore OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
+ case ChatClientProviders.AzureAIAgentsPersistent
+ when rawRepresentation is Azure.AI.Agents.Persistent.RunStepDetailsUpdate stepDetails:
+ return $"{stepDetails.CodeInterpreterInput}{string.Join(
+ string.Empty,
+ stepDetails.CodeInterpreterOutputs.OfType().SelectMany(l => l.Logs)
+ )}";
+ }
+
+ return null;
+ }
+
+ #endregion
+}
diff --git a/dotnet/samples/GettingStarted/Tools/Abstractions/NewHostedCodeInterpreterTool.cs b/dotnet/samples/GettingStarted/Tools/Abstractions/NewHostedCodeInterpreterTool.cs
deleted file mode 100644
index 8f7ec5dedc..0000000000
--- a/dotnet/samples/GettingStarted/Tools/Abstractions/NewHostedCodeInterpreterTool.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Microsoft.Extensions.AI;
-
-namespace GettingStarted.Tools.Abstractions;
-
-///
-/// Proposal for abstraction updates based on the common code interpreter tool properties.
-/// Based on the decision, the abstraction can be updated in M.E.AI or specific SDK if some properties are not common.
-///
-public class NewHostedCodeInterpreterTool : HostedCodeInterpreterTool
-{
- public IList? FileIds { get; set; }
-}
diff --git a/dotnet/samples/GettingStarted/Tools/CodeInterpreterTools.cs b/dotnet/samples/GettingStarted/Tools/CodeInterpreterTools.cs
deleted file mode 100644
index a3444a48cd..0000000000
--- a/dotnet/samples/GettingStarted/Tools/CodeInterpreterTools.cs
+++ /dev/null
@@ -1,140 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.Text;
-using Azure.AI.Agents.Persistent;
-using GettingStarted.Tools.Abstractions;
-using GettingStarted.Tools.Extensions;
-using Microsoft.Extensions.AI;
-using Microsoft.Extensions.AI.Agents;
-using OpenAI;
-using OpenAI.Files;
-
-#pragma warning disable OPENAI001
-
-namespace GettingStarted.Tools;
-
-public sealed class CodeInterpreterTools(ITestOutputHelper output) : AgentSample(output)
-{
- [Theory]
- [InlineData(ChatClientProviders.OpenAIAssistant)]
- [InlineData(ChatClientProviders.AzureAIAgentsPersistent)]
- public async Task RunningWithFileReferenceAsync(ChatClientProviders provider)
- {
- var fileId = await UploadTestFileAsync(provider);
-
- var chatOptions = new ChatOptions()
- {
- Tools = [new NewHostedCodeInterpreterTool { FileIds = [fileId] }]
- };
-
- var agentOptions = new ChatClientAgentOptions
- {
- Name = "HelpfulAssistant",
- Instructions = "You are a helpful assistant.",
- // Transformation is required until the abstraction will be added to either SDK provider or M.E.AI and
- // implementations will handle new properties/classes.
- ChatOptions = TransformChatOptions(chatOptions, provider)
- };
-
- using var chatClient = await base.GetChatClientAsync(provider, agentOptions);
-
- ChatClientAgent agent = new(chatClient, agentOptions);
-
- var thread = agent.GetNewThread();
-
- // Prompt which allows to verify that the data was processed from file correctly and current datetime is returned.
- const string Prompt = "Calculate the total number of items, identify the most frequently puchased item and return the result with today's datetime.";
-
- var assistantOutput = new StringBuilder();
- var codeInterpreterOutput = new StringBuilder();
-
- await foreach (var update in agent.RunStreamingAsync(Prompt, thread))
- {
- if (!string.IsNullOrWhiteSpace(update.Text))
- {
- assistantOutput.Append(update.Text);
- }
- else if (update.RawRepresentation is not null)
- {
- ProcessRawRepresentationOutput(update.RawRepresentation, codeInterpreterOutput, provider);
- }
- }
-
- Console.WriteLine("Assistant Output:");
- Console.WriteLine(assistantOutput.ToString());
-
- Console.WriteLine("Code interpreter Output:");
- Console.WriteLine(codeInterpreterOutput.ToString());
- }
-
- #region private
-
- ///
- /// This method creates a raw representation of tools from newly proposed abstractions, so underlying SDKs can work with it.
- /// Once the tool abstraction is added to either SDK provider or M.E.AI, this method can be removed.
- /// The logic under each provider case should go to related SDK.
- ///
- private static ChatOptions TransformChatOptions(ChatOptions chatOptions, ChatClientProviders provider)
- {
- return provider switch
- {
- ChatClientProviders.OpenAIAssistant => chatOptions.ToOpenAIAssistantChatOptions(),
- ChatClientProviders.AzureAIAgentsPersistent => chatOptions.ToAzureAIPersistentAgentChatOptions(),
- _ => chatOptions
- };
- }
-
- private Task UploadTestFileAsync(ChatClientProviders provider)
- {
- var filePath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Tools", "Files", "groceries.txt"));
- return UploadFileAsync(filePath, provider);
- }
-
- private async Task UploadFileAsync(string filePath, ChatClientProviders provider)
- {
- switch (provider)
- {
- case ChatClientProviders.OpenAIAssistant:
- var fileClient = GetOpenAIFileClient();
- OpenAIFile openAIFileInfo = await fileClient.UploadFileAsync(filePath, FileUploadPurpose.Assistants);
-
- return openAIFileInfo.Id;
- case ChatClientProviders.AzureAIAgentsPersistent:
- PersistentAgentFileInfo persistentAgentFileInfo = await AzureAIPersistentAgentsClient.Files.UploadFileAsync(filePath, PersistentAgentFilePurpose.Agents);
-
- return persistentAgentFileInfo.Id;
-
- default:
- throw new NotSupportedException($"Client provider {provider} is not supported.");
- }
- }
-
- private static void ProcessRawRepresentationOutput(object rawRepresentation, StringBuilder builder, ChatClientProviders provider)
- {
- switch (provider)
- {
- case ChatClientProviders.OpenAIAssistant:
- if (rawRepresentation is OpenAI.Assistants.RunStepDetailsUpdate openAIStepDetailsUpdate)
- {
- builder.Append(openAIStepDetailsUpdate.CodeInterpreterInput);
- builder.Append(string.Join(string.Empty, openAIStepDetailsUpdate.CodeInterpreterOutputs.SelectMany(l => l.Logs)));
- }
-
- break;
- case ChatClientProviders.AzureAIAgentsPersistent:
- if (rawRepresentation is Azure.AI.Agents.Persistent.RunStepDetailsUpdate persistentAgentStepDetailsUpdate)
- {
- builder.Append(persistentAgentStepDetailsUpdate.CodeInterpreterInput);
- builder.Append(string.Join(string.Empty, persistentAgentStepDetailsUpdate
- .CodeInterpreterOutputs
- .OfType().SelectMany(l => l.Logs)));
- }
-
- break;
- }
- }
-
- private OpenAIFileClient GetOpenAIFileClient() => OpenAIClient.GetOpenAIFileClient();
-
- #endregion
-}
diff --git a/dotnet/samples/GettingStarted/Tools/Extensions/AzureAIPersistentAgentChatOptionsExtensions.cs b/dotnet/samples/GettingStarted/Tools/Extensions/AzureAIPersistentAgentChatOptionsExtensions.cs
deleted file mode 100644
index 1398403935..0000000000
--- a/dotnet/samples/GettingStarted/Tools/Extensions/AzureAIPersistentAgentChatOptionsExtensions.cs
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using Azure.AI.Agents.Persistent;
-using GettingStarted.Tools.Abstractions;
-using Microsoft.Extensions.AI;
-
-namespace GettingStarted.Tools.Extensions;
-
-///
-/// conversion for Azure AI Persistent Agent.
-/// When abstraction is in place, this logic should go to Azure AI Persistent Agents SDK.
-///
-internal static class AzureAIPersistentAgentChatOptionsExtensions
-{
- public static ChatOptions ToAzureAIPersistentAgentChatOptions(this ChatOptions chatOptions)
- {
- var fileIds = new List();
-
- foreach (var tool in chatOptions.Tools!)
- {
- if (tool is NewHostedCodeInterpreterTool codeInterpreterTool &&
- codeInterpreterTool.FileIds is { Count: > 0 })
- {
- fileIds.AddRange(codeInterpreterTool.FileIds);
- }
- }
-
- if (fileIds.Count > 0)
- {
- var toolResources = new Azure.AI.Agents.Persistent.ToolResources()
- {
- CodeInterpreter = new Azure.AI.Agents.Persistent.CodeInterpreterToolResource()
- };
-
- foreach (var fileId in fileIds)
- {
- toolResources.CodeInterpreter.FileIds.Add(fileId);
- }
-
- var threadAndRunOptions = new ThreadAndRunOptions { ToolResources = toolResources };
-
- chatOptions.RawRepresentationFactory = (_) => threadAndRunOptions;
- }
-
- return chatOptions;
- }
-}
diff --git a/dotnet/samples/GettingStarted/Tools/Extensions/OpenAIAssistantChatOptionsExtensions.cs b/dotnet/samples/GettingStarted/Tools/Extensions/OpenAIAssistantChatOptionsExtensions.cs
deleted file mode 100644
index bdba77517d..0000000000
--- a/dotnet/samples/GettingStarted/Tools/Extensions/OpenAIAssistantChatOptionsExtensions.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using GettingStarted.Tools.Abstractions;
-using Microsoft.Extensions.AI;
-using OpenAI.Assistants;
-
-#pragma warning disable OPENAI001
-
-namespace GettingStarted.Tools.Extensions;
-
-///
-/// conversion for OpenAI Assistants.
-/// When abstraction is in place, this logic should go to OpenAI Assistants SDK.
-///
-internal static class OpenAIAssistantChatOptionsExtensions
-{
- public static ChatOptions ToOpenAIAssistantChatOptions(this ChatOptions chatOptions)
- {
- // File references can be added on message attachment level only and not on code interpreter tool definition level.
- // Message attachment content should be non-empty.
- var threadInitializationMessage = new ThreadInitializationMessage(MessageRole.User, [MessageContent.FromText("attachments")]);
- var toolDefinitions = new List();
-
- foreach (var tool in chatOptions.Tools!)
- {
- if (tool is NewHostedCodeInterpreterTool codeInterpreterTool)
- {
- var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
- toolDefinitions.Add(codeInterpreterToolDefinition);
-
- if (codeInterpreterTool.FileIds is { Count: > 0 })
- {
- foreach (var fileId in codeInterpreterTool.FileIds)
- {
- threadInitializationMessage.Attachments.Add(new(fileId, [codeInterpreterToolDefinition]));
- }
- }
- }
- }
-
- var runCreationOptions = new RunCreationOptions();
-
- runCreationOptions.AdditionalMessages.Add(threadInitializationMessage);
-
- chatOptions.RawRepresentationFactory = (_) => runCreationOptions;
-
- return chatOptions;
- }
-}
diff --git a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs
index 21c383071a..fad92f7696 100644
--- a/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs
+++ b/dotnet/src/Microsoft.Extensions.AI.Agents.AzureAI/PersistentAgentsChatClient.cs
@@ -251,6 +251,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
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
@@ -287,6 +288,15 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
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:
@@ -299,6 +309,11 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
{
runOptions.OverrideTools = toolDefinitions;
}
+
+ if (toolResources is not null)
+ {
+ runOptions.ToolResources = toolResources;
+ }
}
// Store the tool mode, if relevant.
diff --git a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
index 0bdfd9091a..bce26d9e21 100644
--- a/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
+++ b/dotnet/tests/OpenAIResponse.IntegrationTests/OpenAIResponseFixture.cs
@@ -12,6 +12,8 @@ using OpenAI;
using OpenAI.Responses;
using Shared.IntegrationTests;
+#pragma warning disable OPENAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
+
namespace OpenAIResponse.IntegrationTests;
public class OpenAIResponseFixture(bool store) : IChatClientAgentFixture