mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.Net: Update Code Interpreter Sample as a Step. (#148)
* Updating code interpreter samples * Small adjustments to ensure it works as expected * Update dotnet/samples/GettingStarted/Steps/Step03_ChatClientAgent_UsingCodeInterpreterTools.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Address xmldoc * Addressing PR coment, external implementaions + ChatClients update, removing RawRepresentationFactory requirement * Address warnings * Update Fix warnings * Proposed changes for the OpenAIAssistantChatClient * Address PR comments --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
01fd74a80c
commit
fef4fd2c18
@@ -23,10 +23,10 @@
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.6.0-preview.1.25310.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.0-preview.1.25356.2" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="8.0.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
|
||||
@@ -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());
|
||||
|
||||
/// <summary>
|
||||
/// 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<IChatClient> GetOpenAIChatClientAsync()
|
||||
=> Task.FromResult(
|
||||
OpenAIClient
|
||||
.GetChatClient(TestConfiguration.OpenAI.ChatModelId)
|
||||
new ChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
|
||||
.AsIChatClient());
|
||||
|
||||
private Task<IChatClient> GetAzureOpenAIChatClientAsync()
|
||||
@@ -98,39 +93,52 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
|
||||
|
||||
private Task<IChatClient> GetOpenAIResponsesClientAsync()
|
||||
=> Task.FromResult(
|
||||
OpenAIClient
|
||||
.GetOpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId)
|
||||
new OpenAIResponseClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
|
||||
.AsIChatClient());
|
||||
|
||||
private async Task<IChatClient> 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<IChatClient> 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
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace OpenAI.Assistants;
|
||||
|
||||
/// <summary>
|
||||
/// Proposal for abstraction updates based on the common code interpreter tool properties.
|
||||
/// Based on the decision, the <see cref="HostedCodeInterpreterTool"/> abstraction can be updated in M.E.AI directly.
|
||||
/// </summary>
|
||||
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<string, object?> _additionalProperties = [];
|
||||
|
||||
/// <summary>Gets or sets the list of file IDs that the code interpreter tool can access.</summary>
|
||||
public IList<string> 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<string>();
|
||||
this._additionalProperties["fileIds"] = value;
|
||||
}
|
||||
|
||||
return (IList<string>)value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IReadOnlyDictionary<string, object?> AdditionalProperties => this._additionalProperties;
|
||||
}
|
||||
+644
@@ -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;
|
||||
|
||||
/// <summary>Represents an <see cref="IChatClient"/> for an Azure.AI.Agents.Persistent <see cref="AssistantClient"/>.</summary>
|
||||
public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
{
|
||||
/// <summary>The underlying <see cref="AssistantClient" />.</summary>
|
||||
#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;
|
||||
|
||||
/// <summary>Metadata for the client.</summary>
|
||||
private readonly ChatClientMetadata _metadata;
|
||||
|
||||
/// <summary>The ID of the agent to use.</summary>
|
||||
private readonly string _assistantId;
|
||||
|
||||
/// <summary>The thread ID to use if none is supplied in <see cref="ChatOptions.ConversationId"/>.</summary>
|
||||
private readonly string? _defaultThreadId;
|
||||
|
||||
/// <summary>List of tools associated with the assistant.</summary>
|
||||
private IReadOnlyList<ToolDefinition>? _assistantTools;
|
||||
|
||||
/// <summary>Initializes a new instance of the <see cref="OpenAIAssistantChatClient"/> class for the specified <see cref="AssistantClient"/>.</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ChatResponse> GetResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = null, CancellationToken cancellationToken = default) =>
|
||||
GetStreamingResponseAsync(messages, options, cancellationToken).ToChatResponseAsync(cancellationToken);
|
||||
|
||||
/// <inheritdoc />
|
||||
public async IAsyncEnumerable<ChatResponseUpdate> GetStreamingResponseAsync(
|
||||
IEnumerable<ChatMessage> messages, ChatOptions? options = null, [EnumeratorCancellation] CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
// Extract necessary state from messages and options.
|
||||
(RunCreationOptions runOptions, List<FunctionResultContent>? 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<StreamingUpdate> updates;
|
||||
if (threadRun is not null &&
|
||||
ConvertFunctionResultsToToolOutput(toolResults, out List<ToolOutput>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
void IDisposable.Dispose()
|
||||
{
|
||||
// nop
|
||||
}
|
||||
|
||||
/// <summary>Converts an Extensions function to an OpenAI assistants function tool.</summary>
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates the <see cref="RunCreationOptions"/> to use for the request and extracts any function result contents
|
||||
/// that need to be submitted as tool results.
|
||||
/// </summary>
|
||||
private async ValueTask<(RunCreationOptions RunOptions, List<FunctionResultContent>? ToolResults)> CreateRunOptionsAsync(
|
||||
IEnumerable<ChatMessage> 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<string> 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<FunctionResultContent>? functionResults = null;
|
||||
foreach (var chatMessage in messages)
|
||||
{
|
||||
List<MessageContent> 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<TextContent>())
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Convert <see cref="FunctionResultContent"/> instances to <see cref="ToolOutput"/> instances.</summary>
|
||||
/// <param name="toolResults">The tool results to process.</param>
|
||||
/// <param name="toolOutputs">The generated list of tool outputs, if any could be created.</param>
|
||||
/// <returns>The run ID associated with the corresponding function call requests.</returns>
|
||||
private static string? ConvertFunctionResultsToToolOutput(List<FunctionResultContent>? toolResults, out List<ToolOutput>? 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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Provides extension methods for working with <see cref="OpenAIClient"/>s.</summary>
|
||||
internal static class OpenAIClientExtensions2
|
||||
{
|
||||
/// <summary>Key into AdditionalProperties used to store a strict option.</summary>
|
||||
private const string StrictKey = "strictJsonSchema";
|
||||
|
||||
/// <summary>Gets the default OpenAI endpoint.</summary>
|
||||
internal static Uri DefaultOpenAIEndpoint { get; } = new("https://api.openai.com/v1");
|
||||
|
||||
/// <summary>Gets a <see cref="ChatRole"/> for "developer".</summary>
|
||||
internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer");
|
||||
|
||||
/// <summary>
|
||||
/// Gets the JSON schema transformer cache conforming to OpenAI <b>strict</b> / structured output restrictions per
|
||||
/// https://platform.openai.com/docs/guides/structured-outputs?api-mode=responses#supported-schemas.
|
||||
/// </summary>
|
||||
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<string> 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<string>()}{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.
|
||||
|
||||
/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
|
||||
internal static bool? HasStrict(IReadOnlyDictionary<string, object?>? additionalProperties) =>
|
||||
additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true &&
|
||||
strictObj is bool strictValue ?
|
||||
strictValue : null;
|
||||
|
||||
/// <summary>Extracts from an <see cref="AIFunction"/> the parameters and strictness setting for use with OpenAI's APIs.</summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>Used to create the JSON payload for an OpenAI tool description.</summary>
|
||||
internal sealed class ToolJson
|
||||
{
|
||||
[JsonPropertyName("type")]
|
||||
public string Type { get; set; } = "object";
|
||||
|
||||
[JsonPropertyName("required")]
|
||||
public HashSet<string> Required { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("properties")]
|
||||
public Dictionary<string, JsonElement> Properties { get; set; } = [];
|
||||
|
||||
[JsonPropertyName("additionalProperties")]
|
||||
public bool AdditionalProperties { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Source-generated JSON type information for use by all OpenAI implementations.</summary>
|
||||
[JsonSourceGenerationOptions(JsonSerializerDefaults.Web,
|
||||
UseStringEnumConverter = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
WriteIndented = true)]
|
||||
[JsonSerializable(typeof(OpenAIClientExtensions2.ToolJson))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class OpenAIJsonContext : JsonSerializerContext;
|
||||
@@ -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.
|
||||
@@ -36,13 +36,15 @@
|
||||
<Using Include="Microsoft.Shared.SampleUtilities" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<InjectSharedThrow>true</InjectSharedThrow>
|
||||
</PropertyGroup>
|
||||
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Resources\*">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Tools\Files\groceries.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
+2
@@ -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();
|
||||
|
||||
+5
-1
@@ -6,7 +6,11 @@ using Microsoft.Extensions.AI.Agents;
|
||||
|
||||
namespace Steps;
|
||||
|
||||
public sealed class Step02_ChatClientAgent_UsingTools(ITestOutputHelper output) : AgentSample(output)
|
||||
/// <summary>
|
||||
/// This sample demonstrates how to use a <see cref="ChatClientAgent"/> with function tools.
|
||||
/// It includes examples of both streaming and non-streaming agent interactions.
|
||||
/// </summary>
|
||||
public sealed class Step02_ChatClientAgent_UsingFunctionTools(ITestOutputHelper output) : AgentSample(output)
|
||||
{
|
||||
[Theory]
|
||||
[InlineData(ChatClientProviders.AzureOpenAI)]
|
||||
+126
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// Demonstrates how to use <see cref="ChatClientAgent"/> 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.
|
||||
/// </summary>
|
||||
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
|
||||
|
||||
/// <summary>
|
||||
/// Uploads a file to the specified chat client provider and returns the file ID.
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file to be uploaded.</param>
|
||||
/// <param name="provider">The chat client provider to use for uploading the file.</param>
|
||||
/// <returns>The ID of the uploaded file.</returns>
|
||||
/// <exception cref="NotSupportedException"></exception>
|
||||
private async Task<string> 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.");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Depending on the provider, different strategies are used to extract the code interpreter output from the response raw representation.
|
||||
/// </summary>
|
||||
/// <param name="rawRepresentation">Raw representation of the response containing code interpreter output.</param>
|
||||
/// <param name="provider">Provider of the chat client that is used to determine how to extract the output.</param>
|
||||
/// <returns>The code interpreter output as a string.</returns>
|
||||
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<RunStepDeltaCodeInterpreterLogOutput>().SelectMany(l => l.Logs)
|
||||
)}";
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using Microsoft.Extensions.AI;
|
||||
|
||||
namespace GettingStarted.Tools.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// Proposal for abstraction updates based on the common code interpreter tool properties.
|
||||
/// Based on the decision, the <see cref="HostedCodeInterpreterTool"/> abstraction can be updated in M.E.AI or specific SDK if some properties are not common.
|
||||
/// </summary>
|
||||
public class NewHostedCodeInterpreterTool : HostedCodeInterpreterTool
|
||||
{
|
||||
public IList<string>? FileIds { get; set; }
|
||||
}
|
||||
@@ -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
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private static ChatOptions TransformChatOptions(ChatOptions chatOptions, ChatClientProviders provider)
|
||||
{
|
||||
return provider switch
|
||||
{
|
||||
ChatClientProviders.OpenAIAssistant => chatOptions.ToOpenAIAssistantChatOptions(),
|
||||
ChatClientProviders.AzureAIAgentsPersistent => chatOptions.ToAzureAIPersistentAgentChatOptions(),
|
||||
_ => chatOptions
|
||||
};
|
||||
}
|
||||
|
||||
private Task<string> UploadTestFileAsync(ChatClientProviders provider)
|
||||
{
|
||||
var filePath = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Tools", "Files", "groceries.txt"));
|
||||
return UploadFileAsync(filePath, provider);
|
||||
}
|
||||
|
||||
private async Task<string> 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<RunStepDeltaCodeInterpreterLogOutput>().SelectMany(l => l.Logs)));
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private OpenAIFileClient GetOpenAIFileClient() => OpenAIClient.GetOpenAIFileClient();
|
||||
|
||||
#endregion
|
||||
}
|
||||
-47
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ChatOptions"/> conversion for Azure AI Persistent Agent.
|
||||
/// When abstraction is in place, this logic should go to Azure AI Persistent Agents SDK.
|
||||
/// </summary>
|
||||
internal static class AzureAIPersistentAgentChatOptionsExtensions
|
||||
{
|
||||
public static ChatOptions ToAzureAIPersistentAgentChatOptions(this ChatOptions chatOptions)
|
||||
{
|
||||
var fileIds = new List<string>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
-49
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// <see cref="ChatOptions"/> conversion for OpenAI Assistants.
|
||||
/// When abstraction is in place, this logic should go to OpenAI Assistants SDK.
|
||||
/// </summary>
|
||||
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<ToolDefinition>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
|
||||
if (options.Tools is { Count: > 0 } tools)
|
||||
{
|
||||
List<ToolDefinition> 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<string> 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user