.NET: Declarative Agents (#1301)

* AgentFactory abstractions and ChatClient implementation

* Add a getitng started sample

* Update to latest M.B.OM

* Add some additional samples

* Work in progress

* Merge latest from main

* Start to add support for using different kinds of connections

* Remove IsSupported

* Remove IsSupported

* Refactor code to create clients to support DI

* Add some unit tests

* Update based on the latest code review feedback

* Add support for OOB tools when using persistent agent sdk

* Fix sample naming

* Fix error based on latest MEAI

* Update M.B.OM package to latest

* Update to the latest M.B.OM release

* Remove some obsolete helper methods

* Update to the latest M.B.OM version

* Fix broken unit test

* Update MCP sample

* Bump to latest M.B.OM release

* Update to latest M.B.OM release

* Update to latest M.B.OM release

* Switch to using ExternalModel

* Update to latest M.B.OM

* Resolve merge conflicts

* All tests pass

* All tests pass

* Start to clean up the code

* Start to clean up the code

* More clean up

* More clean up

* More clean up

* Fix apiType checks

* Run dotnet format

* Fix typo

* Address code review feedback

* Add all properties for MCP tool

* Address code review feedback

* Address code review feedback

* Fix merge

* Undo warnings

* Undo test change

* More copilot feedback

* Make class sealed

* Address additional core review feedback

---------

Co-authored-by: Mark Wallace <markwallace@microsoft.com>
This commit is contained in:
Mark Wallace
2025-11-11 11:39:20 +00:00
committed by GitHub
Unverified
parent 105dc82c39
commit aaa91954c5
75 changed files with 3980 additions and 3 deletions
@@ -0,0 +1,49 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="CodeInterpreterTool"/>.
/// </summary>
internal static class CodeInterpreterToolExtensions
{
/// <summary>
/// Creates a <see cref="CodeInterpreterToolDefinition"/> from a <see cref="CodeInterpreterTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="CodeInterpreterTool"/></param>
internal static CodeInterpreterToolDefinition CreateCodeInterpreterToolDefinition(this CodeInterpreterTool tool)
{
Throw.IfNull(tool);
return new CodeInterpreterToolDefinition();
}
/// <summary>
/// Collects the file IDs from the extension data of a <see cref="CodeInterpreterTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="CodeInterpreterTool"/></param>
internal static List<string>? GetFileIds(this CodeInterpreterTool tool)
{
var fileIds = tool.ExtensionData?.GetPropertyOrNull<TableDataValue>(InitializablePropertyPath.Create("fileIds"));
return fileIds is not null
? [.. fileIds.Values.Select(fileId => fileId.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("value"))?.Value)]
: null;
}
/// <summary>
/// Collects the data sources from the extension data of a <see cref="CodeInterpreterTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="CodeInterpreterTool"/></param>
internal static List<VectorStoreDataSource>? GetDataSources(this CodeInterpreterTool tool)
{
var dataSources = tool.ExtensionData?.GetPropertyOrNull<TableDataValue>(InitializablePropertyPath.Create("dataSources"));
return dataSources is not null
? dataSources.Values.Select(dataSource => dataSource.CreateDataSource()).ToList()
: null;
}
}
@@ -0,0 +1,55 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="FileSearchTool"/>.
/// </summary>
internal static class FileSearchToolExtensions
{
/// <summary>
/// Creates a <see cref="FileSearchToolDefinition"/> from a <see cref="FileSearchTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="FileSearchTool"/></param>
internal static FileSearchToolDefinition CreateFileSearchToolDefinition(this FileSearchTool tool)
{
Throw.IfNull(tool);
// TODO: Add support for FileSearchToolDefinitionDetails.
return new FileSearchToolDefinition();
}
/// <summary>
/// Get the vector store IDs for the specified <see cref="FileSearchTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="FileSearchTool"/></param>
internal static List<string>? GetVectorStoreIds(this FileSearchTool tool)
{
return tool.VectorStoreIds?.LiteralValue.ToList();
}
internal static IList<VectorStoreConfigurations>? GetVectorStoreConfigurations(this FileSearchTool tool)
{
var dataSources = tool.ExtensionData?.GetPropertyOrNull<TableDataValue>(InitializablePropertyPath.Create("options.configurations"));
return dataSources?.Values.Select(value => value.CreateVectorStoreConfiguration()).ToList();
}
internal static VectorStoreConfigurations CreateVectorStoreConfiguration(this RecordDataValue value)
{
Throw.IfNull(value);
var storeName = value.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("storeName"))?.Value;
Throw.IfNullOrEmpty(storeName);
var dataSources = value.GetDataSources();
Throw.IfNull(dataSources);
return new VectorStoreConfigurations(storeName, new VectorStoreConfiguration(dataSources));
}
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="InvokeClientTaskAction"/>.
/// </summary>
public static class FunctionToolExtensions
{
/// <summary>
/// Creates a <see cref="FunctionToolDefinition"/> from a <see cref="InvokeClientTaskAction"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="InvokeClientTaskAction"/></param>
internal static FunctionToolDefinition CreateFunctionToolDefinition(this InvokeClientTaskAction tool)
{
Throw.IfNull(tool);
Throw.IfNull(tool.Name);
BinaryData parameters = tool.GetParameters();
return new FunctionToolDefinition(
name: tool.Name,
description: tool.Description,
parameters: parameters);
}
/// <summary>
/// Creates the parameters schema for a <see cref="InvokeClientTaskAction"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="InvokeClientTaskAction"/></param>
internal static BinaryData GetParameters(this InvokeClientTaskAction tool)
{
Throw.IfNull(tool);
var parameters = tool.ClientActionInputSchema?.GetSchema().ToString() ?? DefaultSchema;
return new BinaryData(parameters);
}
private const string DefaultSchema = "{\"type\":\"object\",\"properties\":{},\"additionalProperties\":false}";
}
@@ -0,0 +1,24 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Microsoft.Bot.ObjectModel;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for <see cref="HostedCodeInterpreterTool"/>.
/// </summary>
internal static class HostedCodeInterpreterToolExtensions
{
/// <summary>
/// Creates a <see cref="CodeInterpreterToolDefinition"/> from a <see cref="HostedCodeInterpreterTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="CodeInterpreterTool"/></param>
internal static CodeInterpreterToolDefinition CreateHostedCodeInterpreterToolDefinition(this HostedCodeInterpreterTool tool)
{
Throw.IfNull(tool);
return new CodeInterpreterToolDefinition();
}
}
@@ -0,0 +1,25 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for <see cref="HostedFileSearchTool"/>.
/// </summary>
internal static class HostedFileSearchToolExtensions
{
/// <summary>
/// Creates a <see cref="FileSearchToolDefinition"/> from a <see cref="HostedFileSearchTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="HostedFileSearchTool"/></param>
internal static FileSearchToolDefinition CreateFileSearchToolDefinition(this HostedFileSearchTool tool)
{
Throw.IfNull(tool);
// TODO: Add support for FileSearchToolDefinitionDetails.
return new FileSearchToolDefinition();
}
}
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Linq;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for <see cref="HostedMcpServerTool"/>.
/// </summary>
internal static class HostedMcpServerToolExtensions
{
/// <summary>
/// Creates a <see cref="MCPToolDefinition"/> from a <see cref="HostedMcpServerTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="HostedMcpServerTool"/></param>
internal static MCPToolDefinition CreateMcpToolDefinition(this HostedMcpServerTool tool)
{
Throw.IfNull(tool);
Throw.IfNull(tool.ServerName);
Throw.IfNull(tool.ServerAddress);
var definition = new MCPToolDefinition(tool.ServerName, tool.ServerAddress);
tool.AllowedTools?.ToList().ForEach(definition.AllowedTools.Add);
return definition;
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Extensions.AI;
/// <summary>
/// Extension methods for <see cref="HostedWebSearchTool"/>.
/// </summary>
internal static class HostedWebSearchToolExtensions
{
/// <summary>
/// Creates a <see cref="BingGroundingToolDefinition"/> from a <see cref="HostedWebSearchTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="HostedWebSearchTool"/></param>
internal static BingGroundingToolDefinition CreateBingGroundingToolDefinition(this HostedWebSearchTool tool)
{
Throw.IfNull(tool);
// TODO: Add support for BingGroundingSearchToolParameters.
var parameters = new BingGroundingSearchToolParameters([]);
return new BingGroundingToolDefinition(parameters);
}
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="McpServerTool"/>.
/// </summary>
internal static class McpServerToolExtensions
{
/// <summary>
/// Creates a <see cref="MCPToolDefinition"/> from a <see cref="McpServerTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="McpServerTool"/></param>
internal static MCPToolDefinition CreateMcpToolDefinition(this McpServerTool tool)
{
Throw.IfNull(tool);
Throw.IfNull(tool.ServerName?.LiteralValue);
Throw.IfNull(tool.Connection);
// TODO: Add support for additional properties
var connection = tool.Connection as AnonymousConnection ?? throw new ArgumentException("Only AnonymousConnection is supported for MCP Server Tool connections.", nameof(tool));
var serverUrl = connection.Endpoint?.LiteralValue;
Throw.IfNullOrEmpty(serverUrl, nameof(connection.Endpoint));
return new MCPToolDefinition(tool.ServerName?.LiteralValue, serverUrl);
}
}
@@ -0,0 +1,114 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="GptComponentMetadata"/>.
/// </summary>
internal static class PromptAgentExtensions
{
/// <summary>
/// Return the Foundry tool definitions which corresponds with the provided <see cref="GptComponentMetadata"/>.
/// </summary>
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
internal static IEnumerable<Azure.AI.Agents.Persistent.ToolDefinition> GetToolDefinitions(this GptComponentMetadata promptAgent)
{
Throw.IfNull(promptAgent);
return promptAgent.Tools.Select<TaskAction, Azure.AI.Agents.Persistent.ToolDefinition>(tool =>
{
return tool switch
{
CodeInterpreterTool => ((CodeInterpreterTool)tool).CreateCodeInterpreterToolDefinition(),
InvokeClientTaskAction => ((InvokeClientTaskAction)tool).CreateFunctionToolDefinition(),
FileSearchTool => ((FileSearchTool)tool).CreateFileSearchToolDefinition(),
WebSearchTool => ((WebSearchTool)tool).CreateBingGroundingToolDefinition(),
McpServerTool => ((McpServerTool)tool).CreateMcpToolDefinition(),
// TODO: Add other tool types as custom tools
// AzureAISearch
// AzureFunction
// OpenApi
_ => throw new NotSupportedException($"Unable to create tool definition because of unsupported tool type: {tool.Kind}"),
};
}).ToList();
}
/// <summary>
/// Return the Foundry tool resources which corresponds with the provided <see cref="GptComponentMetadata"/>.
/// </summary>
/// <param name="promptAgent">Instance of <see cref="GptComponentMetadata"/></param>
internal static ToolResources GetToolResources(this GptComponentMetadata promptAgent)
{
Throw.IfNull(promptAgent);
var toolResources = new ToolResources();
var codeInterpreter = promptAgent.GetCodeInterpreterToolResource();
if (codeInterpreter is not null)
{
toolResources.CodeInterpreter = codeInterpreter;
}
var fileSearch = promptAgent.GetFileSearchToolResource();
if (fileSearch is not null)
{
toolResources.FileSearch = fileSearch;
}
// TODO Handle MCP tool resources
return toolResources;
}
#region private
private static CodeInterpreterToolResource? GetCodeInterpreterToolResource(this GptComponentMetadata promptAgent)
{
Throw.IfNull(promptAgent);
CodeInterpreterToolResource? resource = null;
var codeInterpreter = (CodeInterpreterTool?)promptAgent.GetFirstAgentTool<CodeInterpreterTool>();
if (codeInterpreter is not null)
{
var fileIds = codeInterpreter.GetFileIds();
var dataSources = codeInterpreter.GetDataSources();
if (fileIds is not null || dataSources is not null)
{
resource = new CodeInterpreterToolResource();
fileIds?.ForEach(id => resource.FileIds.Add(id));
dataSources?.ForEach(ds => resource.DataSources.Add(ds));
}
}
return resource;
}
private static FileSearchToolResource? GetFileSearchToolResource(this GptComponentMetadata promptAgent)
{
Throw.IfNull(promptAgent);
var fileSearch = (FileSearchTool?)promptAgent.GetFirstAgentTool<FileSearchTool>();
if (fileSearch is not null)
{
var vectorStoreIds = fileSearch.GetVectorStoreIds();
var vectorStores = fileSearch.GetVectorStoreConfigurations();
if (vectorStoreIds is not null || vectorStores is not null)
{
return new FileSearchToolResource(vectorStoreIds, vectorStores);
}
}
return null;
}
private static TaskAction? GetFirstAgentTool<T>(this GptComponentMetadata promptAgent)
{
return promptAgent.Tools.FirstOrDefault(tool => tool is T);
}
#endregion
}
@@ -0,0 +1,45 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using Microsoft.Extensions.AI;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="RecordDataType"/>.
/// </summary>
internal static class RecordDataTypeExtensions
{
/// <summary>
/// Creates a <see cref="ChatResponseFormat"/> from a <see cref="RecordDataType"/>.
/// </summary>
/// <param name="recordDataType">Instance of <see cref="RecordDataType"/></param>
#pragma warning disable IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
#pragma warning disable IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
internal static BinaryData? AsBinaryData(this RecordDataType recordDataType)
{
Throw.IfNull(recordDataType);
if (recordDataType.Properties.Count == 0)
{
return null;
}
return BinaryData.FromObjectAsJson(
new
{
type = "json_schema",
schema =
new
{
type = "object",
properties = recordDataType.Properties.AsObjectDictionary(),
additionalProperties = false
}
}
);
}
#pragma warning restore IL3050 // Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.
#pragma warning restore IL2026 // Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code
}
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Linq;
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="RecordDataValue"/>.
/// </summary>
internal static class RecordDataValueExtensions
{
/// <summary>
/// Gets the data sources from the specified <see cref="RecordDataValue"/>.
/// </summary>
internal static List<VectorStoreDataSource>? GetDataSources(this RecordDataValue value)
{
var dataSources = value.GetPropertyOrNull<TableDataValue>(InitializablePropertyPath.Create("options.data_sources"));
return dataSources?.Values.Select(dataSource => dataSource.CreateDataSource()).ToList();
}
/// <summary>
/// Creates a new instance of <see cref="VectorStoreDataSource"/> using the specified <see cref="RecordDataValue"/>.
/// </summary>
internal static VectorStoreDataSource CreateDataSource(this RecordDataValue value)
{
Throw.IfNull(value);
string? assetIdentifier = value.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("assetIdentifier"))?.Value;
Throw.IfNullOrEmpty(assetIdentifier);
string? assetType = value.GetPropertyOrNull<StringDataValue>(InitializablePropertyPath.Create("assetType"))?.Value;
Throw.IfNullOrEmpty(assetType);
return new VectorStoreDataSource(assetIdentifier, new VectorStoreDataSourceAssetType(assetType));
}
}
@@ -0,0 +1,26 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.AI.Agents.Persistent;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Bot.ObjectModel;
/// <summary>
/// Extension methods for <see cref="WebSearchTool"/>.
/// </summary>
internal static class WebSearchToolExtensions
{
/// <summary>
/// Creates a <see cref="BingGroundingToolDefinition"/> from a <see cref="WebSearchTool"/>.
/// </summary>
/// <param name="tool">Instance of <see cref="WebSearchTool"/></param>
internal static BingGroundingToolDefinition CreateBingGroundingToolDefinition(this WebSearchTool tool)
{
Throw.IfNull(tool);
// TODO: Add support for BingGroundingSearchToolParameters.
var parameters = new BingGroundingSearchToolParameters([]);
return new BingGroundingToolDefinition(parameters);
}
}
@@ -0,0 +1,89 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Core;
using Microsoft.Bot.ObjectModel;
using Microsoft.Shared.Diagnostics;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an <see cref="AgentFactory"/> which creates instances of <see cref="AIAgent"/> using a <see cref="PersistentAgentsClient"/>.
/// </summary>
public sealed class FoundryPersistentAgentFactory : AgentFactory
{
private readonly PersistentAgentsClient? _agentClient;
private readonly TokenCredential? _tokenCredential;
/// <summary>
/// Creates a new instance of the <see cref="FoundryPersistentAgentFactory"/> class.
/// </summary>
public FoundryPersistentAgentFactory(PersistentAgentsClient agentClient)
{
Throw.IfNull(agentClient);
this._agentClient = agentClient;
}
/// <summary>
/// Creates a new instance of the <see cref="FoundryPersistentAgentFactory"/> class.
/// </summary>
public FoundryPersistentAgentFactory(TokenCredential tokenCredential)
{
Throw.IfNull(tokenCredential);
this._tokenCredential = tokenCredential;
}
/// <inheritdoc/>
public override async Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
var agentClient = this._agentClient ?? this.CreatePersistentAgentClient(promptAgent);
var modelId = promptAgent.Model?.ModelNameHint;
if (string.IsNullOrEmpty(modelId))
{
throw new InvalidOperationException("The model id must be specified in the agent definition model to create a foundry agent.");
}
//var outputSchema = promptAgent.OutputType; TODO: Fix converting RecordDataType to BinaryData
var modelOptions = promptAgent.Model?.Options;
return await agentClient.CreateAIAgentAsync(
model: modelId,
name: promptAgent.Name,
instructions: promptAgent.Instructions?.ToTemplateString(),
tools: promptAgent.GetToolDefinitions(),
toolResources: promptAgent.GetToolResources(),
temperature: (float?)modelOptions?.Temperature?.LiteralValue,
topP: (float?)modelOptions?.TopP?.LiteralValue,
//responseFormat: outputSchema.AsBinaryData(), TODO: Fix converting RecordDataType to BinaryData
metadata: promptAgent.Metadata?.ToDictionary(),
cancellationToken: cancellationToken).ConfigureAwait(false);
}
private PersistentAgentsClient CreatePersistentAgentClient(GptComponentMetadata promptAgent)
{
var externalModel = promptAgent.Model as CurrentModels;
var connection = externalModel?.Connection as RemoteConnection;
if (connection is not null)
{
var endpoint = connection.Endpoint?.LiteralValue;
if (string.IsNullOrEmpty(endpoint))
{
throw new InvalidOperationException("The endpoint must be specified in the agent definition model connection to create an PersistentAgentsClient.");
}
if (this._tokenCredential is null)
{
throw new InvalidOperationException("A TokenCredential must be registered in the service provider to create an PersistentAgentsClient.");
}
return new PersistentAgentsClient(endpoint, this._tokenCredential);
}
throw new InvalidOperationException("A PersistentAgentsClient must be registered in the service provider or a FoundryConnection must be specified in the agent definition model connection to create an PersistentAgentsClient.");
}
}
@@ -0,0 +1,32 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.AI;
namespace Microsoft.Agents.AI.Declarative.AzureAI;
/// <summary>
/// A class to describe the parameters of an <see cref="AIFunction"/> in a JSON Schema friendly way.
/// </summary>
internal sealed class JsonSchemaFunctionParameters
{
/// <summary>
/// The type of schema which is always "object" when describing function parameters.
/// </summary>
[JsonPropertyName("type")]
public string Type => "object";
/// <summary>
/// The list of required properties.
/// </summary>
[JsonPropertyName("required")]
public List<string> Required { get; set; } = [];
/// <summary>
/// A dictionary of properties, keyed by name => JSON Schema.
/// </summary>
[JsonPropertyName("properties")]
public Dictionary<string, JsonElement> Properties { get; set; } = [];
}
@@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(ProjectsTargetFrameworks)</TargetFrameworks>
<TargetFrameworks Condition="'$(Configuration)' == 'Debug'">$(ProjectsDebugTargetFrameworks)</TargetFrameworks>
<VersionSuffix>preview</VersionSuffix>
<NoWarn>$(NoWarn);MEAI001;OPENAI001</NoWarn>
</PropertyGroup>
<PropertyGroup>
<InjectSharedThrow>true</InjectSharedThrow>
<InjectDiagnosticClassesOnLegacy>true</InjectDiagnosticClassesOnLegacy>
<InjectTrimAttributesOnLegacy>true</InjectTrimAttributesOnLegacy>
<InjectIsExternalInitOnLegacy>true</InjectIsExternalInitOnLegacy>
</PropertyGroup>
<Import Project="$(RepoRoot)/dotnet/nuget/nuget-package.props" />
<PropertyGroup>
<!-- NuGet Package Settings -->
<Title>Microsoft Agent Framework Declarative AzureAI</Title>
<Description>Provides Microsoft Agent Framework support for declarative AzureAI agents.</Description>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Azure.Identity" />
<PackageReference Include="Azure.AI.OpenAI" />
<PackageReference Include="System.Diagnostics.DiagnosticSource" />
<PackageReference Include="Azure.AI.Agents.Persistent" />
<PackageReference Include="Microsoft.Extensions.AI" />
<PackageReference Include="Microsoft.Extensions.AI.OpenAI" />
<PackageReference Include="Microsoft.Bot.ObjectModel" />
<PackageReference Include="Microsoft.Bot.ObjectModel.Json" />
<PackageReference Include="Microsoft.Bot.ObjectModel.PowerFx" />
<PackageReference Include="Microsoft.PowerFx.Interpreter" />
<PackageReference Include="Microsoft.Extensions.Configuration" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Microsoft.Agents.AI.Abstractions\Microsoft.Agents.AI.Abstractions.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.AzureAI.Persistent\Microsoft.Agents.AI.AzureAI.Persistent.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.Declarative\Microsoft.Agents.AI.Declarative.csproj" />
<ProjectReference Include="..\Microsoft.Agents.AI.OpenAI\Microsoft.Agents.AI.OpenAI.csproj" />
</ItemGroup>
<ItemGroup>
<InternalsVisibleTo Include="Microsoft.Agents.AI.Declarative.AzureAI.UnitTests" />
</ItemGroup>
</Project>
@@ -0,0 +1,182 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ClientModel;
using Azure.AI.OpenAI;
using Azure.Core;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI;
using OpenAI.Assistants;
using OpenAI.Chat;
using OpenAI.Responses;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an <see cref="OpenAIAgentFactory"/> abstract base class.
/// </summary>
public abstract class OpenAIAgentFactory : AgentFactory
{
/// <summary>
/// Creates a new instance of the <see cref="OpenAIAgentFactory"/> class.
/// </summary>
protected OpenAIAgentFactory(ILoggerFactory? loggerFactory)
{
this.LoggerFactory = loggerFactory;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIAgentFactory"/> class.
/// </summary>
protected OpenAIAgentFactory(Uri endpoint, TokenCredential tokenCredential, ILoggerFactory? loggerFactory)
{
Throw.IfNull(endpoint);
Throw.IfNull(tokenCredential);
this._endpoint = endpoint;
this._tokenCredential = tokenCredential;
this.LoggerFactory = loggerFactory;
}
/// <summary>
/// Gets the <see cref="ILoggerFactory"/> instance used for creating loggers.
/// </summary>
protected ILoggerFactory? LoggerFactory { get; }
/// <summary>
/// Creates a new instance of the <see cref="ChatClient"/> class.
/// </summary>
protected ChatClient? CreateChatClient(GptComponentMetadata promptAgent)
{
var model = promptAgent.Model as CurrentModels;
var provider = model?.Provider?.Value ?? ModelProvider.OpenAI;
if (provider == ModelProvider.OpenAI)
{
return CreateOpenAIChatClient(promptAgent);
}
else if (provider == ModelProvider.AzureOpenAI)
{
Throw.IfNull(this._endpoint, "A endpoint must be specified to create an Azure OpenAI client");
Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client");
return CreateAzureOpenAIChatClient(promptAgent, this._endpoint, this._tokenCredential);
}
return null;
}
/// <summary>
/// Creates a new instance of the <see cref="AssistantClient"/> class.
/// </summary>
protected AssistantClient? CreateAssistantClient(GptComponentMetadata promptAgent)
{
var model = promptAgent.Model as CurrentModels;
var provider = model?.Provider?.Value ?? ModelProvider.OpenAI;
if (provider == ModelProvider.OpenAI)
{
return CreateOpenAIAssistantClient(promptAgent);
}
else if (provider == ModelProvider.AzureOpenAI)
{
Throw.IfNull(this._endpoint, "The connection endpoint must be specified to create an Azure OpenAI client.");
Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client");
return CreateAzureOpenAIAssistantClient(promptAgent, this._endpoint, this._tokenCredential);
}
return null;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIResponseClient"/> class.
/// </summary>
protected OpenAIResponseClient? CreateResponseClient(GptComponentMetadata promptAgent)
{
var model = promptAgent.Model as CurrentModels;
var provider = model?.Provider?.Value ?? ModelProvider.OpenAI;
if (provider == ModelProvider.OpenAI)
{
return CreateOpenAIResponseClient(promptAgent);
}
else if (provider == ModelProvider.AzureOpenAI)
{
Throw.IfNull(this._endpoint, "The connection endpoint must be specified to create an Azure OpenAI client.");
Throw.IfNull(this._tokenCredential, "A token credential must be specified to create an Azure OpenAI client");
return CreateAzureOpenAIResponseClient(promptAgent, this._endpoint, this._tokenCredential);
}
return null;
}
#region private
private readonly Uri? _endpoint;
private readonly TokenCredential? _tokenCredential;
private static ChatClient CreateOpenAIChatClient(GptComponentMetadata promptAgent)
{
var modelId = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent.");
return CreateOpenAIClient(promptAgent).GetChatClient(modelId);
}
private static ChatClient CreateAzureOpenAIChatClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential)
{
var deploymentName = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent.");
return new AzureOpenAIClient(endpoint, tokenCredential).GetChatClient(deploymentName);
}
private static AssistantClient CreateOpenAIAssistantClient(GptComponentMetadata promptAgent)
{
var modelId = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent.");
return CreateOpenAIClient(promptAgent).GetAssistantClient();
}
private static AssistantClient CreateAzureOpenAIAssistantClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential)
{
var deploymentName = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent.");
return new AzureOpenAIClient(endpoint, tokenCredential).GetAssistantClient();
}
private static OpenAIResponseClient CreateOpenAIResponseClient(GptComponentMetadata promptAgent)
{
var modelId = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI agent.");
return CreateOpenAIClient(promptAgent).GetOpenAIResponseClient(modelId);
}
private static OpenAIResponseClient CreateAzureOpenAIResponseClient(GptComponentMetadata promptAgent, Uri endpoint, TokenCredential tokenCredential)
{
var deploymentName = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(deploymentName, "The deployment name (using model.id) must be specified in the agent definition to create an Azure OpenAI agent.");
return new AzureOpenAIClient(endpoint, tokenCredential).GetOpenAIResponseClient(deploymentName);
}
private static OpenAIClient CreateOpenAIClient(GptComponentMetadata promptAgent)
{
var model = promptAgent.Model as CurrentModels;
var keyConnection = model?.Connection as ApiKeyConnection;
Throw.IfNull(keyConnection, "A key connection must be specified when create an OpenAI client");
var apiKey = keyConnection.Key?.LiteralValue;
Throw.IfNullOrEmpty(apiKey, "The connection key must be specified in the agent definition to create an OpenAI client.");
var clientOptions = new OpenAIClientOptions();
var endpoint = keyConnection.Endpoint?.LiteralValue;
if (!string.IsNullOrEmpty(endpoint))
{
clientOptions.Endpoint = new Uri(endpoint);
}
return new OpenAIClient(new ApiKeyCredential(apiKey), clientOptions);
}
#endregion
}
@@ -0,0 +1,91 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Core;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI;
using OpenAI.Assistants;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an <see cref="AgentFactory"/> which creates instances of <see cref="AIAgent"/> using a <see cref="AssistantClient"/>.
/// </summary>
public sealed class OpenAIAssistantAgentFactory : OpenAIAgentFactory
{
/// <summary>
/// Creates a new instance of the <see cref="OpenAIAssistantAgentFactory"/> class.
/// </summary>
public OpenAIAssistantAgentFactory(IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIAssistantAgentFactory"/> class.
/// </summary>
public OpenAIAssistantAgentFactory(AssistantClient assistantClient, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
Throw.IfNull(assistantClient);
this._assistantClient = assistantClient;
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIAssistantAgentFactory"/> class.
/// </summary>
public OpenAIAssistantAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory)
{
this._functions = functions;
}
/// <inheritdoc/>
public override async Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
var model = promptAgent.Model as CurrentModels;
var apiType = model?.ApiType;
if (apiType?.IsUnknown() == false || apiType?.UnknownValue?.Equals(API_TYPE_ASSISTANTS, StringComparison.OrdinalIgnoreCase) == false)
{
return null;
}
var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Description = promptAgent.Description,
Instructions = promptAgent.Instructions?.ToTemplateString(),
ChatOptions = promptAgent.GetChatOptions(this._functions),
};
AssistantClient? assistantClient = this._assistantClient ?? this.CreateAssistantClient(promptAgent);
if (assistantClient is not null)
{
var modelId = promptAgent.Model?.ModelNameHint;
Throw.IfNullOrEmpty(modelId, "The model id must be specified in the agent definition to create an OpenAI Assistant.");
Throw.IfNullOrEmpty(promptAgent.Instructions?.ToTemplateString(), "The instructions must be specified in the agent definition to create an OpenAI Assistant.");
return await assistantClient.CreateAIAgentAsync(
modelId,
options
).ConfigureAwait(false);
}
return null;
}
#region private
private readonly AssistantClient? _assistantClient;
private readonly IList<AIFunction>? _functions;
private const string API_TYPE_ASSISTANTS = "ASSISTANTS";
#endregion
}
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Core;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Chat;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an <see cref="AgentFactory"/> which creates instances of <see cref="AIAgent"/> using a <see cref="ChatClient"/>.
/// </summary>
public sealed class OpenAIChatAgentFactory : OpenAIAgentFactory
{
/// <summary>
/// Creates a new instance of the <see cref="OpenAIChatAgentFactory"/> class.
/// </summary>
public OpenAIChatAgentFactory(IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIChatAgentFactory"/> class.
/// </summary>
public OpenAIChatAgentFactory(ChatClient chatClient, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
Throw.IfNull(chatClient);
this._chatClient = chatClient;
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIChatAgentFactory"/> class.
/// </summary>
public OpenAIChatAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory)
{
this._functions = functions;
}
/// <inheritdoc/>
public override async Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
var model = promptAgent.Model as CurrentModels;
var apiType = model?.ApiType;
if (apiType?.IsUnknown() == true || apiType?.Value != ModelApiType.Chat)
{
return null;
}
var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Description = promptAgent.Description,
Instructions = promptAgent.Instructions?.ToTemplateString(),
ChatOptions = promptAgent.GetChatOptions(this._functions),
};
ChatClient? chatClient = this._chatClient ?? this.CreateChatClient(promptAgent);
if (chatClient is not null)
{
return new ChatClientAgent(
chatClient.AsIChatClient(),
options,
this.LoggerFactory);
}
return null;
}
#region private
private readonly ChatClient? _chatClient;
private readonly IList<AIFunction>? _functions;
#endregion
}
@@ -0,0 +1,84 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Azure.AI.Agents.Persistent;
using Azure.Core;
using Microsoft.Bot.ObjectModel;
using Microsoft.Extensions.AI;
using Microsoft.Extensions.Logging;
using Microsoft.Shared.Diagnostics;
using OpenAI.Responses;
namespace Microsoft.Agents.AI;
/// <summary>
/// Provides an <see cref="AgentFactory"/> which creates instances of <see cref="AIAgent"/> using a <see cref="OpenAIResponseClient"/>.
/// </summary>
public sealed class OpenAIResponseAgentFactory : OpenAIAgentFactory
{
/// <summary>
/// Creates a new instance of the <see cref="OpenAIResponseAgentFactory"/> class.
/// </summary>
public OpenAIResponseAgentFactory(IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIResponseAgentFactory"/> class.
/// </summary>
public OpenAIResponseAgentFactory(OpenAIResponseClient responseClient, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(loggerFactory)
{
Throw.IfNull(responseClient);
this._responseClient = responseClient;
this._functions = functions;
}
/// <summary>
/// Creates a new instance of the <see cref="OpenAIChatAgentFactory"/> class.
/// </summary>
public OpenAIResponseAgentFactory(Uri endpoint, TokenCredential tokenCredential, IList<AIFunction>? functions = null, ILoggerFactory? loggerFactory = null) : base(endpoint, tokenCredential, loggerFactory)
{
this._functions = functions;
}
/// <inheritdoc/>
public override async Task<AIAgent?> TryCreateAsync(GptComponentMetadata promptAgent, CancellationToken cancellationToken = default)
{
Throw.IfNull(promptAgent);
var model = promptAgent.Model as CurrentModels;
var apiType = model?.ApiType;
if (apiType?.IsUnknown() == true || apiType?.Value != ModelApiType.Responses)
{
return null;
}
var options = new ChatClientAgentOptions()
{
Name = promptAgent.Name,
Description = promptAgent.Description,
Instructions = promptAgent.Instructions?.ToTemplateString(),
ChatOptions = promptAgent.GetChatOptions(this._functions),
};
var responseClient = this._responseClient ?? this.CreateResponseClient(promptAgent);
if (responseClient is not null)
{
return new ChatClientAgent(
responseClient.AsIChatClient(),
options,
this.LoggerFactory);
}
return null;
}
#region private
private readonly OpenAIResponseClient? _responseClient;
private readonly IList<AIFunction>? _functions;
#endregion
}