.Net: Code interpreter tool abstraction and implementation examples (#110)

* Added code interpreter abstraction updates for OpenAI Assistants

* Updated Persistent Agents implementation based on latest changes in SDK

* Added code interpreter abstraction updates for Azure AI Persistent Agents

* Small note for OpenAI responses code interpreter

* Small update

* Fixes after merge

* Addressed PR feedback

* Small update

* Small fix

* Fix after merge
This commit is contained in:
Dmytro Struk
2025-07-01 14:59:51 +00:00
committed by GitHub
parent 95738b0ac2
commit fbb0fdfe0d
10 changed files with 339 additions and 33 deletions
@@ -30,10 +30,13 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
private readonly string _agentId;
/// <summary>The thread ID to use if none is supplied in <see cref="ChatOptions.ConversationId"/>.</summary>
private readonly string? _threadId;
private readonly string? _defaultThreadId;
/// <summary>List of tools associated with the agent.</summary>
private IReadOnlyList<ToolDefinition>? _agentTools;
/// <summary>Initializes a new instance of the <see cref="PersistentAgentsChatClient"/> class for the specified <see cref="PersistentAgentsClient"/>.</summary>
public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? threadId)
public PersistentAgentsChatClient(PersistentAgentsClient client, string agentId, string? defaultThreadId = null)
{
if (client is null)
{
@@ -47,7 +50,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
this._client = client;
this._agentId = agentId;
this._threadId = threadId;
this._defaultThreadId = defaultThreadId;
this._metadata = new(ProviderName);
}
@@ -76,10 +79,11 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
}
// Extract necessary state from messages and options.
(ThreadAndRunOptions runOptions, List<FunctionResultContent>? toolResults) = this.CreateRunOptions(messages, options);
(ThreadAndRunOptions runOptions, List<FunctionResultContent>? toolResults) =
await this.CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false);
// Get the thread ID.
string? threadId = options?.ConversationId ?? this._threadId;
string? threadId = options?.ConversationId ?? this._defaultThreadId;
if (threadId is null && toolResults is not null)
{
throw new ArgumentException("No thread ID was provided, but chat messages includes tool results.", nameof(messages));
@@ -89,7 +93,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
ThreadRun? threadRun = null;
if (threadId is not null)
{
await foreach (var run in this._client.Runs.GetRunsAsync(threadId, limit: 1, ListSortOrder.Descending, cancellationToken: cancellationToken).ConfigureAwait(false))
await foreach (ThreadRun? run in this._client.Runs.GetRunsAsync(threadId, limit: 1, ListSortOrder.Descending, cancellationToken: cancellationToken).ConfigureAwait(false))
{
if (run.Status != RunStatus.Completed && run.Status != RunStatus.Cancelled && run.Status != RunStatus.Failed && run.Status != RunStatus.Expired)
{
@@ -149,7 +153,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
// Process each update.
string? responseId = null;
await foreach (var update in updates.ConfigureAwait(false))
await foreach (StreamingUpdate? update in updates.ConfigureAwait(false))
{
switch (update)
{
@@ -226,8 +230,8 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
/// Creates the <see cref="ThreadAndRunOptions"/> to use for the request and extracts any function result contents
/// that need to be submitted as tool results.
/// </summary>
private (ThreadAndRunOptions RunOptions, List<FunctionResultContent>? ToolResults) CreateRunOptions(
IEnumerable<ChatMessage> messages, ChatOptions? options)
private async ValueTask<(ThreadAndRunOptions 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.
ThreadAndRunOptions runOptions =
@@ -244,12 +248,32 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
runOptions.ParallelToolCalls ??= options.AllowMultipleToolCalls;
// Ignored: options.TopK, options.FrequencyPenalty, options.Seed, options.StopSequences
// TODO: When moved to Azure.AI.Agents.Persistent, merge agent tools with override tools, in similar way like here:
// https://github.com/dotnet/extensions/blob/694b95ef75c6bd9de00ef761dadae4e70ee8739f/src/Libraries/Microsoft.Extensions.AI.OpenAI/OpenAIAssistantChatClient.cs#L263-L279
if (options.Tools is { Count: > 0 } tools)
{
// The caller can provide tools in the supplied ThreadAndRunOptions. Augment it with any supplied via ChatOptions.Tools.
IList<ToolDefinition> toolDefinitions = runOptions.OverrideTools is not null ? [.. runOptions.OverrideTools] : [];
List<ToolDefinition> toolDefinitions = [];
// If the caller has provided any tool overrides, we'll assume they don't want to use the agent's tools.
// But if they haven't, the only way we can provide our tools is via an override, whereas we'd really like to
// just add them. To handle that, we'll get all of the agent's tools and add them to the override list
// along with our tools.
if (runOptions.OverrideTools is null || !runOptions.OverrideTools.Any())
{
if (this._agentTools is null)
{
PersistentAgent agent = await this._client.Administration.GetAgentAsync(this._agentId, cancellationToken).ConfigureAwait(false);
this._agentTools = agent.Tools;
}
toolDefinitions.AddRange(this._agentTools);
}
// The caller can provide tools in the supplied ThreadAndRunOptions.
if (runOptions.OverrideTools is not null)
{
toolDefinitions.AddRange(runOptions.OverrideTools);
}
// Now add the tools from ChatOptions.Tools.
foreach (AITool tool in tools)
{
switch (tool)
@@ -318,7 +342,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
runOptions.ThreadOptions ??= new();
foreach (var chatMessage in messages)
foreach (ChatMessage chatMessage in messages)
{
List<MessageInputContentBlock> messageContents = [];
@@ -326,7 +350,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
chatMessage.Role == new ChatRole("developer"))
{
instructions ??= new();
foreach (var textContent in chatMessage.Contents.OfType<TextContent>())
foreach (TextContent textContent in chatMessage.Contents.OfType<TextContent>())
{
_ = instructions.Append(textContent);
}
@@ -347,7 +371,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
break;
case UriContent image when image.HasTopLevelMediaType("image"):
messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri.ToString())));
messageContents.Add(new MessageInputImageUriBlock(new MessageImageUriParam(image.Uri.AbsoluteUri)));
break;
case FunctionResultContent result:
@@ -379,7 +403,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
return (runOptions, functionResults);
}
/// <summary>Convert <see cref="FunctionResultContent"/> instances to <see cref="ToolOutput"/> instances."/></summary>
/// <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>
@@ -389,7 +413,7 @@ public sealed partial class PersistentAgentsChatClient : IChatClient
toolOutputs = null;
if (toolResults?.Count > 0)
{
foreach (var frc in toolResults)
foreach (FunctionResultContent frc in toolResults)
{
// When creating the FunctionCallContext, we created it with a CallId == [runId, callId].
// We need to extract the run ID and ensure that the ToolOutput we send back to Azure