mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
.NET: Update MEAI 9.8.0 and accepted proposed abstractions (#427)
* Update MEAI 9.8.0 and accepted proposed abstractions
* Address manual change in samples
* Address warnings
* Revert "Address warnings"
This reverts commit 52a7d60129.
This commit is contained in:
committed by
GitHub
Unverified
parent
db253ccda0
commit
753f336a3a
@@ -17,9 +17,9 @@
|
||||
<PackageVersion Include="Azure.AI.OpenAI" Version="2.2.0-beta.5" />
|
||||
<PackageVersion Include="Azure.Identity" Version="1.14.2" />
|
||||
<PackageVersion Include="CommunityToolkit.Aspire.OllamaSharp" Version="9.6.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.7.1-preview.1.25365.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.7.0" />
|
||||
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.AzureAIInference" Version="9.8.0-preview.1.25412.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="9.8.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="9.3.1" />
|
||||
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.12.0" />
|
||||
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.12.0" />
|
||||
@@ -31,10 +31,10 @@
|
||||
<PackageVersion Include="Swashbuckle.AspNetCore.SwaggerUI" Version="9.0.3" />
|
||||
<!-- System.* -->
|
||||
<PackageVersion Include="System.Linq.Async" Version="6.0.3" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.7" />
|
||||
<PackageVersion Include="System.Net.ServerSentEvents" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Text.Json" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Diagnostics.DiagnosticSource" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Threading.Channels" Version="9.0.8" />
|
||||
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
|
||||
<!-- OpenTelemetry -->
|
||||
<PackageVersion Include="OpenTelemetry" Version="1.12.0" />
|
||||
@@ -43,24 +43,24 @@
|
||||
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.12.0" />
|
||||
<!-- Microsoft.Extensions.* -->
|
||||
<PackageVersion Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.7.1" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.7.1-preview.1.25365.4" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.7.1" />
|
||||
<PackageVersion Include="OpenAI" Version="2.2.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.7" />
|
||||
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.3.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.8.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.OpenAI" Version="9.8.0-preview.1.25412.6" />
|
||||
<PackageVersion Include="Microsoft.Extensions.AI.Abstractions" Version="9.8.0" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.Json" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.8" />
|
||||
<PackageVersion Include="Microsoft.Extensions.Logging.Testing" Version="9.0.8" />
|
||||
<!-- Vector Stores -->
|
||||
<PackageVersion Include="Microsoft.SemanticKernel.Connectors.InMemory" Version="1.61.0-preview" />
|
||||
<!-- Agent SDKs -->
|
||||
|
||||
@@ -116,7 +116,7 @@ public class AgentSample(ITestOutputHelper output) : BaseSample(output)
|
||||
=> new(new PersistentAgentsClient(TestConfiguration.AzureAI.Endpoint, new AzureCliCredential()), options.Id!);
|
||||
|
||||
private NewOpenAIAssistantChatClient GetOpenAIAssistantChatClient(ChatClientAgentOptions options)
|
||||
=> new(new(TestConfiguration.OpenAI.ApiKey), options.Id!, null);
|
||||
=> new(new AssistantClient(TestConfiguration.OpenAI.ApiKey), options.Id!);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
+1
-1
@@ -21,7 +21,7 @@ public sealed class Step03_ChatClientAgent_UsingCodeInterpreterTools(ITestOutput
|
||||
[InlineData(ChatClientProviders.OpenAIAssistant)]
|
||||
public async Task RunningWithFileReferenceAsync(ChatClientProviders provider)
|
||||
{
|
||||
var codeInterpreterTool = new NewHostedCodeInterpreterTool()
|
||||
var codeInterpreterTool = new HostedCodeInterpreterTool()
|
||||
{
|
||||
Inputs = [new HostedFileContent(await UploadFileAsync("Resources/groceries.txt", provider))]
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ public sealed class Step07_ChatClientAgent_UsingFileSearchTools(ITestOutputHelpe
|
||||
var vectorStoreId = await CreateVectorStoreAsync([fileId], provider);
|
||||
|
||||
// Create a file search tool that can access the vector store.
|
||||
var fileSearchTool = new NewHostedFileSearchTool()
|
||||
var fileSearchTool = new HostedFileSearchTool()
|
||||
{
|
||||
Inputs = [new HostedVectorStoreContent(vectorStoreId)],
|
||||
};
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a file that is hosted by the AI service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="DataContent"/> which contains the data for a file or blob, this class represents a file that is hosted
|
||||
/// by the AI service and referenced by an identifier. Such identifiers are specific to the provider.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("FileId = {FileId}")]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class HostedFileContent : AIContent
|
||||
{
|
||||
private string _fileId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HostedFileContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="fileId">The ID of the hosted file.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="fileId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="fileId"/> is empty or composed entirely of whitespace.</exception>
|
||||
public HostedFileContent(string fileId)
|
||||
{
|
||||
_fileId = Throw.IfNullOrWhitespace(fileId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the hosted file.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or composed entirely of whitespace.</exception>
|
||||
public string FileId
|
||||
{
|
||||
get => _fileId;
|
||||
set => _fileId = Throw.IfNullOrWhitespace(value);
|
||||
}
|
||||
}
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a vector store that is hosted by the AI service.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Unlike <see cref="DataContent"/> which contains the data for a file or blob, this class represents a vector store that is hosted
|
||||
/// by the AI service and referenced by an identifier. Such identifiers are specific to the provider.
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("VectorStoreId = {VectorStoreId}")]
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class HostedVectorStoreContent : AIContent
|
||||
{
|
||||
private string? _vectorStoreId;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="HostedVectorStoreContent"/> class.
|
||||
/// </summary>
|
||||
/// <param name="vectorStoreId">The ID of the hosted vector store.</param>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="vectorStoreId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="vectorStoreId"/> is empty or composed entirely of whitespace.</exception>
|
||||
public HostedVectorStoreContent(string vectorStoreId)
|
||||
{
|
||||
_vectorStoreId = Throw.IfNullOrWhitespace(vectorStoreId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the ID of the hosted vector store.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="value"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentException"><paramref name="value"/> is empty or composed entirely of whitespace.</exception>
|
||||
public string VectorStoreId
|
||||
{
|
||||
get => _vectorStoreId ?? string.Empty;
|
||||
set => _vectorStoreId = Throw.IfNullOrWhitespace(value);
|
||||
}
|
||||
}
|
||||
-22
@@ -1,22 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <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>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NewHostedCodeInterpreterTool : HostedCodeInterpreterTool
|
||||
{
|
||||
/// <summary>Gets or sets a collection of <see cref="AIContent"/> to be used as input to the code interpreter tool.</summary>
|
||||
/// <remarks>
|
||||
/// Services support different varied kinds of inputs. Most support the IDs of files that are hosted by the service,
|
||||
/// represented via <see cref="HostedFileContent"/>. Some also support binary data, represented via <see cref="DataContent"/>.
|
||||
/// Unsupported inputs will be ignored by the <see cref="IChatClient"/> to which the tool is passed.
|
||||
/// </remarks>
|
||||
public IList<AIContent>? Inputs { get; set; }
|
||||
}
|
||||
-24
@@ -1,24 +0,0 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
// Line removed as it is unnecessary due to ImplicitUsings being enabled.
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Extensions.AI;
|
||||
|
||||
/// <summary>
|
||||
/// Proposal for abstraction updates based on the common file search tool properties.
|
||||
/// This provides a standardized interface for file search functionality across providers.
|
||||
/// </summary>
|
||||
[ExcludeFromCodeCoverage]
|
||||
public class NewHostedFileSearchTool : AITool
|
||||
{
|
||||
/// <summary>Gets or sets a collection of <see cref="AIContent"/> to be used as input to the code interpreter tool.</summary>
|
||||
/// <remarks>
|
||||
/// Services support different varied kinds of inputs. Most support the IDs of vector stores that are hosted by the service,
|
||||
/// represented via <see cref="HostedVectorStoreContent"/>. Some also support binary data, represented via <see cref="DataContent"/>.
|
||||
/// Unsupported inputs will be ignored by the <see cref="IChatClient"/> to which the tool is passed.
|
||||
/// </remarks>
|
||||
public IList<AIContent>? Inputs { get; set; }
|
||||
}
|
||||
-1
@@ -30,5 +30,4 @@
|
||||
<ItemGroup>
|
||||
<InternalsVisibleTo Include="Microsoft.Extensions.AI.Agents.Abstractions.UnitTests" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -295,7 +295,7 @@ namespace Azure.AI.Agents.Persistent
|
||||
BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(aiFunction.JsonSchema, AgentsChatClientJsonContext.Default.JsonElement))));
|
||||
break;
|
||||
|
||||
case NewHostedCodeInterpreterTool codeTool:
|
||||
case HostedCodeInterpreterTool codeTool:
|
||||
toolDefinitions.Add(new CodeInterpreterToolDefinition());
|
||||
|
||||
if (codeTool.Inputs is { Count: > 0 })
|
||||
@@ -313,7 +313,7 @@ namespace Azure.AI.Agents.Persistent
|
||||
}
|
||||
break;
|
||||
|
||||
case NewHostedFileSearchTool fileSearchTool:
|
||||
case HostedFileSearchTool fileSearchTool:
|
||||
toolDefinitions.Add(new FileSearchToolDefinition());
|
||||
|
||||
if (fileSearchTool.Inputs is { Count: > 0 })
|
||||
|
||||
@@ -11,7 +11,6 @@ using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Shared.Diagnostics;
|
||||
using OpenAI;
|
||||
using OpenAI.Assistants;
|
||||
@@ -48,7 +47,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
/// <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>
|
||||
/// <summary>Initializes a new instance of the <see cref="NewOpenAIAssistantChatClient"/> class for the specified <see cref="AssistantClient"/>.</summary>
|
||||
public NewOpenAIAssistantChatClient(AssistantClient assistantClient, string assistantId, string? defaultThreadId = null)
|
||||
{
|
||||
_client = Throw.IfNull(assistantClient);
|
||||
@@ -96,7 +95,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
{
|
||||
switch (tool)
|
||||
{
|
||||
case NewHostedCodeInterpreterTool codeTool:
|
||||
case HostedCodeInterpreterTool codeTool:
|
||||
|
||||
if (codeTool.Inputs is { Count: > 0 })
|
||||
{
|
||||
@@ -115,7 +114,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
|
||||
break;
|
||||
|
||||
case NewHostedFileSearchTool fileSearchTool:
|
||||
case HostedFileSearchTool fileSearchTool:
|
||||
|
||||
// Handle file IDs for file search tool
|
||||
if (fileSearchTool.Inputs is { Count: > 0 })
|
||||
@@ -155,7 +154,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
_ = Throw.IfNull(messages);
|
||||
|
||||
// Extract necessary state from messages and options.
|
||||
(RunCreationOptions runOptions, List<FunctionResultContent>? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false);
|
||||
(RunCreationOptions runOptions, ToolResources? toolResources, List<FunctionResultContent>? toolResults) = await CreateRunOptionsAsync(messages, options, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Get the thread ID.
|
||||
string? threadId = options?.ConversationId ?? _defaultThreadId;
|
||||
@@ -202,7 +201,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
// No thread ID was provided, so create a new thread.
|
||||
ThreadCreationOptions threadCreationOptions = new()
|
||||
{
|
||||
ToolResources = CreateToolResources(options)
|
||||
ToolResources = toolResources,
|
||||
};
|
||||
|
||||
foreach (var message in runOptions.AdditionalMessages)
|
||||
@@ -268,18 +267,19 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
|
||||
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)!));
|
||||
var fcc = OpenAIClientExtensions2.ParseCallContent(
|
||||
rau.FunctionArguments,
|
||||
JsonSerializer.Serialize([ru.Value.Id, toolCallId], OpenAIJsonContext.Default.StringArray),
|
||||
functionName);
|
||||
fcc.RawRepresentation = ru;
|
||||
ruUpdate.Contents.Add(fcc);
|
||||
}
|
||||
|
||||
yield return ruUpdate;
|
||||
break;
|
||||
|
||||
case MessageContentUpdate mcu:
|
||||
yield return new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text)
|
||||
ChatResponseUpdate textUpdate = new(mcu.Role == MessageRole.User ? ChatRole.User : ChatRole.Assistant, mcu.Text)
|
||||
{
|
||||
AuthorName = _assistantId,
|
||||
ConversationId = threadId,
|
||||
@@ -287,10 +287,48 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
RawRepresentation = mcu,
|
||||
ResponseId = responseId,
|
||||
};
|
||||
|
||||
// Add any annotations from the text update. The OpenAI Assistants API does not support passing these back
|
||||
// into the model (MessageContent.FromXx does not support providing annotations), so they end up being one way and are dropped
|
||||
// on subsequent requests.
|
||||
if (mcu.TextAnnotation is { } tau)
|
||||
{
|
||||
string? fileId = null;
|
||||
string? toolName = null;
|
||||
if (!string.IsNullOrWhiteSpace(tau.InputFileId))
|
||||
{
|
||||
fileId = tau.InputFileId;
|
||||
toolName = "file_search";
|
||||
}
|
||||
else if (!string.IsNullOrWhiteSpace(tau.OutputFileId))
|
||||
{
|
||||
fileId = tau.OutputFileId;
|
||||
toolName = "code_interpreter";
|
||||
}
|
||||
|
||||
if (fileId is not null)
|
||||
{
|
||||
if (textUpdate.Contents.Count == 0)
|
||||
{
|
||||
// Create a empty chunk of text content to hold the annotation.
|
||||
textUpdate.Contents.Add(new TextContent(string.Empty));
|
||||
}
|
||||
|
||||
(((TextContent)textUpdate.Contents[0]).Annotations ??= []).Add(new CitationAnnotation
|
||||
{
|
||||
RawRepresentation = tau,
|
||||
AnnotatedRegions = [new TextSpanAnnotatedRegion { StartIndex = tau.StartIndex, EndIndex = tau.EndIndex }],
|
||||
FileId = fileId,
|
||||
ToolName = toolName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
yield return textUpdate;
|
||||
break;
|
||||
|
||||
default:
|
||||
yield return new ChatResponseUpdate
|
||||
yield return new()
|
||||
{
|
||||
AuthorName = _assistantId,
|
||||
ConversationId = threadId,
|
||||
@@ -329,7 +367,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
/// 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(
|
||||
private async ValueTask<(RunCreationOptions RunOptions, ToolResources? Resources, 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.
|
||||
@@ -337,6 +375,8 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
options?.RawRepresentationFactory?.Invoke(this) as RunCreationOptions ??
|
||||
new();
|
||||
|
||||
ToolResources? resources = null;
|
||||
|
||||
// Populate the run options from the ChatOptions, if provided.
|
||||
if (options is not null)
|
||||
{
|
||||
@@ -375,51 +415,42 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
runOptions.ToolsOverride.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction, options));
|
||||
break;
|
||||
|
||||
case NewHostedCodeInterpreterTool codeTool:
|
||||
var codeInterpreterToolDefinition = new CodeInterpreterToolDefinition();
|
||||
runOptions.ToolsOverride.Add(codeInterpreterToolDefinition);
|
||||
case HostedCodeInterpreterTool codeInterpreterTool:
|
||||
var interpreterToolDef = ToolDefinition.CreateCodeInterpreter();
|
||||
runOptions.ToolsOverride.Add(interpreterToolDef);
|
||||
|
||||
if (codeTool.Inputs is { Count: > 0 })
|
||||
if (codeInterpreterTool.Inputs?.Count is > 0)
|
||||
{
|
||||
var threadInitializationMessage = new ThreadInitializationMessage(OpenAI.Assistants.MessageRole.User, [OpenAI.Assistants.MessageContent.FromText("attachments")]);
|
||||
|
||||
foreach (var input in codeTool.Inputs)
|
||||
ThreadInitializationMessage? threadInitializationMessage = null;
|
||||
foreach (var input in codeInterpreterTool.Inputs)
|
||||
{
|
||||
switch (input)
|
||||
if (input is HostedFileContent hostedFile)
|
||||
{
|
||||
case HostedFileContent fileContent:
|
||||
// Use the file ID from the HostedFileContent.
|
||||
threadInitializationMessage.Attachments.Add(new(fileContent.FileId, [codeInterpreterToolDefinition]));
|
||||
break;
|
||||
threadInitializationMessage ??= new(MessageRole.User, [MessageContent.FromText("attachments")]);
|
||||
threadInitializationMessage.Attachments.Add(new(hostedFile.FileId, [interpreterToolDef]));
|
||||
}
|
||||
}
|
||||
|
||||
runOptions.AdditionalMessages.Add(threadInitializationMessage);
|
||||
if (threadInitializationMessage is not null)
|
||||
{
|
||||
runOptions.AdditionalMessages.Add(threadInitializationMessage);
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case NewHostedFileSearchTool fileSearchTool:
|
||||
var fileSearchToolDefinition = new FileSearchToolDefinition();
|
||||
runOptions.ToolsOverride.Add(fileSearchToolDefinition);
|
||||
|
||||
// Handle file IDs for file search tool
|
||||
if (fileSearchTool.Inputs is { Count: > 0 })
|
||||
case HostedFileSearchTool fileSearchTool:
|
||||
runOptions.ToolsOverride.Add(ToolDefinition.CreateFileSearch(fileSearchTool.MaximumResultCount));
|
||||
if (fileSearchTool.Inputs is { Count: > 0 } fileSearchInputs)
|
||||
{
|
||||
var threadInitializationMessage = new ThreadInitializationMessage(OpenAI.Assistants.MessageRole.User, [OpenAI.Assistants.MessageContent.FromText("file search attachments")]);
|
||||
|
||||
foreach (var input in fileSearchTool.Inputs)
|
||||
foreach (var input in fileSearchInputs)
|
||||
{
|
||||
switch (input)
|
||||
if (input is HostedVectorStoreContent file)
|
||||
{
|
||||
case HostedFileContent fileContent:
|
||||
// Use the file ID from the HostedFileContent.
|
||||
threadInitializationMessage.Attachments.Add(new(fileContent.FileId, [fileSearchToolDefinition]));
|
||||
break;
|
||||
(resources ??= new()).FileSearch ??= new();
|
||||
resources.FileSearch.VectorStoreIds.Add(file.VectorStoreId);
|
||||
}
|
||||
}
|
||||
|
||||
runOptions.AdditionalMessages.Add(threadInitializationMessage);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -522,6 +553,10 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
{
|
||||
switch (content)
|
||||
{
|
||||
case AIContent when content.RawRepresentation is MessageContent rawRep:
|
||||
messageContents.Add(rawRep);
|
||||
break;
|
||||
|
||||
case TextContent text:
|
||||
messageContents.Add(MessageContent.FromText(text.Text));
|
||||
break;
|
||||
@@ -530,18 +565,9 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -555,7 +581,7 @@ public sealed class NewOpenAIAssistantChatClient : IChatClient
|
||||
|
||||
runOptions.AdditionalInstructions = instructions?.ToString();
|
||||
|
||||
return (runOptions, functionResults);
|
||||
return (runOptions, resources, functionResults);
|
||||
}
|
||||
|
||||
/// <summary>Convert <see cref="FunctionResultContent"/> instances to <see cref="ToolOutput"/> instances.</summary>
|
||||
@@ -613,6 +639,28 @@ internal static class OpenAIClientExtensions2
|
||||
/// <summary>Gets a <see cref="ChatRole"/> for "developer".</summary>
|
||||
internal static ChatRole ChatRoleDeveloper { get; } = new ChatRole("developer");
|
||||
|
||||
/// <summary>Creates a new instance of <see cref="FunctionCallContent"/> parsing arguments using a specified encoding and parser.</summary>
|
||||
/// <param name="json">The input arguments to be parsed.</param>
|
||||
/// <param name="callId">The function call ID.</param>
|
||||
/// <param name="name">The function name.</param>
|
||||
/// <returns>A new instance of <see cref="FunctionCallContent"/> containing the parse result.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
internal static FunctionCallContent ParseCallContent(string json, string callId, string name) =>
|
||||
FunctionCallContent.CreateFromParsedArguments(json, callId, name,
|
||||
static json => JsonSerializer.Deserialize(json, OpenAIJsonContext.Default.IDictionaryStringObject)!);
|
||||
|
||||
/// <summary>Creates a new instance of <see cref="FunctionCallContent"/> parsing arguments using a specified encoding and parser.</summary>
|
||||
/// <param name="utf8json">The input arguments to be parsed.</param>
|
||||
/// <param name="callId">The function call ID.</param>
|
||||
/// <param name="name">The function name.</param>
|
||||
/// <returns>A new instance of <see cref="FunctionCallContent"/> containing the parse result.</returns>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="callId"/> is <see langword="null"/>.</exception>
|
||||
/// <exception cref="ArgumentNullException"><paramref name="name"/> is <see langword="null"/>.</exception>
|
||||
internal static FunctionCallContent ParseCallContent(BinaryData utf8json, string callId, string name) =>
|
||||
FunctionCallContent.CreateFromParsedArguments(utf8json, callId, name,
|
||||
static utf8json => JsonSerializer.Deserialize(utf8json, OpenAIJsonContext.Default.IDictionaryStringObject)!);
|
||||
|
||||
/// <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.
|
||||
@@ -707,31 +755,4 @@ internal static class OpenAIClientExtensions2
|
||||
|
||||
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;
|
||||
|
||||
+142
-4
@@ -1,5 +1,9 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.AI;
|
||||
using Microsoft.Extensions.AI.Agents;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -20,6 +24,9 @@ namespace OpenAI;
|
||||
/// </remarks>
|
||||
public static class OpenAIAssistantClientExtensions
|
||||
{
|
||||
/// <summary>Key into AdditionalProperties used to store a strict option.</summary>
|
||||
private const string StrictKey = "strictJsonSchema";
|
||||
|
||||
/// <summary>
|
||||
/// Creates an AI agent from an <see cref="AssistantClient"/> using the OpenAI Assistant API.
|
||||
/// </summary>
|
||||
@@ -80,7 +87,7 @@ public static class OpenAIAssistantClientExtensions
|
||||
switch (tool)
|
||||
{
|
||||
case AIFunction aiFunction:
|
||||
assistantOptions.Tools.Add(NewOpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
|
||||
assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
|
||||
break;
|
||||
|
||||
case HostedCodeInterpreterTool:
|
||||
@@ -107,7 +114,7 @@ public static class OpenAIAssistantClientExtensions
|
||||
};
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope
|
||||
var chatClient = new NewOpenAIAssistantChatClient(client, assistantId);
|
||||
var chatClient = client.AsIChatClient(assistantId);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
|
||||
}
|
||||
@@ -172,7 +179,7 @@ public static class OpenAIAssistantClientExtensions
|
||||
switch (tool)
|
||||
{
|
||||
case AIFunction aiFunction:
|
||||
assistantOptions.Tools.Add(NewOpenAIAssistantChatClient.ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
|
||||
assistantOptions.Tools.Add(ToOpenAIAssistantsFunctionToolDefinition(aiFunction));
|
||||
break;
|
||||
|
||||
case HostedCodeInterpreterTool:
|
||||
@@ -199,8 +206,139 @@ public static class OpenAIAssistantClientExtensions
|
||||
};
|
||||
|
||||
#pragma warning disable CA2000 // Dispose objects before losing scope
|
||||
var chatClient = new NewOpenAIAssistantChatClient(client, assistantId);
|
||||
var chatClient = client.AsIChatClient(assistantId);
|
||||
#pragma warning restore CA2000 // Dispose objects before losing scope
|
||||
return new ChatClientAgent(chatClient, agentOptions, loggerFactory);
|
||||
}
|
||||
|
||||
/// <summary>Converts an Extensions function to an OpenAI assistants function tool.</summary>
|
||||
private static FunctionToolDefinition ToOpenAIAssistantsFunctionToolDefinition(AIFunction aiFunction, ChatOptions? options = null)
|
||||
{
|
||||
bool? strict =
|
||||
HasStrict(aiFunction.AdditionalProperties) ??
|
||||
HasStrict(options?.AdditionalProperties);
|
||||
|
||||
return new FunctionToolDefinition(aiFunction.Name)
|
||||
{
|
||||
Description = aiFunction.Description,
|
||||
Parameters = ToOpenAIFunctionParameters(aiFunction, strict),
|
||||
StrictParameterSchemaEnabled = strict,
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>Extracts from an <see cref="AIFunction"/> the parameters and strictness setting for use with OpenAI's APIs.</summary>
|
||||
private 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 = jsonSchema.Deserialize(OpenAIJsonContext.Default.ToolJson)!;
|
||||
var functionParameters = BinaryData.FromBytes(JsonSerializer.SerializeToUtf8Bytes(tool, OpenAIJsonContext.Default.ToolJson));
|
||||
|
||||
return functionParameters;
|
||||
}
|
||||
|
||||
/// <summary>Gets whether the properties specify that strict schema handling is desired.</summary>
|
||||
private static bool? HasStrict(IReadOnlyDictionary<string, object?>? additionalProperties) =>
|
||||
additionalProperties?.TryGetValue(StrictKey, out object? strictObj) is true &&
|
||||
strictObj is bool strictValue ?
|
||||
strictValue : null;
|
||||
|
||||
private 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;
|
||||
},
|
||||
});
|
||||
|
||||
/// <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(OpenAIAssistantClientExtensions.ToolJson))]
|
||||
[JsonSerializable(typeof(IDictionary<string, object?>))]
|
||||
[JsonSerializable(typeof(string[]))]
|
||||
[JsonSerializable(typeof(JsonElement))]
|
||||
internal sealed partial class OpenAIJsonContext : JsonSerializerContext;
|
||||
|
||||
Reference in New Issue
Block a user