diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props
index 1b55be45d37d..2e15ff89460f 100644
--- a/dotnet/Directory.Packages.props
+++ b/dotnet/Directory.Packages.props
@@ -9,7 +9,6 @@
-
diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln
index 4c4ed6c4df5a..b4580b4d1146 100644
--- a/dotnet/SK-dotnet.sln
+++ b/dotnet/SK-dotnet.sln
@@ -277,16 +277,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithAgents",
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{77E141BA-AF5E-4C01-A970-6C07AC3CD55A}"
ProjectSection(SolutionItems) = preProject
- src\InternalUtilities\samples\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\ConfigurationNotFoundException.cs
- src\InternalUtilities\samples\EnumerableExtensions.cs = src\InternalUtilities\samples\EnumerableExtensions.cs
- src\InternalUtilities\samples\Env.cs = src\InternalUtilities\samples\Env.cs
- src\InternalUtilities\samples\ObjectExtensions.cs = src\InternalUtilities\samples\ObjectExtensions.cs
- src\InternalUtilities\samples\PlanExtensions.cs = src\InternalUtilities\samples\PlanExtensions.cs
- src\InternalUtilities\samples\RepoFiles.cs = src\InternalUtilities\samples\RepoFiles.cs
src\InternalUtilities\samples\SamplesInternalUtilities.props = src\InternalUtilities\samples\SamplesInternalUtilities.props
- src\InternalUtilities\samples\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\TextOutputHelperExtensions.cs
- src\InternalUtilities\samples\XunitLogger.cs = src\InternalUtilities\samples\XunitLogger.cs
- src\InternalUtilities\samples\YourAppException.cs = src\InternalUtilities\samples\YourAppException.cs
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Prompty", "src\Functions\Functions.Prompty\Functions.Prompty.csproj", "{12B06019-740B-466D-A9E0-F05BC123A47D}"
@@ -340,9 +331,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}"
EndProject
-Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}"
-EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CreateChatGptPlugin", "CreateChatGptPlugin", "{F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098}"
EndProject
@@ -352,6 +341,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MathPlugin", "MathPlugin",
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{EE454832-085F-4D37-B19B-F94F7FC6984A}"
+ ProjectSection(SolutionItems) = preProject
+ src\InternalUtilities\samples\InternalUtilities\BaseTest.cs = src\InternalUtilities\samples\InternalUtilities\BaseTest.cs
+ src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs
+ src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs = src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs
+ src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs = src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs
+ src\InternalUtilities\samples\InternalUtilities\Env.cs = src\InternalUtilities\samples\InternalUtilities\Env.cs
+ src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs = src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs
+ src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs = src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs
+ src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs = src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs
+ src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs = src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs
+ src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs
+ src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs = src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs
+ src\InternalUtilities\samples\InternalUtilities\YourAppException.cs = src\InternalUtilities\samples\InternalUtilities\YourAppException.cs
+ EndProjectSection
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30E0-7AC1-47F4-8244-57B066B43FD8}"
+ ProjectSection(SolutionItems) = preProject
+ src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs = src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs
+ EndProjectSection
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -839,10 +851,6 @@ Global
{ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.Build.0 = Debug|Any CPU
{ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.Build.0 = Release|Any CPU
- {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
- {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.Build.0 = Release|Any CPU
{E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
{E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
@@ -861,6 +869,12 @@ Global
{4326A974-F027-4ABD-A220-382CC6BB0801}.Publish|Any CPU.Build.0 = Debug|Any CPU
{4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.Build.0 = Release|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.Build.0 = Debug|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -975,12 +989,14 @@ Global
{738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB}
{8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
{ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C}
- {38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{E06818E3-00A5-41AC-97ED-9491070CDEA1} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
{6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A}
{4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098}
{4326A974-F027-4ABD-A220-382CC6BB0801} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A}
+ {EE454832-085F-4D37-B19B-F94F7FC6984A} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A}
+ {5C6C30E0-7AC1-47F4-8244-57B066B43FD8} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A}
+ {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83}
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
index 16c019aebbfd..d0b8e92d39d7 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs
@@ -12,7 +12,7 @@ namespace Agents;
/// Demonstrate usage of for both direction invocation
/// of and via .
///
-public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseTest(output)
+public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseAgentsTest(output)
{
[Fact]
public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync()
@@ -44,25 +44,25 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync()
Console.WriteLine("================================");
foreach (ChatMessageContent message in chat)
{
- this.WriteContent(message);
+ this.WriteAgentChatMessage(message);
}
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- ChatMessageContent userContent = new(AuthorRole.User, input);
- chat.Add(userContent);
- this.WriteContent(userContent);
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.Add(message);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in agent.InvokeAsync(chat))
+ await foreach (ChatMessageContent response in agent.InvokeAsync(chat))
{
// Do not add a message implicitly added to the history.
- if (!content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent))
+ if (!response.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent))
{
- chat.Add(content);
+ chat.Add(response);
}
- this.WriteContent(content);
+ this.WriteAgentChatMessage(response);
}
}
}
@@ -98,28 +98,23 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync()
ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync();
for (int index = history.Length; index > 0; --index)
{
- this.WriteContent(history[index - 1]);
+ this.WriteAgentChatMessage(history[index - 1]);
}
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- ChatMessageContent userContent = new(AuthorRole.User, input);
- chat.AddChatMessage(userContent);
- this.WriteContent(userContent);
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.AddChatMessage(message);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- this.WriteContent(content);
+ this.WriteAgentChatMessage(response);
}
}
}
- private void WriteContent(ChatMessageContent content)
- {
- Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'");
- }
-
private Kernel CreateKernelWithFilter()
{
IKernelBuilder builder = Kernel.CreateBuilder();
diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
index d3e94386af96..575db7f7f288 100644
--- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
+++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs
@@ -12,7 +12,7 @@ namespace Agents;
/// Demonstrate creation of and
/// eliciting its response to three explicit user messages.
///
-public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseTest(output)
+public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ParrotName = "Parrot";
private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound.";
@@ -66,32 +66,33 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync()
// Local function to invoke agent and display the conversation messages.
private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input)
{
- chat.Add(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.Add(message);
+ this.WriteAgentChatMessage(message);
StringBuilder builder = new();
- await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat))
+ await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat))
{
- if (string.IsNullOrEmpty(message.Content))
+ if (string.IsNullOrEmpty(response.Content))
{
continue;
}
if (builder.Length == 0)
{
- Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:");
+ Console.WriteLine($"# {response.Role} - {response.AuthorName ?? "*"}:");
}
- Console.WriteLine($"\t > streamed: '{message.Content}'");
- builder.Append(message.Content);
+ Console.WriteLine($"\t > streamed: '{response.Content}'");
+ builder.Append(response.Content);
}
if (builder.Length > 0)
{
// Display full response and capture in chat history
- Console.WriteLine($"\t > complete: '{builder}'");
- chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name });
+ ChatMessageContent response = new(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name };
+ chat.Add(response);
+ this.WriteAgentChatMessage(response);
}
}
diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
index 81b2914ade3b..0d7b27917d78 100644
--- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
+++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs
@@ -13,10 +13,8 @@ namespace Agents;
/// Demonstrate usage of and
/// to manage execution.
///
-public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseTest(output)
+public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseAgentsTest(output)
{
- protected override bool ForceOpenAI => true;
-
private const string InternalLeaderName = "InternalLeader";
private const string InternalLeaderInstructions =
"""
@@ -154,20 +152,20 @@ public async Task NestedChatWithAggregatorAgentAsync()
Console.WriteLine(">>>> AGGREGATED CHAT");
Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>");
- await foreach (ChatMessageContent content in chat.GetChatMessagesAsync(personalShopperAgent).Reverse())
+ await foreach (ChatMessageContent message in chat.GetChatMessagesAsync(personalShopperAgent).Reverse())
{
- Console.WriteLine($">>>> {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(message);
}
async Task InvokeChatAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.AddChatMessage(message);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in chat.InvokeAsync(personalShopperAgent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(personalShopperAgent))
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
Console.WriteLine($"\n# IS COMPLETE: {chat.IsComplete}");
diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs
index 062262fe8a8c..53276c75a24d 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs
@@ -9,12 +9,6 @@ namespace Agents;
///
public class Legacy_AgentAuthoring(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and parallel function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";
-
// Track agents for clean-up
private static readonly List s_agents = [];
@@ -72,7 +66,7 @@ private static async Task CreateArticleGeneratorAsync()
return
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.WithInstructions("You write concise opinionated articles that are published online. Use an outline to generate an article with one section of prose for each top-level outline element. Each section is based on research with a maximum of 120 words.")
.WithName("Article Author")
.WithDescription("Author an article on a given topic.")
@@ -87,7 +81,7 @@ private static async Task CreateOutlineGeneratorAsync()
return
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.WithInstructions("Produce an single-level outline (no child elements) based on the given topic with at most 3 sections.")
.WithName("Outline Generator")
.WithDescription("Generate an outline.")
@@ -100,7 +94,7 @@ private static async Task CreateResearchGeneratorAsync()
return
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.WithInstructions("Provide insightful research that supports the given topic based on your knowledge of the outline topic.")
.WithName("Researcher")
.WithDescription("Author research summary.")
diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs
index b64f183adbc8..d40755101309 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs
@@ -1,8 +1,10 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Azure.AI.OpenAI;
using Microsoft.SemanticKernel.Experimental.Agents;
+using OpenAI;
+using OpenAI.Files;
namespace Agents;
@@ -12,28 +14,15 @@ namespace Agents;
///
public sealed class Legacy_AgentCharts(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and parallel function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";
-
- ///
- /// Flag to force usage of OpenAI configuration if both
- /// and are defined.
- /// If 'false', Azure takes precedence.
- ///
- private new const bool ForceOpenAI = false;
-
///
/// Create a chart and retrieve by file_id.
///
- [Fact(Skip = "Launches external processes")]
+ [Fact]
public async Task CreateChartAsync()
{
Console.WriteLine("======== Using CodeInterpreter tool ========");
- var fileService = CreateFileService();
+ FileClient fileClient = CreateFileClient();
var agent = await CreateAgentBuilder().WithCodeInterpreter().BuildAsync();
@@ -69,11 +58,11 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi
{
var filename = $"{imageName}.jpg";
var path = Path.Combine(Environment.CurrentDirectory, filename);
- Console.WriteLine($"# {message.Role}: {message.Content}");
+ var fileId = message.Content;
+ Console.WriteLine($"# {message.Role}: {fileId}");
Console.WriteLine($"# {message.Role}: {path}");
- var content = await fileService.GetFileContentAsync(message.Content);
- await using var outputStream = File.OpenWrite(filename);
- await outputStream.WriteAsync(content.Data!.Value);
+ BinaryData content = await fileClient.DownloadFileAsync(fileId);
+ File.WriteAllBytes(filename, content.ToArray());
Process.Start(
new ProcessStartInfo
{
@@ -91,22 +80,23 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi
}
}
-#pragma warning disable CS0618 // Type or member is obsolete
- private static OpenAIFileService CreateFileService()
+ private FileClient CreateFileClient()
{
- return
- ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
- new OpenAIFileService(TestConfiguration.OpenAI.ApiKey) :
- new OpenAIFileService(new Uri(TestConfiguration.AzureOpenAI.Endpoint), apiKey: TestConfiguration.AzureOpenAI.ApiKey);
+ OpenAIClient client =
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ new OpenAIClient(TestConfiguration.OpenAI.ApiKey) :
+ new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey);
+
+ return client.GetFileClient();
}
#pragma warning restore CS0618 // Type or member is obsolete
- private static AgentBuilder CreateAgentBuilder()
+ private AgentBuilder CreateAgentBuilder()
{
return
- ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
- new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) :
new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}
}
diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs
index 53ae0c07662a..fa257d2764b3 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs
@@ -9,17 +9,6 @@ namespace Agents;
///
public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-4-turbo-preview";
-
- ///
- /// Set this to 'true' to target OpenAI instead of Azure OpenAI.
- ///
- private const bool UseOpenAI = false;
-
// Track agents for clean-up
private static readonly List s_agents = [];
@@ -29,8 +18,6 @@ public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(outp
[Fact(Skip = "This test take more than 5 minutes to execute")]
public async Task RunCollaborationAsync()
{
- Console.WriteLine($"======== Example72:Collaboration:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========");
-
IAgentThread? thread = null;
try
{
@@ -82,8 +69,6 @@ public async Task RunCollaborationAsync()
[Fact(Skip = "This test take more than 2 minutes to execute")]
public async Task RunAsPluginsAsync()
{
- Console.WriteLine($"======== Example72:AsPlugins:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========");
-
try
{
// Create copy-writer agent to generate ideas
@@ -113,7 +98,7 @@ await CreateAgentBuilder()
}
}
- private static async Task CreateCopyWriterAsync(IAgent? agent = null)
+ private async Task CreateCopyWriterAsync(IAgent? agent = null)
{
return
Track(
@@ -125,7 +110,7 @@ await CreateAgentBuilder()
.BuildAsync());
}
- private static async Task CreateArtDirectorAsync()
+ private async Task CreateArtDirectorAsync()
{
return
Track(
@@ -136,13 +121,13 @@ await CreateAgentBuilder()
.BuildAsync());
}
- private static AgentBuilder CreateAgentBuilder()
+ private AgentBuilder CreateAgentBuilder()
{
var builder = new AgentBuilder();
return
- UseOpenAI ?
- builder.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ builder.WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) :
builder.WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}
diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs
index 86dacb9c256d..b4b0ed93199f 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs
@@ -12,12 +12,6 @@ namespace Agents;
///
public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";
-
// Track agents for clean-up
private static readonly List s_agents = [];
@@ -27,8 +21,6 @@ public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output)
[Fact]
public async Task RunAsync()
{
- Console.WriteLine("======== Example71_AgentDelegation ========");
-
if (TestConfiguration.OpenAI.ApiKey is null)
{
Console.WriteLine("OpenAI apiKey not found. Skipping example.");
@@ -39,11 +31,11 @@ public async Task RunAsync()
try
{
- var plugin = KernelPluginFactory.CreateFromType();
+ var plugin = KernelPluginFactory.CreateFromType();
var menuAgent =
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml"))
.WithDescription("Answer questions about how the menu uses the tool.")
.WithPlugin(plugin)
@@ -52,14 +44,14 @@ public async Task RunAsync()
var parrotAgent =
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml"))
.BuildAsync());
var toolAgent =
Track(
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml"))
.WithPlugin(parrotAgent.AsPlugin())
.WithPlugin(menuAgent.AsPlugin())
diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs
index c75a5e403cea..00af8faab617 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs
@@ -1,8 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
+using Azure.AI.OpenAI;
using Microsoft.SemanticKernel.Experimental.Agents;
+using OpenAI;
+using OpenAI.Files;
using Resources;
namespace Agents;
@@ -13,21 +14,8 @@ namespace Agents;
///
public sealed class Legacy_AgentTools(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and parallel function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview";
-
- ///
- /// Flag to force usage of OpenAI configuration if both
- /// and are defined.
- /// If 'false', Azure takes precedence.
- ///
- ///
- /// NOTE: Retrieval tools is not currently available on Azure.
- ///
- private new const bool ForceOpenAI = true;
+ ///
+ protected override bool ForceOpenAI => true;
// Track agents for clean-up
private readonly List _agents = [];
@@ -79,14 +67,13 @@ public async Task RunRetrievalToolAsync()
return;
}
- Kernel kernel = CreateFileEnabledKernel();
-#pragma warning disable CS0618 // Type or member is obsolete
- var fileService = kernel.GetRequiredService();
- var result =
- await fileService.UploadContentAsync(
- new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"),
- new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants));
-#pragma warning restore CS0618 // Type or member is obsolete
+ FileClient fileClient = CreateFileClient();
+
+ OpenAIFileInfo result =
+ await fileClient.UploadFileAsync(
+ new BinaryData(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!),
+ "travelinfo.txt",
+ FileUploadPurpose.Assistants);
var fileId = result.Id;
Console.WriteLine($"! {fileId}");
@@ -112,7 +99,7 @@ await ChatAsync(
}
finally
{
- await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId)));
+ await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileClient.DeleteFileAsync(fileId)));
}
}
@@ -167,21 +154,21 @@ async Task InvokeAgentAsync(IAgent agent, string question)
}
}
- private static Kernel CreateFileEnabledKernel()
+ private FileClient CreateFileClient()
{
-#pragma warning disable CS0618 // Type or member is obsolete
- return
- ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
- Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() :
- throw new NotImplementedException("The file service is being deprecated and was not moved to AzureOpenAI connector.");
-#pragma warning restore CS0618 // Type or member is obsolete
+ OpenAIClient client =
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ new OpenAIClient(TestConfiguration.OpenAI.ApiKey) :
+ new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey);
+
+ return client.GetFileClient();
}
- private static AgentBuilder CreateAgentBuilder()
+ private AgentBuilder CreateAgentBuilder()
{
return
- ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
- new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) :
new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}
diff --git a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs
index 5af10987bb3a..31cc4926392b 100644
--- a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs
+++ b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs
@@ -13,19 +13,6 @@ namespace Agents;
///
public class Legacy_Agents(ITestOutputHelper output) : BaseTest(output)
{
- ///
- /// Specific model is required that supports agents and function calling.
- /// Currently this is limited to Open AI hosted services.
- ///
- private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106";
-
- ///
- /// Flag to force usage of OpenAI configuration if both
- /// and are defined.
- /// If 'false', Azure takes precedence.
- ///
- private new const bool ForceOpenAI = false;
-
///
/// Chat using the "Parrot" agent.
/// Tools/functions: None
@@ -61,18 +48,12 @@ public async Task RunWithMethodFunctionsAsync()
await ChatAsync(
"Agents.ToolAgent.yaml", // Defined under ./Resources/Agents
plugin,
- arguments: new() { { LegacyMenuPlugin.CorrelationIdArgument, 3.141592653 } },
+ arguments: null,
"Hello",
"What is the special soup?",
"What is the special drink?",
"Do you have enough soup for 5 orders?",
"Thank you!");
-
- Console.WriteLine("\nCorrelation Ids:");
- foreach (string correlationId in menuApi.CorrelationIds)
- {
- Console.WriteLine($"- {correlationId}");
- }
}
///
@@ -114,7 +95,7 @@ public async Task RunAsFunctionAsync()
// Create parrot agent, same as the other cases.
var agent =
await new AgentBuilder()
- .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey)
+ .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml"))
.BuildAsync();
@@ -187,11 +168,11 @@ await Task.WhenAll(
}
}
- private static AgentBuilder CreateAgentBuilder()
+ private AgentBuilder CreateAgentBuilder()
{
return
- ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
- new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) :
+ this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ?
+ new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) :
new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey);
}
}
diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs
index d3a894dd6c8e..21b19c1d342c 100644
--- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs
+++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs
@@ -10,7 +10,7 @@ namespace Agents;
/// Demonstrate that two different agent types are able to participate in the same conversation.
/// In this case a and participate.
///
-public class MixedChat_Agents(ITestOutputHelper output) : BaseTest(output)
+public class MixedChat_Agents(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
@@ -47,12 +47,12 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync()
OpenAIAssistantAgent agentWriter =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- definition: new()
+ clientProvider: this.GetClientProvider(),
+ definition: new(this.Model)
{
Instructions = CopyWriterInstructions,
Name = CopyWriterName,
- ModelId = this.Model,
+ Metadata = AssistantSampleMetadata,
});
// Create a chat for agent interaction.
@@ -76,16 +76,16 @@ await OpenAIAssistantAgent.CreateAsync(
};
// Invoke chat and display messages.
- string input = "concept: maps made out of egg cartons.";
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons.");
+ chat.AddChatMessage(input);
+ this.WriteAgentChatMessage(input);
- await foreach (ChatMessageContent content in chat.InvokeAsync())
+ await foreach (ChatMessageContent response in chat.InvokeAsync())
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
- Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]");
}
private sealed class ApprovalTerminationStrategy : TerminationStrategy
diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs
index b95c6efca36d..0219c25f7712 100644
--- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs
+++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs
@@ -1,10 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
+using OpenAI.Files;
using Resources;
namespace Agents;
@@ -13,25 +12,22 @@ namespace Agents;
/// Demonstrate agent interacts with
/// when it produces file output.
///
-public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output)
+public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output)
{
- ///
- /// Target OpenAI services.
- ///
- protected override bool ForceOpenAI => true;
-
private const string SummaryInstructions = "Summarize the entire conversation for the user in natural language.";
[Fact]
public async Task AnalyzeFileAndGenerateReportAsync()
{
-#pragma warning disable CS0618 // Type or member is obsolete
- OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey);
+ OpenAIClientProvider provider = this.GetClientProvider();
+
+ FileClient fileClient = provider.Client.GetFileClient();
- OpenAIFileReference uploadFile =
- await fileService.UploadContentAsync(
- new BinaryContent(await EmbeddedResource.ReadAllAsync("30-user-context.txt"), mimeType: "text/plain"),
- new OpenAIFileUploadExecutionSettings("30-user-context.txt", OpenAIFilePurpose.Assistants));
+ OpenAIFileInfo uploadFile =
+ await fileClient.UploadFileAsync(
+ new BinaryData(await EmbeddedResource.ReadAllAsync("30-user-context.txt")),
+ "30-user-context.txt",
+ FileUploadPurpose.Assistants);
Console.WriteLine(this.ApiKey);
@@ -39,12 +35,12 @@ await fileService.UploadContentAsync(
OpenAIAssistantAgent analystAgent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ provider,
+ new(this.Model)
{
- EnableCodeInterpreter = true, // Enable code-interpreter
- ModelId = this.Model,
- FileIds = [uploadFile.Id] // Associate uploaded file with assistant
+ EnableCodeInterpreter = true,
+ CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant code-interpreter
+ Metadata = AssistantSampleMetadata,
});
ChatCompletionAgent summaryAgent =
@@ -71,7 +67,7 @@ Create a tab delimited file report of the ordered (descending) frequency distrib
finally
{
await analystAgent.DeleteAsync();
- await fileService.DeleteFileAsync(uploadFile.Id);
+ await fileClient.DeleteFileAsync(uploadFile.Id);
}
// Local function to invoke agent and display the conversation messages.
@@ -79,23 +75,16 @@ async Task InvokeAgentAsync(Agent agent, string? input = null)
{
if (!string.IsNullOrWhiteSpace(input))
{
+ ChatMessageContent message = new(AuthorRole.User, input);
chat.AddChatMessage(new(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ this.WriteAgentChatMessage(message);
}
- await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- Console.WriteLine($"\n# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
-
- foreach (AnnotationContent annotation in content.Items.OfType())
- {
- Console.WriteLine($"\t* '{annotation.Quote}' => {annotation.FileId}");
- BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!);
- byte[] byteContent = fileContent.Data?.ToArray() ?? [];
- Console.WriteLine($"\n{Encoding.Default.GetString(byteContent)}");
- }
+ this.WriteAgentChatMessage(response);
+ await this.DownloadResponseContentAsync(fileClient, response);
}
}
-#pragma warning restore CS0618 // Type or member is obsolete
}
}
diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs
index 36b96fc4be54..142706e8506c 100644
--- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs
+++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs
@@ -3,7 +3,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
+using OpenAI.Files;
namespace Agents;
@@ -11,13 +11,8 @@ namespace Agents;
/// Demonstrate agent interacts with
/// when it produces image output.
///
-public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output)
+public class MixedChat_Images(ITestOutputHelper output) : BaseAgentsTest(output)
{
- ///
- /// Target OpenAI services.
- ///
- protected override bool ForceOpenAI => true;
-
private const string AnalystName = "Analyst";
private const string AnalystInstructions = "Create charts as requested without explanation.";
@@ -27,20 +22,21 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output)
[Fact]
public async Task AnalyzeDataAndGenerateChartAsync()
{
-#pragma warning disable CS0618 // Type or member is obsolete
- OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey);
+ OpenAIClientProvider provider = this.GetClientProvider();
+
+ FileClient fileClient = provider.Client.GetFileClient();
// Define the agents
OpenAIAssistantAgent analystAgent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ provider,
+ new(this.Model)
{
Instructions = AnalystInstructions,
Name = AnalystName,
EnableCodeInterpreter = true,
- ModelId = this.Model,
+ Metadata = AssistantSampleMetadata,
});
ChatCompletionAgent summaryAgent =
@@ -87,28 +83,16 @@ async Task InvokeAgentAsync(Agent agent, string? input = null)
{
if (!string.IsNullOrWhiteSpace(input))
{
+ ChatMessageContent message = new(AuthorRole.User, input);
chat.AddChatMessage(new(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ this.WriteAgentChatMessage(message);
}
- await foreach (ChatMessageContent message in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- if (!string.IsNullOrWhiteSpace(message.Content))
- {
- Console.WriteLine($"\n# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'");
- }
-
- foreach (FileReferenceContent fileReference in message.Items.OfType())
- {
- Console.WriteLine($"\t* Generated image - @{fileReference.FileId}");
- BinaryContent fileContent = await fileService.GetFileContentAsync(fileReference.FileId!);
- byte[] byteContent = fileContent.Data?.ToArray() ?? [];
- string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png");
- await File.WriteAllBytesAsync($"{filePath}.png", byteContent);
- Console.WriteLine($"\t* Local path - {filePath}");
- }
+ this.WriteAgentChatMessage(response);
+ await this.DownloadResponseImageAsync(fileClient, response);
}
}
-#pragma warning restore CS0618 // Type or member is obsolete
}
}
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs
index ef5ba80154fa..cd81f7c4d187 100644
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs
+++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs
@@ -3,6 +3,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Files;
namespace Agents;
@@ -10,30 +11,29 @@ namespace Agents;
/// Demonstrate using code-interpreter with to
/// produce image content displays the requested charts.
///
-public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(output)
+public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseAgentsTest(output)
{
- ///
- /// Target Open AI services.
- ///
- protected override bool ForceOpenAI => true;
-
private const string AgentName = "ChartMaker";
private const string AgentInstructions = "Create charts as requested without explanation.";
[Fact]
public async Task GenerateChartWithOpenAIAssistantAgentAsync()
{
+ OpenAIClientProvider provider = this.GetClientProvider();
+
+ FileClient fileClient = provider.Client.GetFileClient();
+
// Define the agent
OpenAIAssistantAgent agent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ provider,
+ new(this.Model)
{
Instructions = AgentInstructions,
Name = AgentName,
EnableCodeInterpreter = true,
- ModelId = this.Model,
+ Metadata = AssistantSampleMetadata,
});
// Create a chat for agent interaction.
@@ -55,6 +55,7 @@ Sum 426 1622 856 2904
""");
await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?");
+ await InvokeAgentAsync("Perfect, can you regenerate this as a line chart?");
}
finally
{
@@ -64,21 +65,14 @@ Sum 426 1622 856 2904
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.AddChatMessage(new(AuthorRole.User, input));
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent message in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- if (!string.IsNullOrWhiteSpace(message.Content))
- {
- Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'");
- }
-
- foreach (FileReferenceContent fileReference in message.Items.OfType())
- {
- Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}");
- }
+ this.WriteAgentChatMessage(response);
+ await this.DownloadResponseImageAsync(fileClient, response);
}
}
}
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs
index f99130790eef..dc4af2ad2743 100644
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs
+++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs
@@ -1,10 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
-using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
+using OpenAI.Files;
using Resources;
namespace Agents;
@@ -12,38 +11,31 @@ namespace Agents;
///
/// Demonstrate using code-interpreter to manipulate and generate csv files with .
///
-public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTest(output)
+public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseAgentsTest(output)
{
- ///
- /// Target OpenAI services.
- ///
- protected override bool ForceOpenAI => true;
-
[Fact]
public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync()
{
-#pragma warning disable CS0618 // Type or member is obsolete
- OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey);
-
- OpenAIFileReference uploadFile =
- await fileService.UploadContentAsync(
- new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"),
- new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants));
+ OpenAIClientProvider provider = this.GetClientProvider();
-#pragma warning restore CS0618 // Type or member is obsolete
+ FileClient fileClient = provider.Client.GetFileClient();
- Console.WriteLine(this.ApiKey);
+ OpenAIFileInfo uploadFile =
+ await fileClient.UploadFileAsync(
+ new BinaryData(await EmbeddedResource.ReadAllAsync("sales.csv")!),
+ "sales.csv",
+ FileUploadPurpose.Assistants);
// Define the agent
OpenAIAssistantAgent agent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ provider,
+ new(this.Model)
{
- EnableCodeInterpreter = true, // Enable code-interpreter
- ModelId = this.Model,
- FileIds = [uploadFile.Id] // Associate uploaded file
+ EnableCodeInterpreter = true,
+ CodeInterpreterFileIds = [uploadFile.Id],
+ Metadata = AssistantSampleMetadata,
});
// Create a chat for agent interaction.
@@ -59,27 +51,20 @@ await OpenAIAssistantAgent.CreateAsync(
finally
{
await agent.DeleteAsync();
- await fileService.DeleteFileAsync(uploadFile.Id);
+ await fileClient.DeleteFileAsync(uploadFile.Id);
}
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.AddChatMessage(new(AuthorRole.User, input));
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
-
- foreach (AnnotationContent annotation in content.Items.OfType())
- {
- Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}");
- BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!);
- byte[] byteContent = fileContent.Data?.ToArray() ?? [];
- Console.WriteLine(Encoding.Default.GetString(byteContent));
- }
+ this.WriteAgentChatMessage(response);
+ await this.DownloadResponseContentAsync(fileClient, response);
}
}
}
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs
index 38bac46f648a..a8f31622c753 100644
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs
+++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs
@@ -28,7 +28,7 @@ public async Task UploadAndRetrieveFilesAsync()
new BinaryContent(data: await EmbeddedResource.ReadAllAsync("travelinfo.txt"), mimeType: "text/plain") { InnerContent = "travelinfo.txt" }
];
- var fileContents = new Dictionary();
+ Dictionary fileContents = new();
foreach (BinaryContent file in files)
{
OpenAIFileReference result = await fileService.UploadContentAsync(file, new(file.InnerContent!.ToString()!, OpenAIFilePurpose.FineTune));
@@ -49,7 +49,7 @@ public async Task UploadAndRetrieveFilesAsync()
string? fileName = fileContents[fileReference.Id].InnerContent!.ToString();
ReadOnlyMemory data = content.Data ?? new();
- var typedContent = mimeType switch
+ BinaryContent typedContent = mimeType switch
{
"image/jpeg" => new ImageContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata },
"audio/wav" => new AudioContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata },
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs
deleted file mode 100644
index 71acf3db0e85..000000000000
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.Agents.OpenAI;
-using Microsoft.SemanticKernel.ChatCompletion;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
-using Resources;
-
-namespace Agents;
-
-///
-/// Demonstrate using retrieval on .
-///
-public class OpenAIAssistant_Retrieval(ITestOutputHelper output) : BaseTest(output)
-{
- ///
- /// Retrieval tool not supported on Azure OpenAI.
- ///
- protected override bool ForceOpenAI => true;
-
- [Fact]
- public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync()
- {
-#pragma warning disable CS0618 // Type or member is obsolete
- OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey);
-
- OpenAIFileReference uploadFile =
- await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"),
- new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants));
-#pragma warning restore CS0618 // Type or member is obsolete
- // Define the agent
- OpenAIAssistantAgent agent =
- await OpenAIAssistantAgent.CreateAsync(
- kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
- {
- EnableRetrieval = true, // Enable retrieval
- ModelId = this.Model,
- FileIds = [uploadFile.Id] // Associate uploaded file
- });
-
- // Create a chat for agent interaction.
- AgentGroupChat chat = new();
-
- // Respond to user input
- try
- {
- await InvokeAgentAsync("Where did sam go?");
- await InvokeAgentAsync("When does the flight leave Seattle?");
- await InvokeAgentAsync("What is the hotel contact info at the destination?");
- }
- finally
- {
- await agent.DeleteAsync();
- }
-
- // Local function to invoke agent and display the conversation messages.
- async Task InvokeAgentAsync(string input)
- {
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
-
- await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
- {
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
- }
- }
- }
-}
diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj
index 89ac1452713a..aa303046bd36 100644
--- a/dotnet/samples/Concepts/Concepts.csproj
+++ b/dotnet/samples/Concepts/Concepts.csproj
@@ -8,7 +8,7 @@
false
true
- $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110
+ $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001
Library
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -41,7 +41,10 @@
-
+
+
+ true
+
@@ -109,5 +112,8 @@
Always
+
+ Always
+
diff --git a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs
index 7111e873cf4c..c383ea9025f1 100644
--- a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs
+++ b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs
@@ -7,12 +7,6 @@ namespace Plugins;
public sealed class LegacyMenuPlugin
{
- public const string CorrelationIdArgument = "correlationId";
-
- private readonly List _correlationIds = [];
-
- public IReadOnlyList CorrelationIds => this._correlationIds;
-
///
/// Returns a mock item menu.
///
@@ -20,8 +14,6 @@ public sealed class LegacyMenuPlugin
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
public string[] GetSpecials(KernelArguments? arguments)
{
- CaptureCorrelationId(arguments, nameof(GetSpecials));
-
return
[
"Special Soup: Clam Chowder",
@@ -39,8 +31,6 @@ public string GetItemPrice(
string menuItem,
KernelArguments? arguments)
{
- CaptureCorrelationId(arguments, nameof(GetItemPrice));
-
return "$9.99";
}
@@ -55,21 +45,6 @@ public bool IsItem86d(
int count,
KernelArguments? arguments)
{
- CaptureCorrelationId(arguments, nameof(IsItem86d));
-
return count < 3;
}
-
- private void CaptureCorrelationId(KernelArguments? arguments, string scope)
- {
- if (arguments?.TryGetValue(CorrelationIdArgument, out object? correlationId) ?? false)
- {
- string? correlationText = correlationId?.ToString();
-
- if (!string.IsNullOrWhiteSpace(correlationText))
- {
- this._correlationIds.Add($"{scope}:{correlationText}");
- }
- }
- }
}
diff --git a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs
deleted file mode 100644
index be82177eda5d..000000000000
--- a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs
+++ /dev/null
@@ -1,34 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-
-using System.ComponentModel;
-using Microsoft.SemanticKernel;
-
-namespace Plugins;
-
-public sealed class MenuPlugin
-{
- public const string CorrelationIdArgument = "correlationId";
-
- private readonly List _correlationIds = [];
-
- public IReadOnlyList CorrelationIds => this._correlationIds;
-
- [KernelFunction, Description("Provides a list of specials from the menu.")]
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
- public string GetSpecials()
- {
- return @"
-Special Soup: Clam Chowder
-Special Salad: Cobb Salad
-Special Drink: Chai Tea
-";
- }
-
- [KernelFunction, Description("Provides the price of the requested menu item.")]
- public string GetItemPrice(
- [Description("The name of the menu item.")]
- string menuItem)
- {
- return "$9.99";
- }
-}
diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
index decbe920b28b..df9e025b678f 100644
--- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
+++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj
@@ -9,7 +9,7 @@
true
- $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110
+ $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001
Library
5ee045b0-aea3-4f08-8d31-32d1a6f8fed0
@@ -32,12 +32,16 @@
-
+
+
+ true
+
+
@@ -47,4 +51,14 @@
+
+
+ Always
+
+
+
+
+
+
+
diff --git a/dotnet/samples/GettingStartedWithAgents/README.md b/dotnet/samples/GettingStartedWithAgents/README.md
index 39952506548c..ed0e68802994 100644
--- a/dotnet/samples/GettingStartedWithAgents/README.md
+++ b/dotnet/samples/GettingStartedWithAgents/README.md
@@ -19,13 +19,17 @@ The getting started with agents examples include:
Example|Description
---|---
-[Step1_Agent](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs)|How to create and use an agent.
-[Step2_Plugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs)|How to associate plug-ins with an agent.
-[Step3_Chat](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs)|How to create a conversation between agents.
-[Step4_KernelFunctionStrategies](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs)|How to utilize a `KernelFunction` as a _chat strategy_.
-[Step5_JsonResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs)|How to have an agent produce JSON.
-[Step6_DependencyInjection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs)|How to define dependency injection patterns for agents.
-[Step7_OpenAIAssistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step7_OpenAIAssistant.cs)|How to create an Open AI Assistant agent.
+[Step01_Agent](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs)|How to create and use an agent.
+[Step02_Plugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs)|How to associate plug-ins with an agent.
+[Step03_Chat](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs)|How to create a conversation between agents.
+[Step04_KernelFunctionStrategies](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs)|How to utilize a `KernelFunction` as a _chat strategy_.
+[Step05_JsonResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs)|How to have an agent produce JSON.
+[Step06_DependencyInjection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs)|How to define dependency injection patterns for agents.
+[Step07_Logging](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs)|How to enable logging for agents.
+[Step08_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs)|How to create an Open AI Assistant agent.
+[Step09_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs)|How to provide an image as input to an Open AI Assistant agent.
+[Step10_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter_.cs)|How to use the code-interpreter tool for an Open AI Assistant agent.
+[Step11_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs)|How to use the file-search tool for an Open AI Assistant agent.
## Legacy Agents
diff --git a/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg b/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg
new file mode 100644
index 000000000000..1e9f26de48fc
Binary files /dev/null and b/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg differ
diff --git a/dotnet/samples/GettingStartedWithAgents/Resources/employees.pdf b/dotnet/samples/GettingStartedWithAgents/Resources/employees.pdf
new file mode 100644
index 000000000000..bba45f80a90b
Binary files /dev/null and b/dotnet/samples/GettingStartedWithAgents/Resources/employees.pdf differ
diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
similarity index 76%
rename from dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs
rename to dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
index d7d4a0471b01..bc5bee5249e5 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs
@@ -9,7 +9,7 @@ namespace GettingStarted;
/// Demonstrate creation of and
/// eliciting its response to three explicit user messages.
///
-public class Step1_Agent(ITestOutputHelper output) : BaseTest(output)
+public class Step01_Agent(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ParrotName = "Parrot";
private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound.";
@@ -37,15 +37,15 @@ public async Task UseSingleChatCompletionAgentAsync()
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.Add(new ChatMessageContent(AuthorRole.User, input));
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.Add(message);
+ this.WriteAgentChatMessage(message);
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
-
- await foreach (ChatMessageContent content in agent.InvokeAsync(chat))
+ await foreach (ChatMessageContent response in agent.InvokeAsync(chat))
{
- chat.Add(content);
+ chat.Add(response);
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
}
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
similarity index 76%
rename from dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs
rename to dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
index 7946adc7f687..29394991dcc4 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs
@@ -11,7 +11,7 @@ namespace GettingStarted;
/// Demonstrate creation of with a ,
/// and then eliciting its response to explicit user messages.
///
-public class Step2_Plugins(ITestOutputHelper output) : BaseTest(output)
+public class Step02_Plugins(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string HostName = "Host";
private const string HostInstructions = "Answer questions about the menu.";
@@ -45,37 +45,34 @@ public async Task UseChatCompletionWithPluginAgentAsync()
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.Add(new ChatMessageContent(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.Add(message);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in agent.InvokeAsync(chat))
+ await foreach (ChatMessageContent response in agent.InvokeAsync(chat))
{
- chat.Add(content);
+ chat.Add(response);
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
}
}
- public sealed class MenuPlugin
+ private sealed class MenuPlugin
{
[KernelFunction, Description("Provides a list of specials from the menu.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
- public string GetSpecials()
- {
- return @"
-Special Soup: Clam Chowder
-Special Salad: Cobb Salad
-Special Drink: Chai Tea
-";
- }
+ public string GetSpecials() =>
+ """
+ Special Soup: Clam Chowder
+ Special Salad: Cobb Salad
+ Special Drink: Chai Tea
+ """;
[KernelFunction, Description("Provides the price of the requested menu item.")]
public string GetItemPrice(
[Description("The name of the menu item.")]
- string menuItem)
- {
- return "$9.99";
- }
+ string menuItem) =>
+ "$9.99";
}
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs
similarity index 86%
rename from dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs
rename to dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs
index 5d0c185f95f5..1ada85d512f3 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs
@@ -11,7 +11,7 @@ namespace GettingStarted;
/// that inform how chat proceeds with regards to: Agent selection, chat continuation, and maximum
/// number of agent interactions.
///
-public class Step3_Chat(ITestOutputHelper output) : BaseTest(output)
+public class Step03_Chat(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
@@ -74,16 +74,16 @@ public async Task UseAgentGroupChatWithTwoAgentsAsync()
};
// Invoke chat and display messages.
- string input = "concept: maps made out of egg cartons.";
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons.");
+ chat.AddChatMessage(input);
+ this.WriteAgentChatMessage(input);
- await foreach (ChatMessageContent content in chat.InvokeAsync())
+ await foreach (ChatMessageContent response in chat.InvokeAsync())
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
- Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]");
}
private sealed class ApprovalTerminationStrategy : TerminationStrategy
diff --git a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs
similarity index 84%
rename from dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs
rename to dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs
index 9cabe0193d3e..36424e6c268b 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs
@@ -10,7 +10,7 @@ namespace GettingStarted;
/// Demonstrate usage of and
/// to manage execution.
///
-public class Step4_KernelFunctionStrategies(ITestOutputHelper output) : BaseTest(output)
+public class Step04_KernelFunctionStrategies(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
@@ -64,17 +64,18 @@ public async Task UseKernelFunctionStrategiesWithAgentGroupChatAsync()
KernelFunction selectionFunction =
KernelFunctionFactory.CreateFromPrompt(
$$$"""
- Your job is to determine which participant takes the next turn in a conversation according to the action of the most recent participant.
+ Determine which participant takes the next turn in a conversation based on the the most recent participant.
State only the name of the participant to take the next turn.
+ No participant should take more than one turn in a row.
Choose only from these participants:
- {{{ReviewerName}}}
- {{{CopyWriterName}}}
Always follow these rules when selecting the next participant:
- - After user input, it is {{{CopyWriterName}}}'a turn.
- - After {{{CopyWriterName}}} replies, it is {{{ReviewerName}}}'s turn.
- - After {{{ReviewerName}}} provides feedback, it is {{{CopyWriterName}}}'s turn.
+ - After user input, it is {{{CopyWriterName}}}'s turn.
+ - After {{{CopyWriterName}}}, it is {{{ReviewerName}}}'s turn.
+ - After {{{ReviewerName}}}, it is {{{CopyWriterName}}}'s turn.
History:
{{$history}}
@@ -116,15 +117,15 @@ State only the name of the participant to take the next turn.
};
// Invoke chat and display messages.
- string input = "concept: maps made out of egg cartons.";
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, "concept: maps made out of egg cartons.");
+ chat.AddChatMessage(message);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in chat.InvokeAsync())
+ await foreach (ChatMessageContent responese in chat.InvokeAsync())
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(responese);
}
- Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]");
}
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs
similarity index 79%
rename from dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs
rename to dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs
index 20ad4c2096d4..8806c7d3b62d 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs
@@ -10,14 +10,14 @@ namespace GettingStarted;
///
/// Demonstrate parsing JSON response.
///
-public class Step5_JsonResult(ITestOutputHelper output) : BaseTest(output)
+public class Step05_JsonResult(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const int ScoreCompletionThreshold = 70;
private const string TutorName = "Tutor";
private const string TutorInstructions =
"""
- Think step-by-step and rate the user input on creativity and expressivness from 1-100.
+ Think step-by-step and rate the user input on creativity and expressiveness from 1-100.
Respond in JSON format with the following JSON schema:
@@ -60,19 +60,20 @@ public async Task UseKernelFunctionStrategiesWithJsonResultAsync()
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
+ ChatMessageContent message = new(AuthorRole.User, input);
+ chat.AddChatMessage(message);
+ this.WriteAgentChatMessage(message);
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
-
- await foreach (ChatMessageContent content in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
- Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ this.WriteAgentChatMessage(response);
+
+ Console.WriteLine($"[IS COMPLETED: {chat.IsComplete}]");
}
}
}
- private record struct InputScore(int score, string notes);
+ private record struct WritingScore(int score, string notes);
private sealed class ThresholdTerminationStrategy : TerminationStrategy
{
@@ -80,7 +81,7 @@ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyLi
{
string lastMessageContent = history[history.Count - 1].Content ?? string.Empty;
- InputScore? result = JsonResultTranslator.Translate(lastMessageContent);
+ WritingScore? result = JsonResultTranslator.Translate(lastMessageContent);
return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold);
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs
similarity index 65%
rename from dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs
rename to dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs
index 21af5db70dce..a0d32f8cefba 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs
@@ -3,23 +3,19 @@
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
-using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
-using Resources;
namespace GettingStarted;
///
/// Demonstrate creation of an agent via dependency injection.
///
-public class Step6_DependencyInjection(ITestOutputHelper output) : BaseTest(output)
+public class Step06_DependencyInjection(ITestOutputHelper output) : BaseAgentsTest(output)
{
- private const int ScoreCompletionThreshold = 70;
-
private const string TutorName = "Tutor";
private const string TutorInstructions =
"""
- Think step-by-step and rate the user input on creativity and expressivness from 1-100.
+ Think step-by-step and rate the user input on creativity and expressiveness from 1-100.
Respond in JSON format with the following JSON schema:
@@ -80,50 +76,27 @@ public async Task UseDependencyInjectionToCreateAgentAsync()
// Local function to invoke agent and display the conversation messages.
async Task WriteAgentResponse(string input)
{
- Console.WriteLine($"# {AuthorRole.User}: {input}");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ this.WriteAgentChatMessage(message);
- await foreach (ChatMessageContent content in agentClient.RunDemoAsync(input))
+ await foreach (ChatMessageContent response in agentClient.RunDemoAsync(message))
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
}
}
private sealed class AgentClient([FromKeyedServices(TutorName)] ChatCompletionAgent agent)
{
- private readonly AgentGroupChat _chat =
- new()
- {
- ExecutionSettings =
- new()
- {
- // Here a TerminationStrategy subclass is used that will terminate when
- // the response includes a score that is greater than or equal to 70.
- TerminationStrategy = new ThresholdTerminationStrategy()
- }
- };
-
- public IAsyncEnumerable RunDemoAsync(string input)
- {
- // Create a chat for agent interaction.
+ private readonly AgentGroupChat _chat = new();
- this._chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
+ public IAsyncEnumerable RunDemoAsync(ChatMessageContent input)
+ {
+ this._chat.AddChatMessage(input);
return this._chat.InvokeAsync(agent);
}
}
- private record struct InputScore(int score, string notes);
-
- private sealed class ThresholdTerminationStrategy : TerminationStrategy
- {
- protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken)
- {
- string lastMessageContent = history[history.Count - 1].Content ?? string.Empty;
-
- InputScore? result = JsonResultTranslator.Translate(lastMessageContent);
-
- return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold);
- }
- }
+ private record struct WritingScore(int score, string notes);
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs
similarity index 86%
rename from dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs
rename to dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs
index 1ab559e668fb..3a48d407dea9 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs
@@ -8,13 +8,13 @@
namespace GettingStarted;
///
-/// A repeat of with logging enabled via assignment
+/// A repeat of with logging enabled via assignment
/// of a to .
///
///
/// Samples become super noisy with logging always enabled.
///
-public class Step7_Logging(ITestOutputHelper output) : BaseTest(output)
+public class Step07_Logging(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
@@ -81,16 +81,16 @@ public async Task UseLoggerFactoryWithAgentGroupChatAsync()
};
// Invoke chat and display messages.
- string input = "concept: maps made out of egg cartons.";
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons.");
+ chat.AddChatMessage(input);
+ this.WriteAgentChatMessage(input);
- await foreach (ChatMessageContent content in chat.InvokeAsync())
+ await foreach (ChatMessageContent response in chat.InvokeAsync())
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
- Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}");
+ Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]");
}
private sealed class ApprovalTerminationStrategy : TerminationStrategy
diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs
similarity index 57%
rename from dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs
rename to dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs
index d9e9760e3fa6..ba4ab065c2a6 100644
--- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs
@@ -8,36 +8,35 @@
namespace GettingStarted;
///
-/// This example demonstrates that outside of initialization (and cleanup), using
-/// is no different from
-/// even with with a .
+/// This example demonstrates similarity between using
+/// and (see: Step 2).
///
-public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output)
+public class Step08_Assistant(ITestOutputHelper output) : BaseAgentsTest(output)
{
private const string HostName = "Host";
private const string HostInstructions = "Answer questions about the menu.";
[Fact]
- public async Task UseSingleOpenAIAssistantAgentAsync()
+ public async Task UseSingleAssistantAgentAsync()
{
// Define the agent
OpenAIAssistantAgent agent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ clientProvider: this.GetClientProvider(),
+ new(this.Model)
{
Instructions = HostInstructions,
Name = HostName,
- ModelId = this.Model,
+ Metadata = AssistantSampleMetadata,
});
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
agent.Kernel.Plugins.Add(plugin);
- // Create a thread for the agent interaction.
- string threadId = await agent.CreateThreadAsync();
+ // Create a thread for the agent conversation.
+ string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata });
// Respond to user input
try
@@ -56,45 +55,32 @@ await OpenAIAssistantAgent.CreateAsync(
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- await agent.AddChatMessageAsync(threadId, new ChatMessageContent(AuthorRole.User, input));
+ ChatMessageContent message = new(AuthorRole.User, input);
+ await agent.AddChatMessageAsync(threadId, message);
+ this.WriteAgentChatMessage(message);
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
-
- await foreach (ChatMessageContent content in agent.InvokeAsync(threadId))
+ await foreach (ChatMessageContent response in agent.InvokeAsync(threadId))
{
- if (content.Role != AuthorRole.Tool)
- {
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
- }
+ this.WriteAgentChatMessage(response);
}
}
}
private sealed class MenuPlugin
{
- public const string CorrelationIdArgument = "correlationId";
-
- private readonly List _correlationIds = [];
-
- public IReadOnlyList CorrelationIds => this._correlationIds;
-
[KernelFunction, Description("Provides a list of specials from the menu.")]
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
- public string GetSpecials()
- {
- return @"
-Special Soup: Clam Chowder
-Special Salad: Cobb Salad
-Special Drink: Chai Tea
-";
- }
+ public string GetSpecials() =>
+ """
+ Special Soup: Clam Chowder
+ Special Salad: Cobb Salad
+ Special Drink: Chai Tea
+ """;
[KernelFunction, Description("Provides the price of the requested menu item.")]
public string GetItemPrice(
[Description("The name of the menu item.")]
- string menuItem)
- {
- return "$9.99";
- }
+ string menuItem) =>
+ "$9.99";
}
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs
new file mode 100644
index 000000000000..62845f2c4366
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs
@@ -0,0 +1,74 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Resources;
+
+namespace GettingStarted;
+
+///
+/// Demonstrate providing image input to .
+///
+public class Step09_Assistant_Vision(ITestOutputHelper output) : BaseAgentsTest(output)
+{
+ ///
+ /// Azure currently only supports message of type=text.
+ ///
+ protected override bool ForceOpenAI => true;
+
+ [Fact]
+ public async Task UseSingleAssistantAgentAsync()
+ {
+ // Define the agent
+ OpenAIClientProvider provider = this.GetClientProvider();
+ OpenAIAssistantAgent agent =
+ await OpenAIAssistantAgent.CreateAsync(
+ kernel: new(),
+ provider,
+ new(this.Model)
+ {
+ Metadata = AssistantSampleMetadata,
+ });
+
+ // Upload an image
+ await using Stream imageStream = EmbeddedResource.ReadStream("cat.jpg")!;
+ string fileId = await agent.UploadFileAsync(imageStream, "cat.jpg");
+
+ // Create a thread for the agent conversation.
+ string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata });
+
+ // Respond to user input
+ try
+ {
+ // Refer to public image by url
+ await InvokeAgentAsync(CreateMessageWithImageUrl("Describe this image.", "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/New_york_times_square-terabass.jpg/1200px-New_york_times_square-terabass.jpg"));
+ await InvokeAgentAsync(CreateMessageWithImageUrl("What are is the main color in this image?", "https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg"));
+ // Refer to uploaded image by file-id.
+ await InvokeAgentAsync(CreateMessageWithImageReference("Is there an animal in this image?", fileId));
+ }
+ finally
+ {
+ await agent.DeleteThreadAsync(threadId);
+ await agent.DeleteAsync();
+ await provider.Client.GetFileClient().DeleteFileAsync(fileId);
+ }
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(ChatMessageContent message)
+ {
+ await agent.AddChatMessageAsync(threadId, message);
+ this.WriteAgentChatMessage(message);
+
+ await foreach (ChatMessageContent response in agent.InvokeAsync(threadId))
+ {
+ this.WriteAgentChatMessage(response);
+ }
+ }
+ }
+
+ private ChatMessageContent CreateMessageWithImageUrl(string input, string url)
+ => new(AuthorRole.User, [new TextContent(input), new ImageContent(new Uri(url))]);
+
+ private ChatMessageContent CreateMessageWithImageReference(string input, string fileId)
+ => new(AuthorRole.User, [new TextContent(input), new FileReferenceContent(fileId)]);
+}
diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs
similarity index 50%
rename from dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs
rename to dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs
index 75b237489025..1205771d66be 100644
--- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs
+++ b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs
@@ -1,34 +1,31 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
-namespace Agents;
+namespace GettingStarted;
///
/// Demonstrate using code-interpreter on .
///
-public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output)
+public class Step10_AssistantTool_CodeInterpreter(ITestOutputHelper output) : BaseAgentsTest(output)
{
- protected override bool ForceOpenAI => true;
-
[Fact]
- public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync()
+ public async Task UseCodeInterpreterToolWithAssistantAgentAsync()
{
// Define the agent
OpenAIAssistantAgent agent =
await OpenAIAssistantAgent.CreateAsync(
kernel: new(),
- config: new(this.ApiKey, this.Endpoint),
- new()
+ clientProvider: this.GetClientProvider(),
+ new(this.Model)
{
- EnableCodeInterpreter = true, // Enable code-interpreter
- ModelId = this.Model,
+ EnableCodeInterpreter = true,
+ Metadata = AssistantSampleMetadata,
});
- // Create a chat for agent interaction.
- AgentGroupChat chat = new();
+ // Create a thread for the agent conversation.
+ string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata });
// Respond to user input
try
@@ -37,19 +34,20 @@ await OpenAIAssistantAgent.CreateAsync(
}
finally
{
+ await agent.DeleteThreadAsync(threadId);
await agent.DeleteAsync();
}
// Local function to invoke agent and display the conversation messages.
async Task InvokeAgentAsync(string input)
{
- chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input));
-
- Console.WriteLine($"# {AuthorRole.User}: '{input}'");
+ ChatMessageContent message = new(AuthorRole.User, input);
+ await agent.AddChatMessageAsync(threadId, message);
+ this.WriteAgentChatMessage(message);
- await foreach (var content in chat.InvokeAsync(agent))
+ await foreach (ChatMessageContent response in agent.InvokeAsync(threadId))
{
- Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'");
+ this.WriteAgentChatMessage(response);
}
}
}
diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs
new file mode 100644
index 000000000000..d34cadaf3707
--- /dev/null
+++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs
@@ -0,0 +1,83 @@
+// Copyright (c) Microsoft. All rights reserved.
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Files;
+using OpenAI.VectorStores;
+using Resources;
+
+namespace GettingStarted;
+
+///
+/// Demonstrate using code-interpreter on .
+///
+public class Step11_AssistantTool_FileSearch(ITestOutputHelper output) : BaseAgentsTest(output)
+{
+ [Fact]
+ public async Task UseFileSearchToolWithAssistantAgentAsync()
+ {
+ // Define the agent
+ OpenAIClientProvider provider = this.GetClientProvider();
+ OpenAIAssistantAgent agent =
+ await OpenAIAssistantAgent.CreateAsync(
+ kernel: new(),
+ clientProvider: this.GetClientProvider(),
+ new(this.Model)
+ {
+ EnableFileSearch = true,
+ Metadata = AssistantSampleMetadata,
+ });
+
+ // Upload file - Using a table of fictional employees.
+ FileClient fileClient = provider.Client.GetFileClient();
+ await using Stream stream = EmbeddedResource.ReadStream("employees.pdf")!;
+ OpenAIFileInfo fileInfo = await fileClient.UploadFileAsync(stream, "employees.pdf", FileUploadPurpose.Assistants);
+
+ // Create a vector-store
+ VectorStoreClient vectorStoreClient = provider.Client.GetVectorStoreClient();
+ VectorStore vectorStore =
+ await vectorStoreClient.CreateVectorStoreAsync(
+ new VectorStoreCreationOptions()
+ {
+ FileIds = [fileInfo.Id],
+ Metadata = { { AssistantSampleMetadataKey, bool.TrueString } }
+ });
+
+ // Create a thread associated with a vector-store for the agent conversation.
+ string threadId =
+ await agent.CreateThreadAsync(
+ new OpenAIThreadCreationOptions
+ {
+ VectorStoreId = vectorStore.Id,
+ Metadata = AssistantSampleMetadata,
+ });
+
+ // Respond to user input
+ try
+ {
+ await InvokeAgentAsync("Who is the youngest employee?");
+ await InvokeAgentAsync("Who works in sales?");
+ await InvokeAgentAsync("I have a customer request, who can help me?");
+ }
+ finally
+ {
+ await agent.DeleteThreadAsync(threadId);
+ await agent.DeleteAsync();
+ await vectorStoreClient.DeleteVectorStoreAsync(vectorStore);
+ await fileClient.DeleteFileAsync(fileInfo);
+ }
+
+ // Local function to invoke agent and display the conversation messages.
+ async Task InvokeAgentAsync(string input)
+ {
+ ChatMessageContent message = new(AuthorRole.User, input);
+ await agent.AddChatMessageAsync(threadId, message);
+ this.WriteAgentChatMessage(message);
+
+ await foreach (ChatMessageContent response in agent.InvokeAsync(threadId))
+ {
+ this.WriteAgentChatMessage(response);
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs
index 9788464a2adb..73469ed723b5 100644
--- a/dotnet/src/Agents/Abstractions/AgentChannel.cs
+++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs
@@ -31,6 +31,10 @@ public abstract class AgentChannel
/// The agent actively interacting with the chat.
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
+ ///
+ /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user.
+ /// Example of a non-visible message is function-content for functions that are automatically executed.
+ ///
protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
Agent agent,
CancellationToken cancellationToken = default);
@@ -59,6 +63,10 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent
/// The agent actively interacting with the chat.
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
+ ///
+ /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user.
+ /// Example of a non-visible message is function-content for functions that are automatically executed.
+ ///
protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
TAgent agent,
CancellationToken cancellationToken = default);
diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs
index f4654963444e..ca6cbdaab259 100644
--- a/dotnet/src/Agents/Abstractions/AgentChat.cs
+++ b/dotnet/src/Agents/Abstractions/AgentChat.cs
@@ -285,7 +285,7 @@ private void ClearActivitySignal()
/// The activity signal is used to manage ability and visibility for taking actions based
/// on conversation history.
///
- private void SetActivityOrThrow()
+ protected void SetActivityOrThrow()
{
// Note: Interlocked is the absolute lightest synchronization mechanism available in dotnet.
int wasActive = Interlocked.CompareExchange(ref this._isActive, 1, 0);
diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs
index 73561a4eba8b..0c6bc252891d 100644
--- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs
+++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs
@@ -13,11 +13,13 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel
protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken = default)
{
return this._chat.GetChatMessagesAsync(cancellationToken);
}
+ ///
protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(AggregatorAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default)
{
ChatMessageContent? lastMessage = null;
@@ -47,6 +49,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync
}
}
+ ///
protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default)
{
// Always receive the initial history from the owning chat.
diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs
index 314d68ce8cd8..b971fe2ce8d4 100644
--- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs
+++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs
@@ -61,7 +61,7 @@ public static partial void LogAgentChatAddingMessages(
[LoggerMessage(
EventId = 0,
Level = LogLevel.Information,
- Message = "[{MethodName}] Adding Messages: {MessageCount}.")]
+ Message = "[{MethodName}] Added Messages: {MessageCount}.")]
public static partial void LogAgentChatAddedMessages(
this ILogger logger,
string methodName,
diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
index 3423308325c2..91f5b864e725 100644
--- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs
+++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs
@@ -38,7 +38,7 @@ public async IAsyncEnumerable InvokeAsync(
kernel ??= this.Kernel;
arguments ??= this.Arguments;
- (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments);
+ (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments);
ChatHistory chat = this.SetupAgentChatHistory(history);
@@ -65,7 +65,7 @@ await chatCompletionService.GetChatMessageContentsAsync(
history.Add(message);
}
- foreach (ChatMessageContent message in messages ?? [])
+ foreach (ChatMessageContent message in messages)
{
message.AuthorName = this.Name;
@@ -83,7 +83,7 @@ public async IAsyncEnumerable InvokeStreamingAsync(
kernel ??= this.Kernel;
arguments ??= this.Arguments;
- (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments);
+ (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments);
ChatHistory chat = this.SetupAgentChatHistory(history);
@@ -121,6 +121,9 @@ public async IAsyncEnumerable InvokeStreamingAsync(
///
protected override IEnumerable GetChannelKeys()
{
+ // Distinguish from other channel types.
+ yield return typeof(ChatHistoryChannel).FullName!;
+
// Agents with different reducers shall not share the same channel.
// Agents with the same or equivalent reducer shall share the same channel.
if (this.HistoryReducer != null)
@@ -145,7 +148,7 @@ protected override Task CreateChannelAsync(CancellationToken cance
return Task.FromResult(channel);
}
- private (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments)
+ internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments)
{
// Need to provide a KernelFunction to the service selector as a container for the execution-settings.
KernelFunction nullPrompt = KernelFunctionFactory.CreateFromPrompt("placeholder", arguments?.ExecutionSettings?.Values);
diff --git a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs
index a45bfa57011d..8c2f022830d1 100644
--- a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs
+++ b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs
@@ -80,7 +80,7 @@ Provide a concise and complete summarizion of the entire dialog that does not ex
IEnumerable summarizedHistory =
history.Extract(
this.UseSingleSummary ? 0 : insertionPoint,
- truncationIndex,
+ truncationIndex - 1,
(m) => m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent));
try
@@ -154,7 +154,9 @@ public override bool Equals(object? obj)
ChatHistorySummarizationReducer? other = obj as ChatHistorySummarizationReducer;
return other != null &&
this._thresholdCount == other._thresholdCount &&
- this._targetCount == other._targetCount;
+ this._targetCount == other._targetCount &&
+ this.UseSingleSummary == other.UseSingleSummary &&
+ string.Equals(this.SummarizationInstructions, other.SummarizationInstructions, StringComparison.Ordinal);
}
///
diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj
index 22db4073d90a..a5a4cde76d6f 100644
--- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj
+++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj
@@ -19,6 +19,7 @@
+
@@ -28,12 +29,11 @@
-
-
+
diff --git a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs
index cd4e80c3abf1..895482927515 100644
--- a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs
+++ b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs
@@ -1,6 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
-using Azure.AI.OpenAI.Assistants;
using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Assistants;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
index 9665fb680498..97a439729ff3 100644
--- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
+++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs
@@ -2,7 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using Azure.AI.OpenAI.Assistants;
+using OpenAI.Assistants;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
@@ -13,9 +13,8 @@ internal static class KernelFunctionExtensions
///
/// The source function
/// The plugin name
- /// The delimiter character
/// An OpenAI tool definition
- public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName, string delimiter)
+ public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName)
{
var metadata = function.Metadata;
if (metadata.Parameters.Count > 0)
@@ -47,10 +46,10 @@ public static FunctionToolDefinition ToToolDefinition(this KernelFunction functi
required,
};
- return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description, BinaryData.FromObjectAsJson(spec));
+ return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description, BinaryData.FromObjectAsJson(spec));
}
- return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description);
+ return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description);
}
private static string ConvertType(Type? type)
diff --git a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs
similarity index 87%
rename from dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs
rename to dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs
index 084e533fe757..d017fb403f23 100644
--- a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs
@@ -2,7 +2,7 @@
using Azure.Core;
using Azure.Core.Pipeline;
-namespace Microsoft.SemanticKernel.Agents.OpenAI.Azure;
+namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
///
/// Helper class to inject headers into Azure SDK HTTP pipeline
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs
new file mode 100644
index 000000000000..4c31a1bcf291
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using OpenAI.Assistants;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+
+///
+/// Factory for creating based on .
+/// Also able to produce .
+///
+///
+/// Improves testability.
+///
+internal static class AssistantMessageFactory
+{
+ ///
+ /// Produces based on .
+ ///
+ /// The message content.
+ public static MessageCreationOptions CreateOptions(ChatMessageContent message)
+ {
+ MessageCreationOptions options = new();
+
+ if (message.Metadata != null)
+ {
+ foreach (var metadata in message.Metadata)
+ {
+ options.Metadata.Add(metadata.Key, metadata.Value?.ToString() ?? string.Empty);
+ }
+ }
+
+ return options;
+ }
+
+ ///
+ /// Translates into enumeration of .
+ ///
+ /// The message content.
+ public static IEnumerable GetMessageContents(ChatMessageContent message)
+ {
+ foreach (KernelContent content in message.Items)
+ {
+ if (content is TextContent textContent)
+ {
+ yield return MessageContent.FromText(content.ToString());
+ }
+ else if (content is ImageContent imageContent)
+ {
+ if (imageContent.Uri != null)
+ {
+ yield return MessageContent.FromImageUrl(imageContent.Uri);
+ }
+ else if (string.IsNullOrWhiteSpace(imageContent.DataUri))
+ {
+ yield return MessageContent.FromImageUrl(new(imageContent.DataUri!));
+ }
+ }
+ else if (content is FileReferenceContent fileContent)
+ {
+ yield return MessageContent.FromImageFileId(fileContent.FileId);
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs
new file mode 100644
index 000000000000..981c646254af
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs
@@ -0,0 +1,53 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using OpenAI.Assistants;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+
+///
+/// Factory for creating definition.
+///
+///
+/// Improves testability.
+///
+internal static class AssistantRunOptionsFactory
+{
+ ///
+ /// Produce by reconciling and .
+ ///
+ /// The assistant definition
+ /// The run specific options
+ public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions)
+ {
+ int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount);
+
+ RunCreationOptions options =
+ new()
+ {
+ MaxCompletionTokens = ResolveExecutionSetting(invocationOptions?.MaxCompletionTokens, definition.ExecutionOptions?.MaxCompletionTokens),
+ MaxPromptTokens = ResolveExecutionSetting(invocationOptions?.MaxPromptTokens, definition.ExecutionOptions?.MaxPromptTokens),
+ ModelOverride = invocationOptions?.ModelName,
+ NucleusSamplingFactor = ResolveExecutionSetting(invocationOptions?.TopP, definition.TopP),
+ ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled),
+ ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null,
+ Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature),
+ TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null,
+ };
+
+ if (invocationOptions?.Metadata != null)
+ {
+ foreach (var metadata in invocationOptions.Metadata)
+ {
+ options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty);
+ }
+ }
+
+ return options;
+ }
+
+ private static TValue? ResolveExecutionSetting(TValue? setting, TValue? agentSetting) where TValue : struct
+ =>
+ setting.HasValue && (!agentSetting.HasValue || !EqualityComparer.Default.Equals(setting.Value, agentSetting.Value)) ?
+ setting.Value :
+ null;
+}
diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
similarity index 68%
rename from dotnet/src/Agents/OpenAI/AssistantThreadActions.cs
rename to dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
index cfc7a905cfc7..d66f54917d3f 100644
--- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs
@@ -7,19 +7,18 @@
using System.Threading;
using System.Threading.Tasks;
using Azure;
-using Azure.AI.OpenAI.Assistants;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI;
+using OpenAI.Assistants;
-namespace Microsoft.SemanticKernel.Agents.OpenAI;
+namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
///
/// Actions associated with an Open Assistant thread.
///
internal static class AssistantThreadActions
{
- private const string FunctionDelimiter = "-";
-
private static readonly HashSet s_pollingStatuses =
[
RunStatus.Queued,
@@ -34,6 +33,43 @@ internal static class AssistantThreadActions
RunStatus.Cancelled,
];
+ ///
+ /// Create a new assistant thread.
+ ///
+ /// The assistant client
+ /// The options for creating the thread
+ /// The to monitor for cancellation requests. The default is .
+ /// The thread identifier
+ public static async Task CreateThreadAsync(AssistantClient client, OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default)
+ {
+ ThreadCreationOptions createOptions =
+ new()
+ {
+ ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpreterFileIds),
+ };
+
+ if (options?.Messages is not null)
+ {
+ foreach (ChatMessageContent message in options.Messages)
+ {
+ ThreadInitializationMessage threadMessage = new(AssistantMessageFactory.GetMessageContents(message));
+ createOptions.InitialMessages.Add(threadMessage);
+ }
+ }
+
+ if (options?.Metadata != null)
+ {
+ foreach (KeyValuePair item in options.Metadata)
+ {
+ createOptions.Metadata[item.Key] = item.Value;
+ }
+ }
+
+ AssistantThread thread = await client.CreateThreadAsync(createOptions, cancellationToken).ConfigureAwait(false);
+
+ return thread.Id;
+ }
+
///
/// Create a message in the specified thread.
///
@@ -42,18 +78,20 @@ internal static class AssistantThreadActions
/// The message to add
/// The to monitor for cancellation requests. The default is .
/// if a system message is present, without taking any other action
- public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken)
+ public static async Task CreateMessageAsync(AssistantClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken)
{
if (message.Items.Any(i => i is FunctionCallContent))
{
return;
}
+ MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message);
+
await client.CreateMessageAsync(
threadId,
- message.Role.ToMessageRole(),
- message.Content,
- cancellationToken: cancellationToken).ConfigureAwait(false);
+ AssistantMessageFactory.GetMessageContents(message),
+ options,
+ cancellationToken).ConfigureAwait(false);
}
///
@@ -63,51 +101,45 @@ await client.CreateMessageAsync(
/// The thread identifier
/// The to monitor for cancellation requests. The default is .
/// Asynchronous enumeration of messages.
- public static async IAsyncEnumerable GetMessagesAsync(AssistantsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken)
+ public static async IAsyncEnumerable GetMessagesAsync(AssistantClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken)
{
Dictionary agentNames = []; // Cache agent names by their identifier
- PageableList messages;
-
- string? lastId = null;
- do
+ await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false))
{
- messages = await client.GetMessagesAsync(threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false);
- foreach (ThreadMessage message in messages)
+ AuthorRole role = new(message.Role.ToString());
+
+ string? assistantName = null;
+ if (!string.IsNullOrWhiteSpace(message.AssistantId) &&
+ !agentNames.TryGetValue(message.AssistantId, out assistantName))
{
- string? assistantName = null;
- if (!string.IsNullOrWhiteSpace(message.AssistantId) &&
- !agentNames.TryGetValue(message.AssistantId, out assistantName))
+ Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431)
+ if (!string.IsNullOrWhiteSpace(assistant.Name))
{
- Assistant assistant = await client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false);
- if (!string.IsNullOrWhiteSpace(assistant.Name))
- {
- agentNames.Add(assistant.Id, assistant.Name);
- }
+ agentNames.Add(assistant.Id, assistant.Name);
}
+ }
- assistantName ??= message.AssistantId;
-
- ChatMessageContent content = GenerateMessageContent(assistantName, message);
+ assistantName ??= message.AssistantId;
- if (content.Items.Count > 0)
- {
- yield return content;
- }
+ ChatMessageContent content = GenerateMessageContent(assistantName, message);
- lastId = message.Id;
+ if (content.Items.Count > 0)
+ {
+ yield return content;
}
}
- while (messages.HasMore);
}
///
/// Invoke the assistant on the specified thread.
+ /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user.
+ /// Example of a non-visible message is function-content for functions that are automatically executed.
///
/// The assistant agent to interact with the thread.
/// The assistant client
/// The thread identifier
- /// Config to utilize when polling for run state.
+ /// Options to utilize for the invocation
/// The logger to utilize (might be agent or channel scoped)
/// The plugins and other state.
/// Optional arguments to pass to the agents's invocation, including any .
@@ -118,9 +150,9 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
///
public static async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(
OpenAIAssistantAgent agent,
- AssistantsClient client,
+ AssistantClient client,
string threadId,
- OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration,
+ OpenAIAssistantInvocationOptions? invocationOptions,
ILogger logger,
Kernel kernel,
KernelArguments? arguments,
@@ -131,19 +163,15 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}.");
}
- ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))];
-
logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId);
- CreateRunOptions options =
- new(agent.Id)
- {
- OverrideInstructions = agent.Instructions,
- OverrideTools = tools,
- };
+ ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))];
+
+ RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions);
+
+ options.ToolsOverride.AddRange(tools);
- // Create run
- ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false);
+ ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false);
logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId);
@@ -154,7 +182,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
do
{
// Poll run and steps until actionable
- PageableList steps = await PollRunStatusAsync().ConfigureAwait(false);
+ await PollRunStatusAsync().ConfigureAwait(false);
// Is in terminal state?
if (s_terminalStatuses.Contains(run.Status))
@@ -162,13 +190,15 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}");
}
+ RunStep[] steps = await client.GetRunStepsAsync(run).ToArrayAsync(cancellationToken).ConfigureAwait(false);
+
// Is tool action required?
if (run.Status == RunStatus.RequiresAction)
{
logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId);
// Execute functions in parallel and post results at once.
- FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray();
+ FunctionCallContent[] activeFunctionSteps = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray();
if (activeFunctionSteps.Length > 0)
{
// Emit function-call content
@@ -183,7 +213,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
// Process tool output
ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults);
- await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false);
+ await client.SubmitToolOutputsToRunAsync(threadId, run.Id, toolOutputs, cancellationToken).ConfigureAwait(false);
}
logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), activeFunctionSteps.Length, run.Id, threadId);
@@ -200,26 +230,24 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
int messageCount = 0;
foreach (RunStep completedStep in completedStepsToProcess)
{
- if (completedStep.Type.Equals(RunStepType.ToolCalls))
+ if (completedStep.Type == RunStepType.ToolCalls)
{
- RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails;
-
- foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls)
+ foreach (RunStepToolCall toolCall in completedStep.Details.ToolCalls)
{
bool isVisible = false;
ChatMessageContent? content = null;
// Process code-interpreter content
- if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter)
+ if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter)
{
- content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter);
+ content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput);
isVisible = true;
}
// Process function result content
- else if (toolCall is RunStepFunctionToolCall toolFunction)
+ else if (toolCall.ToolKind == RunStepToolCallKind.Function)
{
- FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation
- content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output);
+ FunctionCallContent functionStep = functionSteps[toolCall.ToolCallId]; // Function step always captured on invocation
+ content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolCall.FunctionOutput);
}
if (content is not null)
@@ -230,12 +258,10 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
}
}
}
- else if (completedStep.Type.Equals(RunStepType.MessageCreation))
+ else if (completedStep.Type == RunStepType.MessageCreation)
{
- RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails;
-
// Retrieve the message
- ThreadMessage? message = await RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false);
+ ThreadMessage? message = await RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false);
if (message is not null)
{
@@ -260,7 +286,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist
logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run.Id, threadId);
// Local function to assist in run polling (participates in method closure).
- async Task> PollRunStatusAsync()
+ async Task PollRunStatusAsync()
{
logger.LogOpenAIAssistantPollingRunStatus(nameof(PollRunStatusAsync), run.Id, threadId);
@@ -269,7 +295,7 @@ async Task> PollRunStatusAsync()
do
{
// Reduce polling frequency after a couple attempts
- await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false);
+ await Task.Delay(agent.PollingOptions.GetPollingInterval(count), cancellationToken).ConfigureAwait(false);
++count;
#pragma warning disable CA1031 // Do not catch general exception types
@@ -286,39 +312,37 @@ async Task> PollRunStatusAsync()
while (s_pollingStatuses.Contains(run.Status));
logger.LogOpenAIAssistantPolledRunStatus(nameof(PollRunStatusAsync), run.Status, run.Id, threadId);
-
- return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false);
}
// Local function to capture kernel function state for further processing (participates in method closure).
IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step)
{
- if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails)
+ if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls)
{
- foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType())
+ foreach (RunStepToolCall toolCall in step.Details.ToolCalls)
{
- var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter);
+ var nameParts = FunctionName.Parse(toolCall.FunctionName);
KernelArguments functionArguments = [];
- if (!string.IsNullOrWhiteSpace(toolCall.Arguments))
+ if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments))
{
- Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!;
+ Dictionary arguments = JsonSerializer.Deserialize>(toolCall.FunctionArguments)!;
foreach (var argumentKvp in arguments)
{
functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString();
}
}
- var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments);
+ var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments);
- functionSteps.Add(toolCall.Id, content);
+ functionSteps.Add(toolCall.ToolCallId, content);
yield return content;
}
}
}
- async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken)
+ async Task RetrieveMessageAsync(string messageId, CancellationToken cancellationToken)
{
ThreadMessage? message = null;
@@ -328,7 +352,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R
{
try
{
- message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false);
+ message = await client.GetMessageAsync(threadId, messageId, cancellationToken).ConfigureAwait(false);
}
catch (RequestFailedException exception)
{
@@ -340,7 +364,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R
if (retry)
{
- await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false);
+ await Task.Delay(agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false);
}
++count;
@@ -361,57 +385,58 @@ private static ChatMessageContent GenerateMessageContent(string? assistantName,
AuthorName = assistantName,
};
- foreach (MessageContent itemContent in message.ContentItems)
+ foreach (MessageContent itemContent in message.Content)
{
// Process text content
- if (itemContent is MessageTextContent contentMessage)
+ if (!string.IsNullOrEmpty(itemContent.Text))
{
- content.Items.Add(new TextContent(contentMessage.Text.Trim()));
+ content.Items.Add(new TextContent(itemContent.Text));
- foreach (MessageTextAnnotation annotation in contentMessage.Annotations)
+ foreach (TextAnnotation annotation in itemContent.TextAnnotations)
{
content.Items.Add(GenerateAnnotationContent(annotation));
}
}
// Process image content
- else if (itemContent is MessageImageFileContent contentImage)
+ else if (itemContent.ImageFileId != null)
{
- content.Items.Add(new FileReferenceContent(contentImage.FileId));
+ content.Items.Add(new FileReferenceContent(itemContent.ImageFileId));
}
}
return content;
}
- private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation)
+ private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation)
{
string? fileId = null;
- if (annotation is MessageTextFileCitationAnnotation citationAnnotation)
+
+ if (!string.IsNullOrEmpty(annotation.OutputFileId))
{
- fileId = citationAnnotation.FileId;
+ fileId = annotation.OutputFileId;
}
- else if (annotation is MessageTextFilePathAnnotation pathAnnotation)
+ else if (!string.IsNullOrEmpty(annotation.InputFileId))
{
- fileId = pathAnnotation.FileId;
+ fileId = annotation.InputFileId;
}
return
new()
{
- Quote = annotation.Text,
+ Quote = annotation.TextToReplace,
StartIndex = annotation.StartIndex,
EndIndex = annotation.EndIndex,
FileId = fileId,
};
}
- private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter)
+ private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string pythonCode)
{
return
new ChatMessageContent(
AuthorRole.Assistant,
[
- new TextContent(contentCodeInterpreter.Input)
+ new TextContent(pythonCode)
])
{
AuthorName = agentName,
diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs
new file mode 100644
index 000000000000..6874e1d21755
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using OpenAI.Assistants;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+
+///
+/// Factory for creating definition.
+///
+///
+/// Improves testability.
+///
+internal static class AssistantToolResourcesFactory
+{
+ ///
+ /// Produces a definition based on the provided parameters.
+ ///
+ /// An optional vector-store-id for the 'file_search' tool
+ /// An optionallist of file-identifiers for the 'code_interpreter' tool.
+ public static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds)
+ {
+ bool hasVectorStore = !string.IsNullOrWhiteSpace(vectorStoreId);
+ bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0;
+
+ ToolResources? toolResources = null;
+
+ if (hasVectorStore || hasCodeInterpreterFiles)
+ {
+ toolResources =
+ new ToolResources()
+ {
+ FileSearch =
+ hasVectorStore ?
+ new FileSearchToolResources()
+ {
+ VectorStoreIds = [vectorStoreId!],
+ } :
+ null,
+ CodeInterpreter =
+ hasCodeInterpreterFiles ?
+ new CodeInterpreterToolResources()
+ {
+ FileIds = (IList)codeInterpreterFileIds!,
+ } :
+ null,
+ };
+ }
+
+ return toolResources;
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs
index bc7c8d9919f0..3a39c314c5c3 100644
--- a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs
+++ b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs
@@ -1,7 +1,8 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
-using Azure.AI.OpenAI.Assistants;
using Microsoft.Extensions.Logging;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using OpenAI.Assistants;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
index 6746c6c50d9a..f5c4a3588cf8 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs
@@ -1,17 +1,16 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Runtime.CompilerServices;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Azure;
-using Azure.AI.OpenAI.Assistants;
-using Azure.Core;
-using Azure.Core.Pipeline;
using Microsoft.Extensions.Logging;
-using Microsoft.SemanticKernel.Agents.OpenAI.Azure;
-using Microsoft.SemanticKernel.Http;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using OpenAI;
+using OpenAI.Assistants;
+using OpenAI.Files;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
@@ -25,9 +24,12 @@ public sealed class OpenAIAssistantAgent : KernelAgent
///
public const string CodeInterpreterMetadataKey = "code";
+ internal const string OptionsMetadataKey = "__run_options";
+
+ private readonly OpenAIClientProvider _provider;
private readonly Assistant _assistant;
- private readonly AssistantsClient _client;
- private readonly OpenAIAssistantConfiguration _config;
+ private readonly AssistantClient _client;
+ private readonly string[] _channelKeys;
///
/// Optional arguments for the agent.
@@ -38,57 +40,55 @@ public sealed class OpenAIAssistantAgent : KernelAgent
public KernelArguments? Arguments { get; init; }
///
- /// A list of previously uploaded file IDs to attach to the assistant.
+ /// The assistant definition.
///
- public IReadOnlyList FileIds => this._assistant.FileIds;
+ public OpenAIAssistantDefinition Definition { get; private init; }
///
- /// A set of up to 16 key/value pairs that can be attached to an agent, used for
- /// storing additional information about that object in a structured format.Keys
- /// may be up to 64 characters in length and values may be up to 512 characters in length.
+ /// Set when the assistant has been deleted via .
+ /// An assistant removed by other means will result in an exception when invoked.
///
- public IReadOnlyDictionary Metadata => this._assistant.Metadata;
+ public bool IsDeleted { get; private set; }
///
- /// Expose predefined tools.
+ /// Defines polling behavior for run processing
///
- internal IReadOnlyList Tools => this._assistant.Tools;
+ public RunPollingOptions PollingOptions { get; } = new();
///
- /// Set when the assistant has been deleted via .
- /// An assistant removed by other means will result in an exception when invoked.
+ /// Expose predefined tools for run-processing.
///
- public bool IsDeleted { get; private set; }
+ internal IReadOnlyList Tools => this._assistant.Tools;
///
/// Define a new .
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Configuration for accessing the Assistants API service, such as the api-key.
+ /// OpenAI client provider for accessing the API service.
/// The assistant definition.
/// The to monitor for cancellation requests. The default is .
/// An instance
public static async Task CreateAsync(
Kernel kernel,
- OpenAIAssistantConfiguration config,
+ OpenAIClientProvider clientProvider,
OpenAIAssistantDefinition definition,
CancellationToken cancellationToken = default)
{
// Validate input
Verify.NotNull(kernel, nameof(kernel));
- Verify.NotNull(config, nameof(config));
+ Verify.NotNull(clientProvider, nameof(clientProvider));
Verify.NotNull(definition, nameof(definition));
// Create the client
- AssistantsClient client = CreateClient(config);
+ AssistantClient client = CreateClient(clientProvider);
// Create the assistant
AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition);
- Assistant model = await client.CreateAssistantAsync(assistantCreationOptions, cancellationToken).ConfigureAwait(false);
+ Assistant model = await client.CreateAssistantAsync(definition.ModelId, assistantCreationOptions, cancellationToken).ConfigureAwait(false);
// Instantiate the agent
return
- new OpenAIAssistantAgent(client, model, config)
+ new OpenAIAssistantAgent(model, clientProvider, client)
{
Kernel = kernel,
};
@@ -97,79 +97,46 @@ public static async Task CreateAsync(
///
/// Retrieve a list of assistant definitions: .
///
- /// Configuration for accessing the Assistants API service, such as the api-key.
- /// The maximum number of assistant definitions to retrieve
- /// The identifier of the assistant beyond which to begin selection.
+ /// Configuration for accessing the API service.
/// The to monitor for cancellation requests. The default is .
/// An list of objects.
public static async IAsyncEnumerable ListDefinitionsAsync(
- OpenAIAssistantConfiguration config,
- int maxResults = 100,
- string? lastId = null,
+ OpenAIClientProvider provider,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
{
// Create the client
- AssistantsClient client = CreateClient(config);
+ AssistantClient client = CreateClient(provider);
- // Retrieve the assistants
- PageableList assistants;
-
- int resultCount = 0;
- do
+ // Query and enumerate assistant definitions
+ await foreach (Assistant model in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false))
{
- assistants = await client.GetAssistantsAsync(limit: Math.Min(maxResults, 100), ListSortOrder.Descending, after: lastId, cancellationToken: cancellationToken).ConfigureAwait(false);
- foreach (Assistant assistant in assistants)
- {
- if (resultCount >= maxResults)
- {
- break;
- }
-
- resultCount++;
-
- yield return
- new()
- {
- Id = assistant.Id,
- Name = assistant.Name,
- Description = assistant.Description,
- Instructions = assistant.Instructions,
- EnableCodeInterpreter = assistant.Tools.Any(t => t is CodeInterpreterToolDefinition),
- EnableRetrieval = assistant.Tools.Any(t => t is RetrievalToolDefinition),
- FileIds = assistant.FileIds,
- Metadata = assistant.Metadata,
- ModelId = assistant.Model,
- };
-
- lastId = assistant.Id;
- }
+ yield return CreateAssistantDefinition(model);
}
- while (assistants.HasMore && resultCount < maxResults);
}
///
/// Retrieve a by identifier.
///
/// The containing services, plugins, and other state for use throughout the operation.
- /// Configuration for accessing the Assistants API service, such as the api-key.
+ /// Configuration for accessing the API service.
/// The agent identifier
/// The to monitor for cancellation requests. The default is .
/// An instance
public static async Task RetrieveAsync(
Kernel kernel,
- OpenAIAssistantConfiguration config,
+ OpenAIClientProvider provider,
string id,
CancellationToken cancellationToken = default)
{
// Create the client
- AssistantsClient client = CreateClient(config);
+ AssistantClient client = CreateClient(provider);
// Retrieve the assistant
- Assistant model = await client.GetAssistantAsync(id, cancellationToken).ConfigureAwait(false);
+ Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431)
// Instantiate the agent
return
- new OpenAIAssistantAgent(client, model, config)
+ new OpenAIAssistantAgent(model, provider, client)
{
Kernel = kernel,
};
@@ -180,12 +147,17 @@ public static async Task RetrieveAsync(
///
/// The to monitor for cancellation requests. The default is .
/// The thread identifier
- public async Task CreateThreadAsync(CancellationToken cancellationToken = default)
- {
- AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false);
+ public Task CreateThreadAsync(CancellationToken cancellationToken = default)
+ => AssistantThreadActions.CreateThreadAsync(this._client, options: null, cancellationToken);
- return thread.Id;
- }
+ ///
+ /// Create a new assistant thread.
+ ///
+ /// The options for creating the thread
+ /// The to monitor for cancellation requests. The default is .
+ /// The thread identifier
+ public Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default)
+ => AssistantThreadActions.CreateThreadAsync(this._client, options, cancellationToken);
///
/// Create a new assistant thread.
@@ -203,6 +175,25 @@ public async Task DeleteThreadAsync(
return await this._client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false);
}
+ ///
+ /// Uploads an file for the purpose of using with assistant.
+ ///
+ /// The content to upload
+ /// The name of the file
+ /// The to monitor for cancellation requests. The default is .
+ /// The file identifier
+ ///
+ /// Use the directly for more advanced file operations.
+ ///
+ public async Task UploadFileAsync(Stream stream, string name, CancellationToken cancellationToken = default)
+ {
+ FileClient client = this._provider.Client.GetFileClient();
+
+ OpenAIFileInfo fileInfo = await client.UploadFileAsync(stream, name, FileUploadPurpose.Assistants, cancellationToken).ConfigureAwait(false);
+
+ return fileInfo.Id;
+ }
+
///
/// Adds a message to the specified thread.
///
@@ -232,7 +223,7 @@ public IAsyncEnumerable GetThreadMessagesAsync(string thread
///
/// Delete the assistant definition.
///
- ///
+ /// The to monitor for cancellation requests. The default is .
/// True if assistant definition has been deleted
///
/// Assistant based agent will not be useable after deletion.
@@ -258,8 +249,28 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul
///
/// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility.
///
+ public IAsyncEnumerable InvokeAsync(
+ string threadId,
+ KernelArguments? arguments = null,
+ Kernel? kernel = null,
+ CancellationToken cancellationToken = default)
+ => this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken);
+
+ ///
+ /// Invoke the assistant on the specified thread.
+ ///
+ /// The thread identifier
+ /// Optional invocation options
+ /// Optional arguments to pass to the agents's invocation, including any .
+ /// The containing services, plugins, and other state for use by the agent.
+ /// The to monitor for cancellation requests. The default is .
+ /// Asynchronous enumeration of messages.
+ ///
+ /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility.
+ ///
public async IAsyncEnumerable InvokeAsync(
string threadId,
+ OpenAIAssistantInvocationOptions? options,
KernelArguments? arguments = null,
Kernel? kernel = null,
[EnumeratorCancellation] CancellationToken cancellationToken = default)
@@ -269,7 +280,7 @@ public async IAsyncEnumerable InvokeAsync(
kernel ??= this.Kernel;
arguments ??= this.Arguments;
- await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, kernel, arguments, cancellationToken).ConfigureAwait(false))
+ await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, options, this.Logger, kernel, arguments, cancellationToken).ConfigureAwait(false))
{
if (isVisible)
{
@@ -282,29 +293,11 @@ public async IAsyncEnumerable InvokeAsync(
protected override IEnumerable GetChannelKeys()
{
// Distinguish from other channel types.
- yield return typeof(AgentChannel).FullName!;
+ yield return typeof(OpenAIAssistantChannel).FullName!;
- // Distinguish between different Azure OpenAI endpoints or OpenAI services.
- yield return this._config.Endpoint ?? "openai";
-
- // Distinguish between different API versioning.
- if (this._config.Version.HasValue)
+ foreach (string key in this._channelKeys)
{
- yield return this._config.Version.ToString()!;
- }
-
- // Custom client receives dedicated channel.
- if (this._config.HttpClient is not null)
- {
- if (this._config.HttpClient.BaseAddress is not null)
- {
- yield return this._config.HttpClient.BaseAddress.AbsoluteUri;
- }
-
- foreach (string header in this._config.HttpClient.DefaultRequestHeaders.SelectMany(h => h.Value))
- {
- yield return header;
- }
+ yield return key;
}
}
@@ -313,10 +306,12 @@ protected override async Task CreateChannelAsync(CancellationToken
{
this.Logger.LogOpenAIAssistantAgentCreatingChannel(nameof(CreateChannelAsync), nameof(OpenAIAssistantChannel));
- AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false);
+ AssistantThread thread = await this._client.CreateThreadAsync(options: null, cancellationToken).ConfigureAwait(false);
+
+ this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id);
OpenAIAssistantChannel channel =
- new(this._client, thread.Id, this._config.Polling)
+ new(this._client, thread.Id)
{
Logger = this.LoggerFactory.CreateLogger()
};
@@ -338,13 +333,16 @@ internal void ThrowIfDeleted()
/// Initializes a new instance of the class.
///
private OpenAIAssistantAgent(
- AssistantsClient client,
Assistant model,
- OpenAIAssistantConfiguration config)
+ OpenAIClientProvider provider,
+ AssistantClient client)
{
+ this._provider = provider;
this._assistant = model;
- this._client = client;
- this._config = config;
+ this._client = provider.Client.GetAssistantClient();
+ this._channelKeys = provider.ConfigurationKeys.ToArray();
+
+ this.Definition = CreateAssistantDefinition(model);
this.Description = this._assistant.Description;
this.Id = this._assistant.Id;
@@ -352,64 +350,94 @@ private OpenAIAssistantAgent(
this.Instructions = this._assistant.Instructions;
}
+ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model)
+ {
+ OpenAIAssistantExecutionOptions? options = null;
+
+ if (model.Metadata.TryGetValue(OptionsMetadataKey, out string? optionsJson))
+ {
+ options = JsonSerializer.Deserialize(optionsJson);
+ }
+
+ IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds;
+ string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.SingleOrDefault();
+ bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject;
+
+ return new(model.Model)
+ {
+ Id = model.Id,
+ Name = model.Name,
+ Description = model.Description,
+ Instructions = model.Instructions,
+ CodeInterpreterFileIds = fileIds,
+ EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition),
+ EnableFileSearch = model.Tools.Any(t => t is FileSearchToolDefinition),
+ Metadata = model.Metadata,
+ EnableJsonResponse = enableJsonResponse,
+ TopP = model.NucleusSamplingFactor,
+ Temperature = model.Temperature,
+ VectorStoreId = string.IsNullOrWhiteSpace(vectorStoreId) ? null : vectorStoreId,
+ ExecutionOptions = options,
+ };
+ }
+
private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition)
{
AssistantCreationOptions assistantCreationOptions =
- new(definition.ModelId)
+ new()
{
Description = definition.Description,
Instructions = definition.Instructions,
Name = definition.Name,
- Metadata = definition.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value),
+ ToolResources =
+ AssistantToolResourcesFactory.GenerateToolResources(
+ definition.EnableFileSearch ? definition.VectorStoreId : null,
+ definition.EnableCodeInterpreter ? definition.CodeInterpreterFileIds : null),
+ ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto,
+ Temperature = definition.Temperature,
+ NucleusSamplingFactor = definition.TopP,
};
- assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []);
+ if (definition.Metadata != null)
+ {
+ foreach (KeyValuePair item in definition.Metadata)
+ {
+ assistantCreationOptions.Metadata[item.Key] = item.Value;
+ }
+ }
+
+ if (definition.ExecutionOptions != null)
+ {
+ string optionsJson = JsonSerializer.Serialize(definition.ExecutionOptions);
+ assistantCreationOptions.Metadata[OptionsMetadataKey] = optionsJson;
+ }
if (definition.EnableCodeInterpreter)
{
- assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition());
+ assistantCreationOptions.Tools.Add(ToolDefinition.CreateCodeInterpreter());
}
- if (definition.EnableRetrieval)
+ if (definition.EnableFileSearch)
{
- assistantCreationOptions.Tools.Add(new RetrievalToolDefinition());
+ assistantCreationOptions.Tools.Add(ToolDefinition.CreateFileSearch());
}
return assistantCreationOptions;
}
- private static AssistantsClient CreateClient(OpenAIAssistantConfiguration config)
+ private static AssistantClient CreateClient(OpenAIClientProvider config)
{
- AssistantsClientOptions clientOptions = CreateClientOptions(config);
-
- // Inspect options
- if (!string.IsNullOrWhiteSpace(config.Endpoint))
- {
- // Create client configured for Azure OpenAI, if endpoint definition is present.
- return new AssistantsClient(new Uri(config.Endpoint), new AzureKeyCredential(config.ApiKey), clientOptions);
- }
-
- // Otherwise, create client configured for OpenAI.
- return new AssistantsClient(config.ApiKey, clientOptions);
+ return config.Client.GetAssistantClient();
}
- private static AssistantsClientOptions CreateClientOptions(OpenAIAssistantConfiguration config)
+ private static IEnumerable DefineChannelKeys(OpenAIClientProvider config)
{
- AssistantsClientOptions options =
- config.Version.HasValue ?
- new(config.Version.Value) :
- new();
-
- options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent;
- options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), HttpPipelinePosition.PerCall);
+ // Distinguish from other channel types.
+ yield return typeof(AgentChannel).FullName!;
- if (config.HttpClient is not null)
+ foreach (string key in config.ConfigurationKeys)
{
- options.Transport = new HttpClientTransport(config.HttpClient);
- options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided.
- options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout
+ yield return key;
}
-
- return options;
}
}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
index 051281c95abe..77e8de748653 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs
@@ -2,17 +2,18 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
-using Azure.AI.OpenAI.Assistants;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using OpenAI.Assistants;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
///
/// A specialization for use with .
///
-internal sealed class OpenAIAssistantChannel(AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration)
+internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId)
: AgentChannel
{
- private readonly AssistantsClient _client = client;
+ private readonly AssistantClient _client = client;
private readonly string _threadId = threadId;
///
@@ -31,7 +32,7 @@ protected override async Task ReceiveAsync(IEnumerable histo
{
agent.ThrowIfDeleted();
- return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, agent.Kernel, agent.Arguments, cancellationToken);
+ return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken);
}
///
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs
deleted file mode 100644
index aa037266e7d5..000000000000
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs
+++ /dev/null
@@ -1,91 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-using System;
-using System.Net.Http;
-using Azure.AI.OpenAI.Assistants;
-
-namespace Microsoft.SemanticKernel.Agents.OpenAI;
-
-///
-/// Configuration to target an OpenAI Assistant API.
-///
-public sealed class OpenAIAssistantConfiguration
-{
- ///
- /// The Assistants API Key.
- ///
- public string ApiKey { get; }
-
- ///
- /// An optional endpoint if targeting Azure OpenAI Assistants API.
- ///
- public string? Endpoint { get; }
-
- ///
- /// An optional API version override.
- ///
- public AssistantsClientOptions.ServiceVersion? Version { get; init; }
-
- ///
- /// Custom for HTTP requests.
- ///
- public HttpClient? HttpClient { get; init; }
-
- ///
- /// Defineds polling behavior for Assistant API requests.
- ///
- public PollingConfiguration Polling { get; } = new PollingConfiguration();
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// The Assistants API Key
- /// An optional endpoint if targeting Azure OpenAI Assistants API
- public OpenAIAssistantConfiguration(string apiKey, string? endpoint = null)
- {
- Verify.NotNullOrWhiteSpace(apiKey);
- if (!string.IsNullOrWhiteSpace(endpoint))
- {
- // Only verify `endpoint` when provided (AzureOAI vs OpenAI)
- Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'");
- }
-
- this.ApiKey = apiKey;
- this.Endpoint = endpoint;
- }
-
- ///
- /// Configuration and defaults associated with polling behavior for Assistant API requests.
- ///
- public sealed class PollingConfiguration
- {
- ///
- /// The default polling interval when monitoring thread-run status.
- ///
- public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500);
-
- ///
- /// The default back-off interval when monitoring thread-run status.
- ///
- public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1);
-
- ///
- /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag.
- ///
- public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500);
-
- ///
- /// The polling interval when monitoring thread-run status.
- ///
- public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval;
-
- ///
- /// The back-off interval when monitoring thread-run status.
- ///
- public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff;
-
- ///
- /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag.
- ///
- public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay;
- }
-}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs
index 3699e07ee1ed..7b7015aa3b4a 100644
--- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs
@@ -1,57 +1,112 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Text.Json.Serialization;
namespace Microsoft.SemanticKernel.Agents.OpenAI;
///
-/// The data associated with an assistant's definition.
+/// Defines an assistant.
///
public sealed class OpenAIAssistantDefinition
{
///
- /// Identifies the AI model (OpenAI) or deployment (AzureOAI) this agent targets.
+ /// Identifies the AI model targeted by the agent.
///
- public string? ModelId { get; init; }
+ public string ModelId { get; }
///
/// The description of the assistant.
///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Description { get; init; }
///
/// The assistant's unique id. (Ignored on create.)
///
- public string? Id { get; init; }
+ public string Id { get; init; } = string.Empty;
///
/// The system instructions for the assistant to use.
///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Instructions { get; init; }
///
/// The name of the assistant.
///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public string? Name { get; init; }
+ ///
+ /// Optional file-ids made available to the code_interpreter tool, if enabled.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IReadOnlyList? CodeInterpreterFileIds { get; init; }
+
///
/// Set if code-interpreter is enabled.
///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public bool EnableCodeInterpreter { get; init; }
///
- /// Set if retrieval is enabled.
+ /// Set if file-search is enabled.
///
- public bool EnableRetrieval { get; init; }
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool EnableFileSearch { get; init; }
///
- /// A list of previously uploaded file IDs to attach to the assistant.
+ /// Set if json response-format is enabled.
///
- public IEnumerable? FileIds { get; init; }
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool EnableJsonResponse { get; init; }
///
/// A set of up to 16 key/value pairs that can be attached to an agent, used for
/// storing additional information about that object in a structured format.Keys
/// may be up to 64 characters in length and values may be up to 512 characters in length.
///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
public IReadOnlyDictionary? Metadata { get; init; }
+
+ ///
+ /// The sampling temperature to use, between 0 and 2.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Temperature { get; init; }
+
+ ///
+ /// An alternative to sampling with temperature, called nucleus sampling, where the model
+ /// considers the results of the tokens with top_p probability mass.
+ /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
+ ///
+ ///
+ /// Recommended to set this or temperature but not both.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? TopP { get; init; }
+
+ ///
+ /// Requires file-search if specified.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? VectorStoreId { get; init; }
+
+ ///
+ /// Default execution options for each agent invocation.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public OpenAIAssistantExecutionOptions? ExecutionOptions { get; init; }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The targeted model
+ [JsonConstructor]
+ public OpenAIAssistantDefinition(string modelId)
+ {
+ Verify.NotNullOrWhiteSpace(modelId);
+
+ this.ModelId = modelId;
+ }
}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs
new file mode 100644
index 000000000000..074b92831c92
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs
@@ -0,0 +1,38 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Defines assistant execution options for each invocation.
+///
+///
+/// These options are persisted as a single entry of the assistant's metadata with key: "__run_options"
+///
+public sealed class OpenAIAssistantExecutionOptions
+{
+ ///
+ /// The maximum number of completion tokens that may be used over the course of the run.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxCompletionTokens { get; init; }
+
+ ///
+ /// The maximum number of prompt tokens that may be used over the course of the run.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxPromptTokens { get; init; }
+
+ ///
+ /// Enables parallel function calling during tool use. Enabled by default.
+ /// Use this property to disable.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? ParallelToolCallsEnabled { get; init; }
+
+ ///
+ /// When set, the thread will be truncated to the N most recent messages in the thread.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? TruncationMessageCount { get; init; }
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs
new file mode 100644
index 000000000000..0653c83a13e2
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs
@@ -0,0 +1,88 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Defines per invocation execution options that override the assistant definition.
+///
+///
+/// Not applicable to usage.
+///
+public sealed class OpenAIAssistantInvocationOptions
+{
+ ///
+ /// Override the AI model targeted by the agent.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? ModelName { get; init; }
+
+ ///
+ /// Set if code_interpreter tool is enabled.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool EnableCodeInterpreter { get; init; }
+
+ ///
+ /// Set if file_search tool is enabled.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
+ public bool EnableFileSearch { get; init; }
+
+ ///
+ /// Set if json response-format is enabled.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? EnableJsonResponse { get; init; }
+
+ ///
+ /// The maximum number of completion tokens that may be used over the course of the run.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxCompletionTokens { get; init; }
+
+ ///
+ /// The maximum number of prompt tokens that may be used over the course of the run.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? MaxPromptTokens { get; init; }
+
+ ///
+ /// Enables parallel function calling during tool use. Enabled by default.
+ /// Use this property to disable.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public bool? ParallelToolCallsEnabled { get; init; }
+
+ ///
+ /// When set, the thread will be truncated to the N most recent messages in the thread.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public int? TruncationMessageCount { get; init; }
+
+ ///
+ /// The sampling temperature to use, between 0 and 2.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? Temperature { get; init; }
+
+ ///
+ /// An alternative to sampling with temperature, called nucleus sampling, where the model
+ /// considers the results of the tokens with top_p probability mass.
+ /// So 0.1 means only the tokens comprising the top 10% probability mass are considered.
+ ///
+ ///
+ /// Recommended to set this or temperature but not both.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public float? TopP { get; init; }
+
+ ///
+ /// A set of up to 16 key/value pairs that can be attached to an agent, used for
+ /// storing additional information about that object in a structured format.Keys
+ /// may be up to 64 characters in length and values may be up to 512 characters in length.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IReadOnlyDictionary? Metadata { get; init; }
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs
new file mode 100644
index 000000000000..3e2e395a77ea
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs
@@ -0,0 +1,173 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.ClientModel;
+using System.ClientModel.Primitives;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net.Http;
+using System.Threading;
+using Azure.AI.OpenAI;
+using Azure.Core;
+using Microsoft.SemanticKernel.Http;
+using OpenAI;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Provides an for use by .
+///
+public sealed class OpenAIClientProvider
+{
+ ///
+ /// Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key.
+ ///
+ private const string SingleSpaceKey = " ";
+
+ ///
+ /// An active client instance.
+ ///
+ public OpenAIClient Client { get; }
+
+ ///
+ /// Configuration keys required for management.
+ ///
+ internal IReadOnlyList ConfigurationKeys { get; }
+
+ private OpenAIClientProvider(OpenAIClient client, IEnumerable keys)
+ {
+ this.Client = client;
+ this.ConfigurationKeys = keys.ToArray();
+ }
+
+ ///
+ /// Produce a based on .
+ ///
+ /// The API key
+ /// The service endpoint
+ /// Custom for HTTP requests.
+ public static OpenAIClientProvider ForAzureOpenAI(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null)
+ {
+ Verify.NotNull(apiKey, nameof(apiKey));
+ Verify.NotNull(endpoint, nameof(endpoint));
+
+ AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient);
+
+ return new(new AzureOpenAIClient(endpoint, apiKey!, clientOptions), CreateConfigurationKeys(endpoint, httpClient));
+ }
+
+ ///
+ /// Produce a based on .
+ ///
+ /// The credentials
+ /// The service endpoint
+ /// Custom for HTTP requests.
+ public static OpenAIClientProvider ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null)
+ {
+ Verify.NotNull(credential, nameof(credential));
+ Verify.NotNull(endpoint, nameof(endpoint));
+
+ AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient);
+
+ return new(new AzureOpenAIClient(endpoint, credential, clientOptions), CreateConfigurationKeys(endpoint, httpClient));
+ }
+
+ ///
+ /// Produce a based on .
+ ///
+ /// An optional endpoint
+ /// Custom for HTTP requests.
+ public static OpenAIClientProvider ForOpenAI(Uri? endpoint = null, HttpClient? httpClient = null)
+ {
+ OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
+ return new(new OpenAIClient(SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient));
+ }
+
+ ///
+ /// Produce a based on .
+ ///
+ /// The API key
+ /// An optional endpoint
+ /// Custom for HTTP requests.
+ public static OpenAIClientProvider ForOpenAI(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null)
+ {
+ OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient);
+ return new(new OpenAIClient(apiKey ?? SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient));
+ }
+
+ ///
+ /// Directly provide a client instance.
+ ///
+ public static OpenAIClientProvider FromClient(OpenAIClient client)
+ {
+ return new(client, [client.GetType().FullName!, client.GetHashCode().ToString()]);
+ }
+
+ private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, HttpClient? httpClient)
+ {
+ AzureOpenAIClientOptions options = new()
+ {
+ ApplicationId = HttpHeaderConstant.Values.UserAgent,
+ Endpoint = endpoint,
+ };
+
+ ConfigureClientOptions(httpClient, options);
+
+ return options;
+ }
+
+ private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient)
+ {
+ OpenAIClientOptions options = new()
+ {
+ ApplicationId = HttpHeaderConstant.Values.UserAgent,
+ Endpoint = endpoint ?? httpClient?.BaseAddress,
+ };
+
+ ConfigureClientOptions(httpClient, options);
+
+ return options;
+ }
+
+ private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options)
+ {
+ options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall);
+
+ if (httpClient is not null)
+ {
+ options.Transport = new HttpClientPipelineTransport(httpClient);
+ options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided.
+ options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout
+ }
+ }
+
+ private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue)
+ =>
+ new((message) =>
+ {
+ if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false)
+ {
+ message.Request.Headers.Set(headerName, headerValue);
+ }
+ });
+
+ private static IEnumerable CreateConfigurationKeys(Uri? endpoint, HttpClient? httpClient)
+ {
+ if (endpoint != null)
+ {
+ yield return endpoint.ToString();
+ }
+
+ if (httpClient is not null)
+ {
+ if (httpClient.BaseAddress is not null)
+ {
+ yield return httpClient.BaseAddress.AbsoluteUri;
+ }
+
+ foreach (string header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value))
+ {
+ yield return header;
+ }
+ }
+ }
+}
diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs
new file mode 100644
index 000000000000..3f39c43d03dc
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Thread creation options.
+///
+public sealed class OpenAIThreadCreationOptions
+{
+ ///
+ /// Optional file-ids made available to the code_interpreter tool, if enabled.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IReadOnlyList? CodeInterpreterFileIds { get; init; }
+
+ ///
+ /// Optional messages to initialize thread with..
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IReadOnlyList? Messages { get; init; }
+
+ ///
+ /// Enables file-search if specified.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string? VectorStoreId { get; init; }
+
+ ///
+ /// A set of up to 16 key/value pairs that can be attached to an agent, used for
+ /// storing additional information about that object in a structured format.Keys
+ /// may be up to 64 characters in length and values may be up to 512 characters in length.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public IReadOnlyDictionary? Metadata { get; init; }
+}
diff --git a/dotnet/src/Agents/OpenAI/RunPollingOptions.cs b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs
new file mode 100644
index 000000000000..756ba689131c
--- /dev/null
+++ b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+
+namespace Microsoft.SemanticKernel.Agents.OpenAI;
+
+///
+/// Configuration and defaults associated with polling behavior for Assistant API run processing.
+///
+public sealed class RunPollingOptions
+{
+ ///
+ /// The default polling interval when monitoring thread-run status.
+ ///
+ public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500);
+
+ ///
+ /// The default back-off interval when monitoring thread-run status.
+ ///
+ public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1);
+
+ ///
+ /// The default number of polling iterations before using .
+ ///
+ public static int DefaultPollingBackoffThreshold { get; } = 2;
+
+ ///
+ /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag.
+ ///
+ public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500);
+
+ ///
+ /// The polling interval when monitoring thread-run status.
+ ///
+ public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval;
+
+ ///
+ /// The back-off interval when monitoring thread-run status.
+ ///
+ public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff;
+
+ ///
+ /// The number of polling iterations before using .
+ ///
+ public int RunPollingBackoffThreshold { get; set; } = DefaultPollingBackoffThreshold;
+
+ ///
+ /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag.
+ ///
+ public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay;
+
+ ///
+ /// Gets the polling interval for the specified iteration count.
+ ///
+ /// The number of polling iterations already attempted
+ public TimeSpan GetPollingInterval(int iterationCount) =>
+ iterationCount > this.RunPollingBackoffThreshold ? this.RunPollingBackoff : this.RunPollingInterval;
+}
diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs
index 2a680614a54f..17994a12e6a0 100644
--- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs
+++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs
@@ -23,20 +23,26 @@ public class AgentChannelTests
[Fact]
public async Task VerifyAgentChannelUpcastAsync()
{
+ // Arrange
TestChannel channel = new();
+ // Assert
Assert.Equal(0, channel.InvokeCount);
- var messages = channel.InvokeAgentAsync(new TestAgent()).ToArrayAsync();
+ // Act
+ var messages = channel.InvokeAgentAsync(new MockAgent()).ToArrayAsync();
+ // Assert
Assert.Equal(1, channel.InvokeCount);
+ // Act
await Assert.ThrowsAsync(() => channel.InvokeAgentAsync(new NextAgent()).ToArrayAsync().AsTask());
+ // Assert
Assert.Equal(1, channel.InvokeCount);
}
///
/// Not using mock as the goal here is to provide entrypoint to protected method.
///
- private sealed class TestChannel : AgentChannel
+ private sealed class TestChannel : AgentChannel
{
public int InvokeCount { get; private set; }
@@ -44,7 +50,7 @@ private sealed class TestChannel : AgentChannel
=> base.InvokeAsync(agent, cancellationToken);
#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
- protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(TestAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(MockAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default)
#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously
{
this.InvokeCount++;
@@ -63,18 +69,5 @@ protected internal override Task ReceiveAsync(IEnumerable hi
}
}
- private sealed class NextAgent : TestAgent;
-
- private class TestAgent : KernelAgent
- {
- protected internal override Task CreateChannelAsync(CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
-
- protected internal override IEnumerable GetChannelKeys()
- {
- throw new NotImplementedException();
- }
- }
+ private sealed class NextAgent : MockAgent;
}
diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs
index 49c36ae73c53..cd83ab8b9f45 100644
--- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs
+++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs
@@ -3,9 +3,11 @@
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
+using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests;
@@ -21,53 +23,80 @@ public class AgentChatTests
[Fact]
public async Task VerifyAgentChatLifecycleAsync()
{
- // Create chat
+ // Arrange: Create chat
TestChat chat = new();
- // Verify initial state
+ // Assert: Verify initial state
Assert.False(chat.IsActive);
await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync()); // Primary history
await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent history
- // Inject history
+ // Act: Inject history
chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "More")]);
chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "And then some")]);
- // Verify updated history
+ // Assert: Verify updated history
await this.VerifyHistoryAsync(expectedCount: 2, chat.GetChatMessagesAsync()); // Primary history
await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent hasn't joined
- // Invoke with input & verify (agent joins chat)
+ // Act: Invoke with input & verify (agent joins chat)
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi"));
await chat.InvokeAsync().ToArrayAsync();
- Assert.Equal(1, chat.Agent.InvokeCount);
- // Verify updated history
+ // Assert: Verify updated history
+ Assert.Equal(1, chat.Agent.InvokeCount);
await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync()); // Primary history
await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync(chat.Agent)); // Agent history
- // Invoke without input & verify
+ // Act: Invoke without input
await chat.InvokeAsync().ToArrayAsync();
- Assert.Equal(2, chat.Agent.InvokeCount);
- // Verify final history
+ // Assert: Verify final history
+ Assert.Equal(2, chat.Agent.InvokeCount);
await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync()); // Primary history
await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync(chat.Agent)); // Agent history
}
+ ///
+ /// Verify throw exception for system message.
+ ///
+ [Fact]
+ public void VerifyAgentChatRejectsSystemMessage()
+ {
+ // Arrange: Create chat
+ TestChat chat = new() { LoggerFactory = new Mock().Object };
+
+ // Assert and Act: Verify system message not accepted
+ Assert.Throws(() => chat.AddChatMessage(new ChatMessageContent(AuthorRole.System, "hi")));
+ }
+
+ ///
+ /// Verify throw exception for if invoked when active.
+ ///
+ [Fact]
+ public async Task VerifyAgentChatThrowsWhenActiveAsync()
+ {
+ // Arrange: Create chat
+ TestChat chat = new();
+
+ // Assert and Act: Verify system message not accepted
+ await Assert.ThrowsAsync(() => chat.InvalidInvokeAsync().ToArrayAsync().AsTask());
+ }
+
///
/// Verify the management of instances as they join .
///
[Fact(Skip = "Not 100% reliable for github workflows, but useful for dev testing.")]
public async Task VerifyGroupAgentChatConcurrencyAsync()
{
+ // Arrange
TestChat chat = new();
Task[] tasks;
int isActive = 0;
- // Queue concurrent tasks
+ // Act: Queue concurrent tasks
object syncObject = new();
lock (syncObject)
{
@@ -89,7 +118,7 @@ public async Task VerifyGroupAgentChatConcurrencyAsync()
await Task.Yield();
- // Verify failure
+ // Assert: Verify failure
await Assert.ThrowsAsync(() => Task.WhenAll(tasks));
async Task SynchronizedInvokeAsync()
@@ -119,5 +148,12 @@ private sealed class TestChat : AgentChat
public override IAsyncEnumerable InvokeAsync(
CancellationToken cancellationToken = default) =>
this.InvokeAgentAsync(this.Agent, cancellationToken);
+
+ public IAsyncEnumerable InvalidInvokeAsync(
+ CancellationToken cancellationToken = default)
+ {
+ this.SetActivityOrThrow();
+ return this.InvokeAgentAsync(this.Agent, cancellationToken);
+ }
}
}
diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
index 27e1afcfa92c..6b9fea49fde2 100644
--- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
+++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj
@@ -8,7 +8,7 @@
true
false
12
- $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110
+ $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110;OPENAI001
@@ -32,6 +32,7 @@
+
diff --git a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
index 1a607ea7e6c7..e6668c7ea568 100644
--- a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs
@@ -21,6 +21,7 @@ public class AggregatorAgentTests
[InlineData(AggregatorMode.Flat, 2)]
public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeOffset)
{
+ // Arrange
Agent agent1 = CreateMockAgent();
Agent agent2 = CreateMockAgent();
Agent agent3 = CreateMockAgent();
@@ -44,38 +45,57 @@ public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeO
// Add message to outer chat (no agent has joined)
uberChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test uber"));
+ // Act
var messages = await uberChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Single(messages);
+ // Act
messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync();
+ // Assert
Assert.Empty(messages); // Agent hasn't joined chat, no broadcast
+ // Act
messages = await groupChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Empty(messages); // Agent hasn't joined chat, no broadcast
- // Add message to inner chat (not visible to parent)
+ // Arrange: Add message to inner chat (not visible to parent)
groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test inner"));
+ // Act
messages = await uberChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Single(messages);
+ // Act
messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync();
+ // Assert
Assert.Empty(messages); // Agent still hasn't joined chat
+ // Act
messages = await groupChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Single(messages);
- // Invoke outer chat (outer chat captures final inner message)
+ // Act: Invoke outer chat (outer chat captures final inner message)
messages = await uberChat.InvokeAsync(uberAgent).ToArrayAsync();
+ // Assert
Assert.Equal(1 + modeOffset, messages.Length); // New messages generated from inner chat
+ // Act
messages = await uberChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Equal(2 + modeOffset, messages.Length); // Total messages on uber chat
+ // Act
messages = await groupChat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized
+ // Act
messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync();
+ // Assert
Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized (agent equivalent)
}
diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
index ad7428f6f0b9..62420f90e62b 100644
--- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs
@@ -23,12 +23,18 @@ public class AgentGroupChatTests
[Fact]
public void VerifyGroupAgentChatDefaultState()
{
+ // Arrange
AgentGroupChat chat = new();
+
+ // Assert
Assert.Empty(chat.Agents);
Assert.NotNull(chat.ExecutionSettings);
Assert.False(chat.IsComplete);
+ // Act
chat.IsComplete = true;
+
+ // Assert
Assert.True(chat.IsComplete);
}
@@ -38,21 +44,30 @@ public void VerifyGroupAgentChatDefaultState()
[Fact]
public async Task VerifyGroupAgentChatAgentMembershipAsync()
{
+ // Arrange
Agent agent1 = CreateMockAgent();
Agent agent2 = CreateMockAgent();
Agent agent3 = CreateMockAgent();
Agent agent4 = CreateMockAgent();
AgentGroupChat chat = new(agent1, agent2);
+
+ // Assert
Assert.Equal(2, chat.Agents.Count);
+ // Act
chat.AddAgent(agent3);
+ // Assert
Assert.Equal(3, chat.Agents.Count);
+ // Act
var messages = await chat.InvokeAsync(agent4, isJoining: false).ToArrayAsync();
+ // Assert
Assert.Equal(3, chat.Agents.Count);
+ // Act
messages = await chat.InvokeAsync(agent4).ToArrayAsync();
+ // Assert
Assert.Equal(4, chat.Agents.Count);
}
@@ -62,6 +77,7 @@ public async Task VerifyGroupAgentChatAgentMembershipAsync()
[Fact]
public async Task VerifyGroupAgentChatMultiTurnAsync()
{
+ // Arrange
Agent agent1 = CreateMockAgent();
Agent agent2 = CreateMockAgent();
Agent agent3 = CreateMockAgent();
@@ -81,10 +97,14 @@ public async Task VerifyGroupAgentChatMultiTurnAsync()
IsComplete = true
};
+ // Act and Assert
await Assert.ThrowsAsync(() => chat.InvokeAsync(CancellationToken.None).ToArrayAsync().AsTask());
+ // Act
chat.ExecutionSettings.TerminationStrategy.AutomaticReset = true;
var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync();
+
+ // Assert
Assert.Equal(9, messages.Length);
Assert.False(chat.IsComplete);
@@ -111,6 +131,7 @@ public async Task VerifyGroupAgentChatMultiTurnAsync()
[Fact]
public async Task VerifyGroupAgentChatFailedSelectionAsync()
{
+ // Arrange
AgentGroupChat chat = Create3AgentChat();
chat.ExecutionSettings =
@@ -128,6 +149,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync()
// Remove max-limit in order to isolate the target behavior.
chat.ExecutionSettings.TerminationStrategy.MaximumIterations = int.MaxValue;
+ // Act and Assert
await Assert.ThrowsAsync(() => chat.InvokeAsync().ToArrayAsync().AsTask());
}
@@ -137,6 +159,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync()
[Fact]
public async Task VerifyGroupAgentChatMultiTurnTerminationAsync()
{
+ // Arrange
AgentGroupChat chat = Create3AgentChat();
chat.ExecutionSettings =
@@ -150,7 +173,10 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync()
}
};
+ // Act
var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.True(chat.IsComplete);
}
@@ -161,6 +187,7 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync()
[Fact]
public async Task VerifyGroupAgentChatDiscreteTerminationAsync()
{
+ // Arrange
Agent agent1 = CreateMockAgent();
AgentGroupChat chat =
@@ -178,7 +205,10 @@ public async Task VerifyGroupAgentChatDiscreteTerminationAsync()
}
};
+ // Act
var messages = await chat.InvokeAsync(agent1).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.True(chat.IsComplete);
}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs
index d17391ee24be..ecb5cd6eee33 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs
@@ -16,7 +16,10 @@ public class AgentGroupChatSettingsTests
[Fact]
public void VerifyChatExecutionSettingsDefault()
{
+ // Arrange
AgentGroupChatSettings settings = new();
+
+ // Assert
Assert.IsType(settings.TerminationStrategy);
Assert.Equal(1, settings.TerminationStrategy.MaximumIterations);
Assert.IsType(settings.SelectionStrategy);
@@ -28,6 +31,7 @@ public void VerifyChatExecutionSettingsDefault()
[Fact]
public void VerifyChatExecutionContinuationStrategyDefault()
{
+ // Arrange
Mock strategyMock = new();
AgentGroupChatSettings settings =
new()
@@ -35,6 +39,7 @@ public void VerifyChatExecutionContinuationStrategyDefault()
TerminationStrategy = strategyMock.Object
};
+ // Assert
Assert.Equal(strategyMock.Object, settings.TerminationStrategy);
}
@@ -44,6 +49,7 @@ public void VerifyChatExecutionContinuationStrategyDefault()
[Fact]
public void VerifyChatExecutionSelectionStrategyDefault()
{
+ // Arrange
Mock strategyMock = new();
AgentGroupChatSettings settings =
new()
@@ -51,6 +57,7 @@ public void VerifyChatExecutionSelectionStrategyDefault()
SelectionStrategy = strategyMock.Object
};
+ // Assert
Assert.NotNull(settings.SelectionStrategy);
Assert.Equal(strategyMock.Object, settings.SelectionStrategy);
}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs
index 6ad6fd75b18f..5af211c6cdf1 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs
@@ -6,7 +6,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
-using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core.Chat;
@@ -22,7 +21,10 @@ public class AggregatorTerminationStrategyTests
[Fact]
public void VerifyAggregateTerminationStrategyInitialState()
{
+ // Arrange
AggregatorTerminationStrategy strategy = new();
+
+ // Assert
Assert.Equal(AggregateTerminationCondition.All, strategy.Condition);
}
@@ -32,14 +34,16 @@ public void VerifyAggregateTerminationStrategyInitialState()
[Fact]
public async Task VerifyAggregateTerminationStrategyAnyAsync()
{
+ // Arrange
TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
- Mock agentMock = new();
+ MockAgent agentMock = new();
+ // Act and Assert
await VerifyResultAsync(
expectedResult: true,
- agentMock.Object,
+ agentMock,
new(strategyMockTrue, strategyMockFalse)
{
Condition = AggregateTerminationCondition.Any,
@@ -47,7 +51,7 @@ await VerifyResultAsync(
await VerifyResultAsync(
expectedResult: false,
- agentMock.Object,
+ agentMock,
new(strategyMockFalse, strategyMockFalse)
{
Condition = AggregateTerminationCondition.Any,
@@ -55,7 +59,7 @@ await VerifyResultAsync(
await VerifyResultAsync(
expectedResult: true,
- agentMock.Object,
+ agentMock,
new(strategyMockTrue, strategyMockTrue)
{
Condition = AggregateTerminationCondition.Any,
@@ -68,14 +72,16 @@ await VerifyResultAsync(
[Fact]
public async Task VerifyAggregateTerminationStrategyAllAsync()
{
+ // Arrange
TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
- Mock agentMock = new();
+ MockAgent agentMock = new();
+ // Act and Assert
await VerifyResultAsync(
expectedResult: false,
- agentMock.Object,
+ agentMock,
new(strategyMockTrue, strategyMockFalse)
{
Condition = AggregateTerminationCondition.All,
@@ -83,7 +89,7 @@ await VerifyResultAsync(
await VerifyResultAsync(
expectedResult: false,
- agentMock.Object,
+ agentMock,
new(strategyMockFalse, strategyMockFalse)
{
Condition = AggregateTerminationCondition.All,
@@ -91,7 +97,7 @@ await VerifyResultAsync(
await VerifyResultAsync(
expectedResult: true,
- agentMock.Object,
+ agentMock,
new(strategyMockTrue, strategyMockTrue)
{
Condition = AggregateTerminationCondition.All,
@@ -104,34 +110,39 @@ await VerifyResultAsync(
[Fact]
public async Task VerifyAggregateTerminationStrategyAgentAsync()
{
+ // Arrange
TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true);
TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false);
- Mock agentMockA = new();
- Mock agentMockB = new();
+ MockAgent agentMockA = new();
+ MockAgent agentMockB = new();
+ // Act and Assert
await VerifyResultAsync(
expectedResult: false,
- agentMockB.Object,
+ agentMockB,
new(strategyMockTrue, strategyMockTrue)
{
- Agents = [agentMockA.Object],
+ Agents = [agentMockA],
Condition = AggregateTerminationCondition.All,
});
await VerifyResultAsync(
expectedResult: true,
- agentMockB.Object,
+ agentMockB,
new(strategyMockTrue, strategyMockTrue)
{
- Agents = [agentMockB.Object],
+ Agents = [agentMockB],
Condition = AggregateTerminationCondition.All,
});
}
private static async Task VerifyResultAsync(bool expectedResult, Agent agent, AggregatorTerminationStrategy strategyRoot)
{
+ // Act
var result = await strategyRoot.ShouldTerminateAsync(agent, []);
+
+ // Assert
Assert.Equal(expectedResult, result);
}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs
index af045e67873d..83cb9a3ea337 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs
@@ -5,7 +5,6 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Connectors.OpenAI;
-using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core.Chat;
@@ -21,42 +20,73 @@ public class KernelFunctionSelectionStrategyTests
[Fact]
public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync()
{
- Mock mockAgent = new();
- KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id));
+ // Arrange
+ MockAgent mockAgent = new();
+ KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id));
KernelFunctionSelectionStrategy strategy =
new(plugin.Single(), new())
{
- ResultParser = (result) => result.GetValue() ?? string.Empty,
+ ResultParser = (result) => mockAgent.Id,
+ AgentsVariableName = "agents",
+ HistoryVariableName = "history",
};
+ // Assert
Assert.Null(strategy.Arguments);
Assert.NotNull(strategy.Kernel);
Assert.NotNull(strategy.ResultParser);
+ Assert.NotEqual("agent", KernelFunctionSelectionStrategy.DefaultAgentsVariableName);
+ Assert.NotEqual("history", KernelFunctionSelectionStrategy.DefaultHistoryVariableName);
- Agent nextAgent = await strategy.NextAsync([mockAgent.Object], []);
+ // Act
+ Agent nextAgent = await strategy.NextAsync([mockAgent], []);
+ // Assert
Assert.NotNull(nextAgent);
- Assert.Equal(mockAgent.Object, nextAgent);
+ Assert.Equal(mockAgent, nextAgent);
}
///
/// Verify strategy mismatch.
///
[Fact]
- public async Task VerifyKernelFunctionSelectionStrategyParsingAsync()
+ public async Task VerifyKernelFunctionSelectionStrategyThrowsOnNullResultAsync()
{
- Mock mockAgent = new();
- KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(string.Empty));
+ // Arrange
+ MockAgent mockAgent = new();
+ KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id));
KernelFunctionSelectionStrategy strategy =
new(plugin.Single(), new())
{
- Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } },
- ResultParser = (result) => result.GetValue() ?? string.Empty,
+ Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } },
+ ResultParser = (result) => "larry",
};
- await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], []));
+ // Act and Assert
+ await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], []));
+ }
+
+ ///
+ /// Verify strategy mismatch.
+ ///
+ [Fact]
+ public async Task VerifyKernelFunctionSelectionStrategyThrowsOnBadResultAsync()
+ {
+ // Arrange
+ MockAgent mockAgent = new();
+ KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(""));
+
+ KernelFunctionSelectionStrategy strategy =
+ new(plugin.Single(), new())
+ {
+ Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } },
+ ResultParser = (result) => result.GetValue() ?? null!,
+ };
+
+ // Act and Assert
+ await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], []));
}
private sealed class TestPlugin(string agentName)
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs
index 6f0b446e5e7a..7ee5cf838bc3 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs
@@ -3,10 +3,8 @@
using System.Linq;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Connectors.OpenAI;
-using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core.Chat;
@@ -22,17 +20,26 @@ public class KernelFunctionTerminationStrategyTests
[Fact]
public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync()
{
+ // Arrange
KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin());
- KernelFunctionTerminationStrategy strategy = new(plugin.Single(), new());
+ KernelFunctionTerminationStrategy strategy =
+ new(plugin.Single(), new())
+ {
+ AgentVariableName = "agent",
+ HistoryVariableName = "history",
+ };
+ // Assert
Assert.Null(strategy.Arguments);
Assert.NotNull(strategy.Kernel);
Assert.NotNull(strategy.ResultParser);
+ Assert.NotEqual("agent", KernelFunctionTerminationStrategy.DefaultAgentVariableName);
+ Assert.NotEqual("history", KernelFunctionTerminationStrategy.DefaultHistoryVariableName);
- Mock mockAgent = new();
-
- bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []);
+ // Act
+ MockAgent mockAgent = new();
+ bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []);
Assert.True(isTerminating);
}
@@ -52,9 +59,9 @@ public async Task VerifyKernelFunctionTerminationStrategyParsingAsync()
ResultParser = (result) => string.Equals("test", result.GetValue(), StringComparison.OrdinalIgnoreCase)
};
- Mock mockAgent = new();
+ MockAgent mockAgent = new();
- bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []);
+ bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []);
Assert.True(isTerminating);
}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs
index a1b739ae1d1e..196a89ded6e3 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs
@@ -2,10 +2,8 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
-using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.ChatCompletion;
-using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core.Chat;
@@ -13,7 +11,7 @@ namespace SemanticKernel.Agents.UnitTests.Core.Chat;
///
/// Unit testing of .
///
-public class RegexTerminationStrategyTests
+public partial class RegexTerminationStrategyTests
{
///
/// Verify abililty of strategy to match expression.
@@ -21,10 +19,12 @@ public class RegexTerminationStrategyTests
[Fact]
public async Task VerifyExpressionTerminationStrategyAsync()
{
+ // Arrange
RegexTerminationStrategy strategy = new("test");
- Regex r = new("(?:^|\\W)test(?:$|\\W)");
+ Regex r = MyRegex();
+ // Act and Assert
await VerifyResultAsync(
expectedResult: false,
new(r),
@@ -38,9 +38,17 @@ await VerifyResultAsync(
private static async Task VerifyResultAsync(bool expectedResult, RegexTerminationStrategy strategyRoot, string content)
{
+ // Arrange
ChatMessageContent message = new(AuthorRole.Assistant, content);
- Mock agent = new();
- var result = await strategyRoot.ShouldTerminateAsync(agent.Object, [message]);
+ MockAgent agent = new();
+
+ // Act
+ var result = await strategyRoot.ShouldTerminateAsync(agent, [message]);
+
+ // Assert
Assert.Equal(expectedResult, result);
}
+
+ [GeneratedRegex("(?:^|\\W)test(?:$|\\W)")]
+ private static partial Regex MyRegex();
}
diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs
index 04339a8309e4..8f7ff6b29d03 100644
--- a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs
@@ -3,7 +3,6 @@
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.Chat;
-using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core.Chat;
@@ -19,28 +18,38 @@ public class SequentialSelectionStrategyTests
[Fact]
public async Task VerifySequentialSelectionStrategyTurnsAsync()
{
- Mock agent1 = new();
- Mock agent2 = new();
+ // Arrange
+ MockAgent agent1 = new();
+ MockAgent agent2 = new();
- Agent[] agents = [agent1.Object, agent2.Object];
+ Agent[] agents = [agent1, agent2];
SequentialSelectionStrategy strategy = new();
- await VerifyNextAgent(agent1.Object);
- await VerifyNextAgent(agent2.Object);
- await VerifyNextAgent(agent1.Object);
- await VerifyNextAgent(agent2.Object);
- await VerifyNextAgent(agent1.Object);
+ // Act and Assert
+ await VerifyNextAgent(agent1);
+ await VerifyNextAgent(agent2);
+ await VerifyNextAgent(agent1);
+ await VerifyNextAgent(agent2);
+ await VerifyNextAgent(agent1);
+ // Arrange
strategy.Reset();
- await VerifyNextAgent(agent1.Object);
- // Verify index does not exceed current bounds.
- agents = [agent1.Object];
- await VerifyNextAgent(agent1.Object);
+ // Act and Assert
+ await VerifyNextAgent(agent1);
+
+ // Arrange: Verify index does not exceed current bounds.
+ agents = [agent1];
+
+ // Act and Assert
+ await VerifyNextAgent(agent1);
async Task VerifyNextAgent(Agent agent1)
{
+ // Act
Agent? nextAgent = await strategy.NextAsync(agents, []);
+
+ // Assert
Assert.NotNull(nextAgent);
Assert.Equal(agent1.Id, nextAgent.Id);
}
@@ -52,7 +61,10 @@ async Task VerifyNextAgent(Agent agent1)
[Fact]
public async Task VerifySequentialSelectionStrategyEmptyAsync()
{
+ // Arrange
SequentialSelectionStrategy strategy = new();
+
+ // Act and Assert
await Assert.ThrowsAsync(() => strategy.NextAsync([], []));
}
}
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
index c8a1c0578613..01debd8ded5f 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs
@@ -5,6 +5,7 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
+using Microsoft.SemanticKernel.Agents.History;
using Microsoft.SemanticKernel.ChatCompletion;
using Moq;
using Xunit;
@@ -22,6 +23,7 @@ public class ChatCompletionAgentTests
[Fact]
public void VerifyChatCompletionAgentDefinition()
{
+ // Arrange
ChatCompletionAgent agent =
new()
{
@@ -30,6 +32,7 @@ public void VerifyChatCompletionAgentDefinition()
Name = "test name",
};
+ // Assert
Assert.NotNull(agent.Id);
Assert.Equal("test instructions", agent.Instructions);
Assert.Equal("test description", agent.Description);
@@ -43,7 +46,8 @@ public void VerifyChatCompletionAgentDefinition()
[Fact]
public async Task VerifyChatCompletionAgentInvocationAsync()
{
- var mockService = new Mock();
+ // Arrange
+ Mock mockService = new();
mockService.Setup(
s => s.GetChatMessageContentsAsync(
It.IsAny(),
@@ -51,16 +55,18 @@ public async Task VerifyChatCompletionAgentInvocationAsync()
It.IsAny(),
It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]);
- var agent =
- new ChatCompletionAgent()
+ ChatCompletionAgent agent =
+ new()
{
Instructions = "test instructions",
Kernel = CreateKernel(mockService.Object),
Arguments = [],
};
- var result = await agent.InvokeAsync([]).ToArrayAsync();
+ // Act
+ ChatMessageContent[] result = await agent.InvokeAsync([]).ToArrayAsync();
+ // Assert
Assert.Single(result);
mockService.Verify(
@@ -79,13 +85,14 @@ public async Task VerifyChatCompletionAgentInvocationAsync()
[Fact]
public async Task VerifyChatCompletionAgentStreamingAsync()
{
+ // Arrange
StreamingChatMessageContent[] returnContent =
[
new(AuthorRole.Assistant, "wh"),
new(AuthorRole.Assistant, "at?"),
];
- var mockService = new Mock();
+ Mock mockService = new();
mockService.Setup(
s => s.GetStreamingChatMessageContentsAsync(
It.IsAny(),
@@ -93,16 +100,18 @@ public async Task VerifyChatCompletionAgentStreamingAsync()
It.IsAny(),
It.IsAny())).Returns(returnContent.ToAsyncEnumerable());
- var agent =
- new ChatCompletionAgent()
+ ChatCompletionAgent agent =
+ new()
{
Instructions = "test instructions",
Kernel = CreateKernel(mockService.Object),
Arguments = [],
};
- var result = await agent.InvokeStreamingAsync([]).ToArrayAsync();
+ // Act
+ StreamingChatMessageContent[] result = await agent.InvokeStreamingAsync([]).ToArrayAsync();
+ // Assert
Assert.Equal(2, result.Length);
mockService.Verify(
@@ -115,6 +124,52 @@ public async Task VerifyChatCompletionAgentStreamingAsync()
Times.Once);
}
+ ///
+ /// Verify the invocation and response of .
+ ///
+ [Fact]
+ public void VerifyChatCompletionServiceSelection()
+ {
+ // Arrange
+ Mock mockService = new();
+ Kernel kernel = CreateKernel(mockService.Object);
+
+ // Act
+ (IChatCompletionService service, PromptExecutionSettings? settings) = ChatCompletionAgent.GetChatCompletionService(kernel, null);
+ // Assert
+ Assert.Equal(mockService.Object, service);
+ Assert.Null(settings);
+
+ // Act
+ (service, settings) = ChatCompletionAgent.GetChatCompletionService(kernel, []);
+ // Assert
+ Assert.Equal(mockService.Object, service);
+ Assert.Null(settings);
+
+ // Act and Assert
+ Assert.Throws(() => ChatCompletionAgent.GetChatCompletionService(kernel, new KernelArguments(new PromptExecutionSettings() { ServiceId = "anything" })));
+ }
+
+ ///
+ /// Verify the invocation and response of .
+ ///
+ [Fact]
+ public void VerifyChatCompletionChannelKeys()
+ {
+ // Arrange
+ ChatCompletionAgent agent1 = new();
+ ChatCompletionAgent agent2 = new();
+ ChatCompletionAgent agent3 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) };
+ ChatCompletionAgent agent4 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) };
+ ChatCompletionAgent agent5 = new() { HistoryReducer = new ChatHistoryTruncationReducer(100) };
+
+ // Act ans Assert
+ Assert.Equal(agent1.GetChannelKeys(), agent2.GetChannelKeys());
+ Assert.Equal(agent3.GetChannelKeys(), agent4.GetChannelKeys());
+ Assert.NotEqual(agent1.GetChannelKeys(), agent3.GetChannelKeys());
+ Assert.NotEqual(agent3.GetChannelKeys(), agent5.GetChannelKeys());
+ }
+
private static Kernel CreateKernel(IChatCompletionService chatCompletionService)
{
var builder = Kernel.CreateBuilder();
diff --git a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs
index 43aae918ad52..dfa9f59032c1 100644
--- a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs
@@ -1,11 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
-using System;
-using System.Collections.Generic;
using System.Linq;
-using System.Threading;
using System.Threading.Tasks;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
+using Moq;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.Core;
@@ -22,21 +20,11 @@ public class ChatHistoryChannelTests
[Fact]
public async Task VerifyAgentWithoutIChatHistoryHandlerAsync()
{
- TestAgent agent = new(); // Not a IChatHistoryHandler
+ // Arrange
+ Mock agent = new(); // Not a IChatHistoryHandler
ChatHistoryChannel channel = new(); // Requires IChatHistoryHandler
- await Assert.ThrowsAsync(() => channel.InvokeAsync(agent).ToArrayAsync().AsTask());
- }
-
- private sealed class TestAgent : KernelAgent
- {
- protected internal override Task CreateChannelAsync(CancellationToken cancellationToken)
- {
- throw new NotImplementedException();
- }
- protected internal override IEnumerable GetChannelKeys()
- {
- throw new NotImplementedException();
- }
+ // Act & Assert
+ await Assert.ThrowsAsync(() => channel.InvokeAsync(agent.Object).ToArrayAsync().AsTask());
}
}
diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs
index a75533474147..d9042305d9fa 100644
--- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs
@@ -30,8 +30,10 @@ public class ChatHistoryReducerExtensionsTests
[InlineData(100, 0, int.MaxValue, 100)]
public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? endIndex = null, int? expectedCount = null)
{
+ // Arrange
ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)];
+ // Act
ChatMessageContent[] extractedHistory = history.Extract(startIndex, endIndex).ToArray();
int finalIndex = endIndex ?? messageCount - 1;
@@ -39,6 +41,7 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e
expectedCount ??= finalIndex - startIndex + 1;
+ // Assert
Assert.Equal(expectedCount, extractedHistory.Length);
if (extractedHistory.Length > 0)
@@ -58,16 +61,19 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e
[InlineData(100, 0)]
public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount)
{
+ // Arrange
ChatHistory summaries = [.. MockHistoryGenerator.CreateSimpleHistory(summaryCount)];
foreach (ChatMessageContent summary in summaries)
{
summary.Metadata = new Dictionary() { { "summary", true } };
}
+ // Act
ChatHistory history = [.. summaries, .. MockHistoryGenerator.CreateSimpleHistory(regularCount)];
int finalSummaryIndex = history.LocateSummarizationBoundary("summary");
+ // Assert
Assert.Equal(summaryCount, finalSummaryIndex);
}
@@ -77,17 +83,22 @@ public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount)
[Fact]
public async Task VerifyChatHistoryNotReducedAsync()
{
+ // Arrange
ChatHistory history = [];
+ Mock mockReducer = new();
+ mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null);
+ // Act
bool isReduced = await history.ReduceAsync(null, default);
+ // Assert
Assert.False(isReduced);
Assert.Empty(history);
- Mock mockReducer = new();
- mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null);
+ // Act
isReduced = await history.ReduceAsync(mockReducer.Object, default);
+ // Assert
Assert.False(isReduced);
Assert.Empty(history);
}
@@ -98,13 +109,16 @@ public async Task VerifyChatHistoryNotReducedAsync()
[Fact]
public async Task VerifyChatHistoryReducedAsync()
{
+ // Arrange
Mock mockReducer = new();
mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)[]);
ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(10)];
+ // Act
bool isReduced = await history.ReduceAsync(mockReducer.Object, default);
+ // Assert
Assert.True(isReduced);
Assert.Empty(history);
}
@@ -124,11 +138,13 @@ public async Task VerifyChatHistoryReducedAsync()
[InlineData(900, 500, int.MaxValue)]
public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount, int? thresholdCount = null)
{
- // Shape of history doesn't matter since reduction is not expected
+ // Arrange: Shape of history doesn't matter since reduction is not expected
ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)];
+ // Act
int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount);
+ // Assert
Assert.Equal(0, reductionIndex);
}
@@ -146,11 +162,13 @@ public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount
[InlineData(1000, 500, 499)]
public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCount, int? thresholdCount = null)
{
- // Generate history with only assistant messages
+ // Arrange: Generate history with only assistant messages
ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)];
+ // Act
int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount);
+ // Assert
Assert.True(reductionIndex > 0);
Assert.Equal(targetCount, messageCount - reductionIndex);
}
@@ -170,17 +188,20 @@ public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCoun
[InlineData(1000, 500, 499)]
public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int targetCount, int? thresholdCount = null)
{
- // Generate history with alternating user and assistant messages
+ // Arrange: Generate history with alternating user and assistant messages
ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)];
+ // Act
int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount);
+ // Assert
Assert.True(reductionIndex > 0);
- // The reduction length should align with a user message, if threshold is specified
+ // Act: The reduction length should align with a user message, if threshold is specified
bool hasThreshold = thresholdCount > 0;
int expectedCount = targetCount + (hasThreshold && sourceHistory[^targetCount].Role != AuthorRole.User ? 1 : 0);
+ // Assert
Assert.Equal(expectedCount, messageCount - reductionIndex);
}
@@ -201,14 +222,16 @@ public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int ta
[InlineData(9)]
public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, int? thresholdCount = null)
{
- // Generate a history with function call on index 5 and 9 and
+ // Arrange: Generate a history with function call on index 5 and 9 and
// function result on index 6 and 10 (total length: 14)
ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithFunctionContent()];
ChatHistoryTruncationReducer reducer = new(targetCount, thresholdCount);
+ // Act
int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount);
+ // Assert
Assert.True(reductionIndex > 0);
// The reduction length avoid splitting function call and result, regardless of threshold
@@ -216,7 +239,7 @@ public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, i
if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionCallContent))
{
- expectedCount += 1;
+ expectedCount++;
}
else if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionResultContent))
{
diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs
index f464b6a8214a..53e93d0026c3 100644
--- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs
@@ -23,10 +23,12 @@ public class ChatHistorySummarizationReducerTests
[InlineData(-1)]
[InlineData(-1, int.MaxValue)]
[InlineData(int.MaxValue, -1)]
- public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null)
+ public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null)
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService();
+ // Act & Assert
Assert.Throws(() => new ChatHistorySummarizationReducer(mockCompletionService.Object, targetCount, thresholdCount));
}
@@ -34,15 +36,17 @@ public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int?
/// Verify object state after initialization.
///
[Fact]
- public void VerifyChatHistoryInitializationState()
+ public void VerifyInitializationState()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10);
+ // Assert
Assert.Equal(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions);
Assert.True(reducer.FailOnError);
+ // Act
reducer =
new(mockCompletionService.Object, 10)
{
@@ -50,25 +54,62 @@ public void VerifyChatHistoryInitializationState()
SummarizationInstructions = "instructions",
};
+ // Assert
Assert.NotEqual(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions);
Assert.False(reducer.FailOnError);
}
+ ///
+ /// Validate equality override.
+ ///
+ [Fact]
+ public void VerifyEquality()
+ {
+ // Arrange
+ Mock mockCompletionService = this.CreateMockCompletionService();
+
+ ChatHistorySummarizationReducer reducer1 = new(mockCompletionService.Object, 3, 3);
+ ChatHistorySummarizationReducer reducer2 = new(mockCompletionService.Object, 3, 3);
+ ChatHistorySummarizationReducer reducer3 = new(mockCompletionService.Object, 3, 3) { UseSingleSummary = false };
+ ChatHistorySummarizationReducer reducer4 = new(mockCompletionService.Object, 3, 3) { SummarizationInstructions = "override" };
+ ChatHistorySummarizationReducer reducer5 = new(mockCompletionService.Object, 4, 3);
+ ChatHistorySummarizationReducer reducer6 = new(mockCompletionService.Object, 3, 5);
+ ChatHistorySummarizationReducer reducer7 = new(mockCompletionService.Object, 3);
+ ChatHistorySummarizationReducer reducer8 = new(mockCompletionService.Object, 3);
+
+ // Assert
+ Assert.True(reducer1.Equals(reducer1));
+ Assert.True(reducer1.Equals(reducer2));
+ Assert.True(reducer7.Equals(reducer8));
+ Assert.True(reducer3.Equals(reducer3));
+ Assert.True(reducer4.Equals(reducer4));
+ Assert.False(reducer1.Equals(reducer3));
+ Assert.False(reducer1.Equals(reducer4));
+ Assert.False(reducer1.Equals(reducer5));
+ Assert.False(reducer1.Equals(reducer6));
+ Assert.False(reducer1.Equals(reducer7));
+ Assert.False(reducer1.Equals(reducer8));
+ Assert.False(reducer1.Equals(null));
+ }
+
///
/// Validate hash-code expresses reducer equivalency.
///
[Fact]
- public void VerifyChatHistoryHasCode()
+ public void VerifyHashCode()
{
+ // Arrange
HashSet reducers = [];
Mock mockCompletionService = this.CreateMockCompletionService();
+ // Act
int hashCode1 = GenerateHashCode(3, 4);
int hashCode2 = GenerateHashCode(33, 44);
int hashCode3 = GenerateHashCode(3000, 4000);
int hashCode4 = GenerateHashCode(3000, 4000);
+ // Assert
Assert.NotEqual(hashCode1, hashCode2);
Assert.NotEqual(hashCode2, hashCode3);
Assert.Equal(hashCode3, hashCode4);
@@ -90,12 +131,15 @@ int GenerateHashCode(int targetCount, int thresholdCount)
[Fact]
public async Task VerifyChatHistoryReductionSilentFailureAsync()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService(throwException: true);
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10) { FailOnError = false };
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
+ // Assert
Assert.Null(reducedHistory);
}
@@ -105,10 +149,12 @@ public async Task VerifyChatHistoryReductionSilentFailureAsync()
[Fact]
public async Task VerifyChatHistoryReductionThrowsOnFailureAsync()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService(throwException: true);
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10);
+
+ // Act and Assert
await Assert.ThrowsAsync(() => reducer.ReduceAsync(sourceHistory));
}
@@ -118,12 +164,15 @@ public async Task VerifyChatHistoryReductionThrowsOnFailureAsync()
[Fact]
public async Task VerifyChatHistoryNotReducedAsync()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService();
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 20);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
+ // Assert
Assert.Null(reducedHistory);
}
@@ -133,12 +182,15 @@ public async Task VerifyChatHistoryNotReducedAsync()
[Fact]
public async Task VerifyChatHistoryReducedAsync()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService();
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
+ // Assert
ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11);
VerifySummarization(messages[0]);
}
@@ -149,19 +201,24 @@ public async Task VerifyChatHistoryReducedAsync()
[Fact]
public async Task VerifyChatHistoryRereducedAsync()
{
+ // Arrange
Mock mockCompletionService = this.CreateMockCompletionService();
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]);
+ // Assert
ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11);
VerifySummarization(messages[0]);
+ // Act
reducer = new(mockCompletionService.Object, 10) { UseSingleSummary = false };
reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]);
+ // Assert
messages = VerifyReducedHistory(reducedHistory, 12);
VerifySummarization(messages[0]);
VerifySummarization(messages[1]);
diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs
index eebcf8fc6136..9d8b2e721fdf 100644
--- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs
+++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs
@@ -21,24 +21,54 @@ public class ChatHistoryTruncationReducerTests
[InlineData(-1)]
[InlineData(-1, int.MaxValue)]
[InlineData(int.MaxValue, -1)]
- public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null)
+ public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null)
{
+ // Act and Assert
Assert.Throws(() => new ChatHistoryTruncationReducer(targetCount, thresholdCount));
}
+ ///
+ /// Validate equality override.
+ ///
+ [Fact]
+ public void VerifyEquality()
+ {
+ // Arrange
+ ChatHistoryTruncationReducer reducer1 = new(3, 3);
+ ChatHistoryTruncationReducer reducer2 = new(3, 3);
+ ChatHistoryTruncationReducer reducer3 = new(4, 3);
+ ChatHistoryTruncationReducer reducer4 = new(3, 5);
+ ChatHistoryTruncationReducer reducer5 = new(3);
+ ChatHistoryTruncationReducer reducer6 = new(3);
+
+ // Assert
+ Assert.True(reducer1.Equals(reducer1));
+ Assert.True(reducer1.Equals(reducer2));
+ Assert.True(reducer5.Equals(reducer6));
+ Assert.True(reducer3.Equals(reducer3));
+ Assert.False(reducer1.Equals(reducer3));
+ Assert.False(reducer1.Equals(reducer4));
+ Assert.False(reducer1.Equals(reducer5));
+ Assert.False(reducer1.Equals(reducer6));
+ Assert.False(reducer1.Equals(null));
+ }
+
///
/// Validate hash-code expresses reducer equivalency.
///
[Fact]
- public void VerifyChatHistoryHasCode()
+ public void VerifyHashCode()
{
+ // Arrange
HashSet reducers = [];
+ // Act
int hashCode1 = GenerateHashCode(3, 4);
int hashCode2 = GenerateHashCode(33, 44);
int hashCode3 = GenerateHashCode(3000, 4000);
int hashCode4 = GenerateHashCode(3000, 4000);
+ // Assert
Assert.NotEqual(hashCode1, hashCode2);
Assert.NotEqual(hashCode2, hashCode3);
Assert.Equal(hashCode3, hashCode4);
@@ -60,11 +90,14 @@ int GenerateHashCode(int targetCount, int thresholdCount)
[Fact]
public async Task VerifyChatHistoryNotReducedAsync()
{
+ // Arrange
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(10).ToArray();
-
ChatHistoryTruncationReducer reducer = new(20);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
+ // Assert
Assert.Null(reducedHistory);
}
@@ -74,11 +107,14 @@ public async Task VerifyChatHistoryNotReducedAsync()
[Fact]
public async Task VerifyChatHistoryReducedAsync()
{
+ // Arrange
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistoryTruncationReducer reducer = new(10);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
+ // Assert
VerifyReducedHistory(reducedHistory, 10);
}
@@ -88,12 +124,15 @@ public async Task VerifyChatHistoryReducedAsync()
[Fact]
public async Task VerifyChatHistoryRereducedAsync()
{
+ // Arrange
IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray();
-
ChatHistoryTruncationReducer reducer = new(10);
+
+ // Act
IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory);
reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]);
+ // Assert
VerifyReducedHistory(reducedHistory, 10);
}
diff --git a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs
index 14a938a7b169..d7f370e3734c 100644
--- a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs
@@ -19,10 +19,12 @@ public class ChatHistoryExtensionsTests
[Fact]
public void VerifyChatHistoryOrdering()
{
+ // Arrange
ChatHistory history = [];
history.AddUserMessage("Hi");
history.AddAssistantMessage("Hi");
+ // Act and Assert
VerifyRole(AuthorRole.User, history.First());
VerifyRole(AuthorRole.Assistant, history.Last());
@@ -36,10 +38,12 @@ public void VerifyChatHistoryOrdering()
[Fact]
public async Task VerifyChatHistoryOrderingAsync()
{
+ // Arrange
ChatHistory history = [];
history.AddUserMessage("Hi");
history.AddAssistantMessage("Hi");
+ // Act and Assert
VerifyRole(AuthorRole.User, history.First());
VerifyRole(AuthorRole.Assistant, history.Last());
diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs
index 452a0566e11f..96ed232fb109 100644
--- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs
+++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs
@@ -22,8 +22,10 @@ public class BroadcastQueueTests
[Fact]
public void VerifyBroadcastQueueDefaultConfiguration()
{
+ // Arrange
BroadcastQueue queue = new();
+ // Assert
Assert.True(queue.BlockDuration.TotalSeconds > 0);
}
@@ -33,7 +35,7 @@ public void VerifyBroadcastQueueDefaultConfiguration()
[Fact]
public async Task VerifyBroadcastQueueReceiveAsync()
{
- // Create queue and channel.
+ // Arrange: Create queue and channel.
BroadcastQueue queue =
new()
{
@@ -42,23 +44,31 @@ public async Task VerifyBroadcastQueueReceiveAsync()
TestChannel channel = new();
ChannelReference reference = new(channel, "test");
- // Verify initial state
+ // Act: Verify initial state
await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test");
+
+ // Assert
Assert.Empty(channel.ReceivedMessages);
- // Verify empty invocation with no channels.
+ // Act: Verify empty invocation with no channels.
queue.Enqueue([], []);
await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test");
+
+ // Assert
Assert.Empty(channel.ReceivedMessages);
- // Verify empty invocation of channel.
+ // Act: Verify empty invocation of channel.
queue.Enqueue([reference], []);
await VerifyReceivingStateAsync(receiveCount: 1, queue, channel, "test");
+
+ // Assert
Assert.Empty(channel.ReceivedMessages);
- // Verify expected invocation of channel.
+ // Act: Verify expected invocation of channel.
queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]);
await VerifyReceivingStateAsync(receiveCount: 2, queue, channel, "test");
+
+ // Assert
Assert.NotEmpty(channel.ReceivedMessages);
}
@@ -68,7 +78,7 @@ public async Task VerifyBroadcastQueueReceiveAsync()
[Fact]
public async Task VerifyBroadcastQueueFailureAsync()
{
- // Create queue and channel.
+ // Arrange: Create queue and channel.
BroadcastQueue queue =
new()
{
@@ -77,9 +87,10 @@ public async Task VerifyBroadcastQueueFailureAsync()
BadChannel channel = new();
ChannelReference reference = new(channel, "test");
- // Verify expected invocation of channel.
+ // Act: Verify expected invocation of channel.
queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]);
+ // Assert
await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference));
await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference));
await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference));
@@ -91,7 +102,7 @@ public async Task VerifyBroadcastQueueFailureAsync()
[Fact]
public async Task VerifyBroadcastQueueConcurrencyAsync()
{
- // Create queue and channel.
+ // Arrange: Create queue and channel.
BroadcastQueue queue =
new()
{
@@ -100,7 +111,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync()
TestChannel channel = new();
ChannelReference reference = new(channel, "test");
- // Enqueue multiple channels
+ // Act: Enqueue multiple channels
for (int count = 0; count < 10; ++count)
{
queue.Enqueue([new(channel, $"test{count}")], [new ChatMessageContent(AuthorRole.User, "hi")]);
@@ -112,7 +123,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync()
await queue.EnsureSynchronizedAsync(new ChannelReference(channel, $"test{count}"));
}
- // Verify result
+ // Assert
Assert.NotEmpty(channel.ReceivedMessages);
Assert.Equal(10, channel.ReceivedMessages.Count);
}
diff --git a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs
index 0a9715f25115..13cc3203d58c 100644
--- a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs
+++ b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs
@@ -17,21 +17,24 @@ public class KeyEncoderTests
[Fact]
public void VerifyKeyEncoderUniqueness()
{
+ // Act
this.VerifyHashEquivalancy([]);
this.VerifyHashEquivalancy(nameof(KeyEncoderTests));
this.VerifyHashEquivalancy(nameof(KeyEncoderTests), "http://localhost", "zoo");
- // Verify "well-known" value
+ // Assert: Verify "well-known" value
string localHash = KeyEncoder.GenerateHash([typeof(ChatHistoryChannel).FullName!]);
Assert.Equal("Vdx37EnWT9BS+kkCkEgFCg9uHvHNw1+hXMA4sgNMKs4=", localHash);
}
private void VerifyHashEquivalancy(params string[] keys)
{
+ // Act
string hash1 = KeyEncoder.GenerateHash(keys);
string hash2 = KeyEncoder.GenerateHash(keys);
string hash3 = KeyEncoder.GenerateHash(keys.Concat(["another"]));
+ // Assert
Assert.Equal(hash1, hash2);
Assert.NotEqual(hash1, hash3);
}
diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs
index f3b833024001..2535446dae7b 100644
--- a/dotnet/src/Agents/UnitTests/MockAgent.cs
+++ b/dotnet/src/Agents/UnitTests/MockAgent.cs
@@ -1,4 +1,5 @@
// Copyright (c) Microsoft. All rights reserved.
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
@@ -14,7 +15,7 @@ namespace SemanticKernel.Agents.UnitTests;
///
/// Mock definition of with a contract.
///
-internal sealed class MockAgent : KernelAgent, IChatHistoryHandler
+internal class MockAgent : KernelAgent, IChatHistoryHandler
{
public int InvokeCount { get; private set; }
@@ -46,7 +47,7 @@ public IAsyncEnumerable InvokeStreamingAsync(
///
protected internal override IEnumerable GetChannelKeys()
{
- yield return typeof(ChatHistoryChannel).FullName!;
+ yield return Guid.NewGuid().ToString();
}
///
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs
new file mode 100644
index 000000000000..cd51c736ac18
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs
@@ -0,0 +1,46 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+internal static class AssertCollection
+{
+ public static void Equal(IReadOnlyList? source, IReadOnlyList? target, Func? adapter = null)
+ {
+ if (source == null)
+ {
+ Assert.Null(target);
+ return;
+ }
+
+ Assert.NotNull(target);
+ Assert.Equal(source.Count, target.Count);
+
+ adapter ??= (x) => x;
+
+ for (int i = 0; i < source.Count; i++)
+ {
+ Assert.Equal(adapter(source[i]), adapter(target[i]));
+ }
+ }
+
+ public static void Equal(IReadOnlyDictionary? source, IReadOnlyDictionary? target)
+ {
+ if (source == null)
+ {
+ Assert.Null(target);
+ return;
+ }
+
+ Assert.NotNull(target);
+ Assert.Equal(source.Count, target.Count);
+
+ foreach ((TKey key, TValue value) in source)
+ {
+ Assert.True(target.TryGetValue(key, out TValue? targetValue));
+ Assert.Equal(value, targetValue);
+ }
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs
index b1e4d397eded..6288c6a5aed8 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs
@@ -2,7 +2,7 @@
using System.Linq;
using Azure.Core;
using Azure.Core.Pipeline;
-using Microsoft.SemanticKernel.Agents.OpenAI.Azure;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.OpenAI.Azure;
@@ -18,14 +18,17 @@ public class AddHeaderRequestPolicyTests
[Fact]
public void VerifyAddHeaderRequestPolicyExecution()
{
+ // Arrange
using HttpClientTransport clientTransport = new();
HttpPipeline pipeline = new(clientTransport);
HttpMessage message = pipeline.CreateMessage();
-
AddHeaderRequestPolicy policy = new(headerName: "testname", headerValue: "testvalue");
+
+ // Act
policy.OnSendingRequest(message);
+ // Assert
Assert.Single(message.Request.Headers);
HttpHeader header = message.Request.Headers.Single();
Assert.Equal("testname", header.Name);
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs
index 0b0a0707e49a..97dbf32903d6 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft. All rights reserved.
-using Azure.AI.OpenAI.Assistants;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Assistants;
using Xunit;
using KernelExtensions = Microsoft.SemanticKernel.Agents.OpenAI;
@@ -29,7 +29,10 @@ public void VerifyToMessageRole()
private void VerifyRoleConversion(AuthorRole inputRole, MessageRole expectedRole)
{
+ // Arrange
MessageRole convertedRole = inputRole.ToMessageRole();
+
+ // Assert
Assert.Equal(expectedRole, convertedRole);
}
}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs
index 3f982f3a7b47..70c27ccb2152 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs
@@ -17,11 +17,15 @@ public class KernelExtensionsTests
[Fact]
public void VerifyGetKernelFunctionLookup()
{
+ // Arrange
Kernel kernel = new();
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
kernel.Plugins.Add(plugin);
+ // Act
KernelFunction function = kernel.GetKernelFunction($"{nameof(TestPlugin)}-{nameof(TestPlugin.TestFunction)}", '-');
+
+ // Assert
Assert.NotNull(function);
Assert.Equal(nameof(TestPlugin.TestFunction), function.Name);
}
@@ -32,10 +36,12 @@ public void VerifyGetKernelFunctionLookup()
[Fact]
public void VerifyGetKernelFunctionInvalid()
{
+ // Arrange
Kernel kernel = new();
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
kernel.Plugins.Add(plugin);
+ // Act and Assert
Assert.Throws(() => kernel.GetKernelFunction("a", '-'));
Assert.Throws(() => kernel.GetKernelFunction("a-b", ':'));
Assert.Throws(() => kernel.GetKernelFunction("a-b-c", '-'));
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
index eeb8a4d3b9d1..acf195840366 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs
@@ -1,9 +1,9 @@
// Copyright (c) Microsoft. All rights reserved.
using System;
using System.ComponentModel;
-using Azure.AI.OpenAI.Assistants;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents.OpenAI;
+using OpenAI.Assistants;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions;
@@ -19,18 +19,28 @@ public class KernelFunctionExtensionsTests
[Fact]
public void VerifyKernelFunctionToFunctionTool()
{
+ // Arrange
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
+
+ // Assert
Assert.Equal(2, plugin.FunctionCount);
+ // Arrange
KernelFunction f1 = plugin[nameof(TestPlugin.TestFunction1)];
KernelFunction f2 = plugin[nameof(TestPlugin.TestFunction2)];
- FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin", "-");
- Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.Name, StringComparison.Ordinal);
+ // Act
+ FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin");
+
+ // Assert
+ Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.FunctionName, StringComparison.Ordinal);
Assert.Equal("test description", definition1.Description);
- FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin", "-");
- Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.Name, StringComparison.Ordinal);
+ // Act
+ FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin");
+
+ // Assert
+ Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.FunctionName, StringComparison.Ordinal);
Assert.Equal("test description", definition2.Description);
}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs
new file mode 100644
index 000000000000..50dec2cb95ae
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs
@@ -0,0 +1,210 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Assistants;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal;
+
+///
+/// Unit testing of .
+///
+public class AssistantMessageFactoryTests
+{
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterCreateOptionsDefault()
+ {
+ // Arrange (Setup message with null metadata)
+ ChatMessageContent message = new(AuthorRole.User, "test");
+
+ // Act: Create options
+ MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.Empty(options.Metadata);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty()
+ {
+ // Arrange Setup message with empty metadata
+ ChatMessageContent message =
+ new(AuthorRole.User, "test")
+ {
+ Metadata = new Dictionary()
+ };
+
+ // Act: Create options
+ MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.Empty(options.Metadata);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata()
+ {
+ // Arrange: Setup message with metadata
+ ChatMessageContent message =
+ new(AuthorRole.User, "test")
+ {
+ Metadata =
+ new Dictionary()
+ {
+ { "a", 1 },
+ { "b", "2" },
+ }
+ };
+
+ // Act: Create options
+ MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.NotEmpty(options.Metadata);
+ Assert.Equal(2, options.Metadata.Count);
+ Assert.Equal("1", options.Metadata["a"]);
+ Assert.Equal("2", options.Metadata["b"]);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull()
+ {
+ // Arrange: Setup message with null metadata value
+ ChatMessageContent message =
+ new(AuthorRole.User, "test")
+ {
+ Metadata =
+ new Dictionary()
+ {
+ { "a", null },
+ { "b", "2" },
+ }
+ };
+
+ // Act: Create options
+ MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.NotEmpty(options.Metadata);
+ Assert.Equal(2, options.Metadata.Count);
+ Assert.Equal(string.Empty, options.Metadata["a"]);
+ Assert.Equal("2", options.Metadata["b"]);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterGetMessageContentsWithText()
+ {
+ // Arrange
+ ChatMessageContent message = new(AuthorRole.User, items: [new TextContent("test")]);
+
+ // Act
+ MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();
+
+ // Assert
+ Assert.NotNull(contents);
+ Assert.Single(contents);
+ Assert.NotNull(contents.Single().Text);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterGetMessageWithImageUrl()
+ {
+ // Arrange
+ ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new Uri("https://localhost/myimage.png"))]);
+
+ // Act
+ MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();
+
+ // Assert
+ Assert.NotNull(contents);
+ Assert.Single(contents);
+ Assert.NotNull(contents.Single().ImageUrl);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact(Skip = "API bug with data Uri construction")]
+ public void VerifyAssistantMessageAdapterGetMessageWithImageData()
+ {
+ // Arrange
+ ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]);
+
+ // Act
+ MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();
+
+ // Assert
+ Assert.NotNull(contents);
+ Assert.Single(contents);
+ Assert.NotNull(contents.Single().ImageUrl);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterGetMessageWithImageFile()
+ {
+ // Arrange
+ ChatMessageContent message = new(AuthorRole.User, items: [new FileReferenceContent("file-id")]);
+
+ // Act
+ MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();
+
+ // Assert
+ Assert.NotNull(contents);
+ Assert.Single(contents);
+ Assert.NotNull(contents.Single().ImageFileId);
+ }
+
+ ///
+ /// Verify options creation.
+ ///
+ [Fact]
+ public void VerifyAssistantMessageAdapterGetMessageWithAll()
+ {
+ // Arrange
+ ChatMessageContent message =
+ new(
+ AuthorRole.User,
+ items:
+ [
+ new TextContent("test"),
+ new ImageContent(new Uri("https://localhost/myimage.png")),
+ new FileReferenceContent("file-id")
+ ]);
+
+ // Act
+ MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray();
+
+ // Assert
+ Assert.NotNull(contents);
+ Assert.Equal(3, contents.Length);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs
new file mode 100644
index 000000000000..d6bcf91b8a94
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs
@@ -0,0 +1,139 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
+using OpenAI.Assistants;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal;
+
+///
+/// Unit testing of .
+///
+public class AssistantRunOptionsFactoryTests
+{
+ ///
+ /// Verify run options generation with null .
+ ///
+ [Fact]
+ public void AssistantRunOptionsFactoryExecutionOptionsNullTest()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("gpt-anything")
+ {
+ Temperature = 0.5F,
+ };
+
+ // Act
+ RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, null);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.Null(options.Temperature);
+ Assert.Null(options.NucleusSamplingFactor);
+ Assert.Empty(options.Metadata);
+ }
+
+ ///
+ /// Verify run options generation with equivalent .
+ ///
+ [Fact]
+ public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("gpt-anything")
+ {
+ Temperature = 0.5F,
+ };
+
+ OpenAIAssistantInvocationOptions invocationOptions =
+ new()
+ {
+ Temperature = 0.5F,
+ };
+
+ // Act
+ RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.Null(options.Temperature);
+ Assert.Null(options.NucleusSamplingFactor);
+ }
+
+ ///
+ /// Verify run options generation with override.
+ ///
+ [Fact]
+ public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("gpt-anything")
+ {
+ Temperature = 0.5F,
+ ExecutionOptions =
+ new()
+ {
+ TruncationMessageCount = 5,
+ },
+ };
+
+ OpenAIAssistantInvocationOptions invocationOptions =
+ new()
+ {
+ Temperature = 0.9F,
+ TruncationMessageCount = 8,
+ EnableJsonResponse = true,
+ };
+
+ // Act
+ RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions);
+
+ // Assert
+ Assert.NotNull(options);
+ Assert.Equal(0.9F, options.Temperature);
+ Assert.Equal(8, options.TruncationStrategy.LastMessages);
+ Assert.Equal(AssistantResponseFormat.JsonObject, options.ResponseFormat);
+ Assert.Null(options.NucleusSamplingFactor);
+ }
+
+ ///
+ /// Verify run options generation with metadata.
+ ///
+ [Fact]
+ public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("gpt-anything")
+ {
+ Temperature = 0.5F,
+ ExecutionOptions =
+ new()
+ {
+ TruncationMessageCount = 5,
+ },
+ };
+
+ OpenAIAssistantInvocationOptions invocationOptions =
+ new()
+ {
+ Metadata = new Dictionary
+ {
+ { "key1", "value" },
+ { "key2", null! },
+ },
+ };
+
+ // Act
+ RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions);
+
+ // Assert
+ Assert.Equal(2, options.Metadata.Count);
+ Assert.Equal("value", options.Metadata["key1"]);
+ Assert.Equal(string.Empty, options.Metadata["key2"]);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
index 1d9a9ec9dfcf..ef67c48f1473 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs
@@ -4,12 +4,14 @@
using System.Linq;
using System.Net;
using System.Net.Http;
+using System.Text;
+using System.Text.Json;
using System.Threading.Tasks;
-using Azure.AI.OpenAI.Assistants;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Assistants;
using Xunit;
namespace SemanticKernel.Agents.UnitTests.OpenAI;
@@ -30,100 +32,257 @@ public sealed class OpenAIAssistantAgentTests : IDisposable
[Fact]
public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync()
{
- OpenAIAssistantDefinition definition =
- new()
- {
- ModelId = "testmodel",
- };
-
- this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple);
-
- OpenAIAssistantAgent agent =
- await OpenAIAssistantAgent.CreateAsync(
- this._emptyKernel,
- this.CreateTestConfiguration(targetAzure: true, useVersion: true),
- definition);
+ // Arrange
+ OpenAIAssistantDefinition definition = new("testmodel");
- Assert.NotNull(agent);
- Assert.NotNull(agent.Id);
- Assert.Null(agent.Instructions);
- Assert.Null(agent.Name);
- Assert.Null(agent.Description);
- Assert.False(agent.IsDeleted);
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
}
///
/// Verify the invocation and response of
- /// for an agent with optional properties defined.
+ /// for an agent with name, instructions, and description.
///
[Fact]
public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync()
{
+ // Arrange
OpenAIAssistantDefinition definition =
- new()
+ new("testmodel")
{
- ModelId = "testmodel",
Name = "testname",
Description = "testdescription",
Instructions = "testinstructions",
};
- this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentFull);
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
- OpenAIAssistantAgent agent =
- await OpenAIAssistantAgent.CreateAsync(
- this._emptyKernel,
- this.CreateTestConfiguration(),
- definition);
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with code-interpreter enabled.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ EnableCodeInterpreter = true,
+ };
- Assert.NotNull(agent);
- Assert.NotNull(agent.Id);
- Assert.NotNull(agent.Instructions);
- Assert.NotNull(agent.Name);
- Assert.NotNull(agent.Description);
- Assert.False(agent.IsDeleted);
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
}
///
/// Verify the invocation and response of
- /// for an agent that has all properties defined..
+ /// for an agent with code-interpreter files.
///
[Fact]
- public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync()
+ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsync()
{
+ // Arrange
OpenAIAssistantDefinition definition =
- new()
+ new("testmodel")
{
- ModelId = "testmodel",
EnableCodeInterpreter = true,
- EnableRetrieval = true,
- FileIds = ["#1", "#2"],
- Metadata = new Dictionary() { { "a", "1" } },
+ CodeInterpreterFileIds = ["file1", "file2"],
};
- this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything);
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
- OpenAIAssistantAgent agent =
- await OpenAIAssistantAgent.CreateAsync(
- this._emptyKernel,
- this.CreateTestConfiguration(),
- definition);
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with a file-search and no vector-store
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ EnableFileSearch = true,
+ };
- Assert.NotNull(agent);
- Assert.Equal(2, agent.Tools.Count);
- Assert.True(agent.Tools.OfType().Any());
- Assert.True(agent.Tools.OfType().Any());
- Assert.NotEmpty(agent.FileIds);
- Assert.NotEmpty(agent.Metadata);
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with a vector-store-id (for file-search).
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ EnableFileSearch = true,
+ VectorStoreId = "#vs1",
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with metadata.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ Metadata = new Dictionary()
+ {
+ { "a", "1" },
+ { "b", "2" },
+ },
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with json-response mode enabled.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ EnableJsonResponse = true,
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with temperature defined.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ Temperature = 2.0F,
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with topP defined.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ TopP = 2.0F,
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with empty execution settings.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ ExecutionOptions = new OpenAIAssistantExecutionOptions(),
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with populated execution settings.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ ExecutionOptions =
+ new()
+ {
+ MaxCompletionTokens = 100,
+ ParallelToolCallsEnabled = false,
+ }
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
+ }
+
+ ///
+ /// Verify the invocation and response of
+ /// for an agent with execution settings and meta-data.
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAndMetadataAsync()
+ {
+ // Arrange
+ OpenAIAssistantDefinition definition =
+ new("testmodel")
+ {
+ ExecutionOptions = new(),
+ Metadata = new Dictionary()
+ {
+ { "a", "1" },
+ { "b", "2" },
+ },
+ };
+
+ // Act and Assert
+ await this.VerifyAgentCreationAsync(definition);
}
///
/// Verify the invocation and response of .
///
[Fact]
- public async Task VerifyOpenAIAssistantAgentRetrieveAsync()
+ public async Task VerifyOpenAIAssistantAgentRetrievalAsync()
{
- this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple);
+ // Arrange
+ OpenAIAssistantDefinition definition = new("testmodel");
+
+ this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition));
OpenAIAssistantAgent agent =
await OpenAIAssistantAgent.RetrieveAsync(
@@ -131,12 +290,8 @@ await OpenAIAssistantAgent.RetrieveAsync(
this.CreateTestConfiguration(),
"#id");
- Assert.NotNull(agent);
- Assert.NotNull(agent.Id);
- Assert.Null(agent.Instructions);
- Assert.Null(agent.Name);
- Assert.Null(agent.Description);
- Assert.False(agent.IsDeleted);
+ // Act and Assert
+ ValidateAgentDefinition(agent, definition);
}
///
@@ -145,16 +300,50 @@ await OpenAIAssistantAgent.RetrieveAsync(
[Fact]
public async Task VerifyOpenAIAssistantAgentDeleteAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
+ // Assert
Assert.False(agent.IsDeleted);
+ // Arrange
this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteAgent);
+ // Act
await agent.DeleteAsync();
+ // Assert
Assert.True(agent.IsDeleted);
+ // Act
await agent.DeleteAsync(); // Doesn't throw
+ // Assert
Assert.True(agent.IsDeleted);
+ await Assert.ThrowsAsync(() => agent.AddChatMessageAsync("threadid", new(AuthorRole.User, "test")));
+ await Assert.ThrowsAsync(() => agent.InvokeAsync("threadid").ToArrayAsync().AsTask());
+ }
+
+ ///
+ /// Verify the deletion of agent via .
+ ///
+ [Fact]
+ public async Task VerifyOpenAIAssistantAgentCreateThreadAsync()
+ {
+ // Arrange
+ OpenAIAssistantAgent agent = await this.CreateAgentAsync();
+
+ this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread);
+
+ // Act
+ string threadId = await agent.CreateThreadAsync();
+ // Assert
+ Assert.NotNull(threadId);
+
+ // Arrange
+ this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread);
+
+ // Act
+ threadId = await agent.CreateThreadAsync(new());
+ // Assert
+ Assert.NotNull(threadId);
}
///
@@ -163,6 +352,7 @@ public async Task VerifyOpenAIAssistantAgentDeleteAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
this.SetupResponses(
@@ -174,7 +364,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync()
ResponseContent.GetTextMessage);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.Single(messages[0].Items);
Assert.IsType(messages[0].Items[0]);
@@ -186,6 +380,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
this.SetupResponses(
@@ -197,7 +392,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync()
ResponseContent.GetTextMessageWithAnnotation);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.Equal(2, messages[0].Items.Count);
Assert.NotNull(messages[0].Items.SingleOrDefault(c => c is TextContent));
@@ -210,6 +409,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
this.SetupResponses(
@@ -221,7 +421,11 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync()
ResponseContent.GetImageMessage);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.Single(messages[0].Items);
Assert.IsType(messages[0].Items[0]);
@@ -233,7 +437,7 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentGetMessagesAsync()
{
- // Create agent
+ // Arrange: Create agent
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
// Initialize agent channel
@@ -246,18 +450,22 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync()
ResponseContent.GetTextMessage);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+ // Assert
Assert.Single(messages);
- // Setup messages
+ // Arrange: Setup messages
this.SetupResponses(
HttpStatusCode.OK,
ResponseContent.ListMessagesPageMore,
ResponseContent.ListMessagesPageMore,
ResponseContent.ListMessagesPageFinal);
- // Get messages and verify
+ // Act: Get messages
messages = await chat.GetChatMessagesAsync(agent).ToArrayAsync();
+ // Assert
Assert.Equal(5, messages.Length);
}
@@ -267,7 +475,7 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentAddMessagesAsync()
{
- // Create agent
+ // Arrange: Create agent
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
// Initialize agent channel
@@ -279,12 +487,18 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync()
ResponseContent.MessageSteps,
ResponseContent.GetTextMessage);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+ // Assert
Assert.Single(messages);
+ // Arrange
chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi"));
+ // Act
messages = await chat.GetChatMessagesAsync().ToArrayAsync();
+ // Assert
Assert.Equal(2, messages.Length);
}
@@ -294,6 +508,7 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync()
[Fact]
public async Task VerifyOpenAIAssistantAgentListDefinitionAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
this.SetupResponses(
@@ -302,20 +517,24 @@ public async Task VerifyOpenAIAssistantAgentListDefinitionAsync()
ResponseContent.ListAgentsPageMore,
ResponseContent.ListAgentsPageFinal);
+ // Act
var messages =
await OpenAIAssistantAgent.ListDefinitionsAsync(
this.CreateTestConfiguration()).ToArrayAsync();
+ // Assert
Assert.Equal(7, messages.Length);
+ // Arrange
this.SetupResponses(
HttpStatusCode.OK,
ResponseContent.ListAgentsPageMore,
- ResponseContent.ListAgentsPageMore);
+ ResponseContent.ListAgentsPageFinal);
+ // Act
messages =
await OpenAIAssistantAgent.ListDefinitionsAsync(
- this.CreateTestConfiguration(),
- maxResults: 4).ToArrayAsync();
+ this.CreateTestConfiguration()).ToArrayAsync();
+ // Assert
Assert.Equal(4, messages.Length);
}
@@ -325,6 +544,7 @@ await OpenAIAssistantAgent.ListDefinitionsAsync(
[Fact]
public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync()
{
+ // Arrange
OpenAIAssistantAgent agent = await this.CreateAgentAsync();
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
@@ -342,7 +562,11 @@ public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync()
ResponseContent.GetTextMessage);
AgentGroupChat chat = new();
+
+ // Act
ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync();
+
+ // Assert
Assert.Single(messages);
Assert.Single(messages[0].Items);
Assert.IsType(messages[0].Items[0]);
@@ -365,15 +589,95 @@ public OpenAIAssistantAgentTests()
this._emptyKernel = new Kernel();
}
- private Task CreateAgentAsync()
+ private async Task VerifyAgentCreationAsync(OpenAIAssistantDefinition definition)
{
- OpenAIAssistantDefinition definition =
- new()
+ this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition));
+
+ OpenAIAssistantAgent agent =
+ await OpenAIAssistantAgent.CreateAsync(
+ this._emptyKernel,
+ this.CreateTestConfiguration(),
+ definition);
+
+ ValidateAgentDefinition(agent, definition);
+ }
+
+ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAssistantDefinition sourceDefinition)
+ {
+ // Verify fundamental state
+ Assert.NotNull(agent);
+ Assert.NotNull(agent.Id);
+ Assert.False(agent.IsDeleted);
+ Assert.NotNull(agent.Definition);
+ Assert.Equal(sourceDefinition.ModelId, agent.Definition.ModelId);
+
+ // Verify core properties
+ Assert.Equal(sourceDefinition.Instructions ?? string.Empty, agent.Instructions);
+ Assert.Equal(sourceDefinition.Name ?? string.Empty, agent.Name);
+ Assert.Equal(sourceDefinition.Description ?? string.Empty, agent.Description);
+
+ // Verify options
+ Assert.Equal(sourceDefinition.Temperature, agent.Definition.Temperature);
+ Assert.Equal(sourceDefinition.TopP, agent.Definition.TopP);
+ Assert.Equal(sourceDefinition.ExecutionOptions?.MaxCompletionTokens, agent.Definition.ExecutionOptions?.MaxCompletionTokens);
+ Assert.Equal(sourceDefinition.ExecutionOptions?.MaxPromptTokens, agent.Definition.ExecutionOptions?.MaxPromptTokens);
+ Assert.Equal(sourceDefinition.ExecutionOptions?.ParallelToolCallsEnabled, agent.Definition.ExecutionOptions?.ParallelToolCallsEnabled);
+ Assert.Equal(sourceDefinition.ExecutionOptions?.TruncationMessageCount, agent.Definition.ExecutionOptions?.TruncationMessageCount);
+
+ // Verify tool definitions
+ int expectedToolCount = 0;
+
+ bool hasCodeInterpreter = false;
+ if (sourceDefinition.EnableCodeInterpreter)
+ {
+ hasCodeInterpreter = true;
+ ++expectedToolCount;
+ }
+
+ Assert.Equal(hasCodeInterpreter, agent.Tools.OfType().Any());
+
+ bool hasFileSearch = false;
+ if (sourceDefinition.EnableFileSearch)
+ {
+ hasFileSearch = true;
+ ++expectedToolCount;
+ }
+
+ Assert.Equal(hasFileSearch, agent.Tools.OfType().Any());
+
+ Assert.Equal(expectedToolCount, agent.Tools.Count);
+
+ // Verify metadata
+ Assert.NotNull(agent.Definition.Metadata);
+ if (sourceDefinition.ExecutionOptions == null)
+ {
+ Assert.Equal(sourceDefinition.Metadata ?? new Dictionary(), agent.Definition.Metadata);
+ }
+ else // Additional metadata present when execution options are defined
+ {
+ Assert.Equal((sourceDefinition.Metadata?.Count ?? 0) + 1, agent.Definition.Metadata.Count);
+
+ if (sourceDefinition.Metadata != null)
{
- ModelId = "testmodel",
- };
+ foreach (var (key, value) in sourceDefinition.Metadata)
+ {
+ string? targetValue = agent.Definition.Metadata[key];
+ Assert.NotNull(targetValue);
+ Assert.Equal(value, targetValue);
+ }
+ }
+ }
+
+ // Verify detail definition
+ Assert.Equal(sourceDefinition.VectorStoreId, agent.Definition.VectorStoreId);
+ Assert.Equal(sourceDefinition.CodeInterpreterFileIds, agent.Definition.CodeInterpreterFileIds);
+ }
- this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple);
+ private Task CreateAgentAsync()
+ {
+ OpenAIAssistantDefinition definition = new("testmodel");
+
+ this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition));
return
OpenAIAssistantAgent.CreateAsync(
@@ -382,14 +686,10 @@ private Task CreateAgentAsync()
definition);
}
- private OpenAIAssistantConfiguration CreateTestConfiguration(bool targetAzure = false, bool useVersion = false)
- {
- return new(apiKey: "fakekey", endpoint: targetAzure ? "https://localhost" : null)
- {
- HttpClient = this._httpClient,
- Version = useVersion ? AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview : null,
- };
- }
+ private OpenAIClientProvider CreateTestConfiguration(bool targetAzure = false)
+ => targetAzure ?
+ OpenAIClientProvider.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) :
+ OpenAIClientProvider.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient);
private void SetupResponse(HttpStatusCode statusCode, string content)
{
@@ -423,58 +723,114 @@ public void MyFunction(int index)
private static class ResponseContent
{
- public const string CreateAgentSimple =
- """
+ public static string CreateAgentPayload(OpenAIAssistantDefinition definition)
+ {
+ StringBuilder builder = new();
+ builder.AppendLine("{");
+ builder.AppendLine(@" ""id"": ""asst_abc123"",");
+ builder.AppendLine(@" ""object"": ""assistant"",");
+ builder.AppendLine(@" ""created_at"": 1698984975,");
+ builder.AppendLine(@$" ""name"": ""{definition.Name}"",");
+ builder.AppendLine(@$" ""description"": ""{definition.Description}"",");
+ builder.AppendLine(@$" ""instructions"": ""{definition.Instructions}"",");
+ builder.AppendLine(@$" ""model"": ""{definition.ModelId}"",");
+
+ bool hasCodeInterpreter = definition.EnableCodeInterpreter;
+ bool hasCodeInterpreterFiles = (definition.CodeInterpreterFileIds?.Count ?? 0) > 0;
+ bool hasFileSearch = definition.EnableFileSearch;
+ if (!hasCodeInterpreter && !hasFileSearch)
{
- "id": "asst_abc123",
- "object": "assistant",
- "created_at": 1698984975,
- "name": null,
- "description": null,
- "model": "gpt-4-turbo",
- "instructions": null,
- "tools": [],
- "file_ids": [],
- "metadata": {}
+ builder.AppendLine(@" ""tools"": [],");
}
- """;
+ else
+ {
+ builder.AppendLine(@" ""tools"": [");
- public const string CreateAgentFull =
- """
+ if (hasCodeInterpreter)
+ {
+ builder.Append(@$" {{ ""type"": ""code_interpreter"" }}{(hasFileSearch ? "," : string.Empty)}");
+ }
+
+ if (hasFileSearch)
+ {
+ builder.AppendLine(@" { ""type"": ""file_search"" }");
+ }
+
+ builder.AppendLine(" ],");
+ }
+
+ if (!hasCodeInterpreterFiles && !hasFileSearch)
{
- "id": "asst_abc123",
- "object": "assistant",
- "created_at": 1698984975,
- "name": "testname",
- "description": "testdescription",
- "model": "gpt-4-turbo",
- "instructions": "testinstructions",
- "tools": [],
- "file_ids": [],
- "metadata": {}
+ builder.AppendLine(@" ""tool_resources"": {},");
}
- """;
+ else
+ {
+ builder.AppendLine(@" ""tool_resources"": {");
- public const string CreateAgentWithEverything =
- """
+ if (hasCodeInterpreterFiles)
+ {
+ string fileIds = string.Join(",", definition.CodeInterpreterFileIds!.Select(fileId => "\"" + fileId + "\""));
+ builder.AppendLine(@$" ""code_interpreter"": {{ ""file_ids"": [{fileIds}] }}{(hasFileSearch ? "," : string.Empty)}");
+ }
+
+ if (hasFileSearch)
+ {
+ builder.AppendLine(@$" ""file_search"": {{ ""vector_store_ids"": [""{definition.VectorStoreId}""] }}");
+ }
+
+ builder.AppendLine(" },");
+ }
+
+ if (definition.Temperature.HasValue)
{
- "id": "asst_abc123",
- "object": "assistant",
- "created_at": 1698984975,
- "name": null,
- "description": null,
- "model": "gpt-4-turbo",
- "instructions": null,
- "tools": [
+ builder.AppendLine(@$" ""temperature"": {definition.Temperature},");
+ }
+
+ if (definition.TopP.HasValue)
+ {
+ builder.AppendLine(@$" ""top_p"": {definition.TopP},");
+ }
+
+ bool hasExecutionOptions = definition.ExecutionOptions != null;
+ int metadataCount = (definition.Metadata?.Count ?? 0);
+ if (metadataCount == 0 && !hasExecutionOptions)
+ {
+ builder.AppendLine(@" ""metadata"": {}");
+ }
+ else
+ {
+ int index = 0;
+ builder.AppendLine(@" ""metadata"": {");
+
+ if (hasExecutionOptions)
{
- "type": "code_interpreter"
- },
+ string serializedExecutionOptions = JsonSerializer.Serialize(definition.ExecutionOptions);
+ builder.AppendLine(@$" ""{OpenAIAssistantAgent.OptionsMetadataKey}"": ""{JsonEncodedText.Encode(serializedExecutionOptions)}""{(metadataCount > 0 ? "," : string.Empty)}");
+ }
+
+ if (metadataCount > 0)
{
- "type": "retrieval"
+ foreach (var (key, value) in definition.Metadata!)
+ {
+ builder.AppendLine(@$" ""{key}"": ""{value}""{(index < metadataCount - 1 ? "," : string.Empty)}");
+ ++index;
+ }
}
- ],
- "file_ids": ["#1", "#2"],
- "metadata": {"a": "1"}
+
+ builder.AppendLine(" }");
+ }
+
+ builder.AppendLine("}");
+
+ return builder.ToString();
+ }
+
+ public const string CreateAgentWithEverything =
+ """
+ {
+ "tool_resources": {
+ "file_search": { "vector_store_ids": ["#vs"] }
+ },
}
""";
@@ -748,7 +1104,6 @@ private static class ResponseContent
"model": "gpt-4-turbo",
"instructions": "You are a helpful assistant designed to make me better at coding!",
"tools": [],
- "file_ids": [],
"metadata": {}
},
{
@@ -760,7 +1115,6 @@ private static class ResponseContent
"model": "gpt-4-turbo",
"instructions": "You are a helpful assistant designed to make me better at coding!",
"tools": [],
- "file_ids": [],
"metadata": {}
},
{
@@ -772,7 +1126,6 @@ private static class ResponseContent
"model": "gpt-4-turbo",
"instructions": null,
"tools": [],
- "file_ids": [],
"metadata": {}
}
],
@@ -796,7 +1149,6 @@ private static class ResponseContent
"model": "gpt-4-turbo",
"instructions": "You are a helpful assistant designed to make me better at coding!",
"tools": [],
- "file_ids": [],
"metadata": {}
}
],
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs
deleted file mode 100644
index 3708ab50ab97..000000000000
--- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (c) Microsoft. All rights reserved.
-using System;
-using System.Net.Http;
-using Azure.AI.OpenAI.Assistants;
-using Microsoft.SemanticKernel.Agents.OpenAI;
-using Xunit;
-
-namespace SemanticKernel.Agents.UnitTests.OpenAI;
-
-///
-/// Unit testing of .
-///
-public class OpenAIAssistantConfigurationTests
-{
- ///
- /// Verify initial state.
- ///
- [Fact]
- public void VerifyOpenAIAssistantConfigurationInitialState()
- {
- OpenAIAssistantConfiguration config = new(apiKey: "testkey");
-
- Assert.Equal("testkey", config.ApiKey);
- Assert.Null(config.Endpoint);
- Assert.Null(config.HttpClient);
- Assert.Null(config.Version);
- }
-
- ///
- /// Verify assignment.
- ///
- [Fact]
- public void VerifyOpenAIAssistantConfigurationAssignment()
- {
- using HttpClient client = new();
-
- OpenAIAssistantConfiguration config =
- new(apiKey: "testkey", endpoint: "https://localhost")
- {
- HttpClient = client,
- Version = AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview,
- };
-
- Assert.Equal("testkey", config.ApiKey);
- Assert.Equal("https://localhost", config.Endpoint);
- Assert.NotNull(config.HttpClient);
- Assert.Equal(AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, config.Version);
- }
-
- ///
- /// Verify secure endpoint.
- ///
- [Fact]
- public void VerifyOpenAIAssistantConfigurationThrows()
- {
- using HttpClient client = new();
-
- Assert.Throws(
- () => new OpenAIAssistantConfiguration(apiKey: "testkey", endpoint: "http://localhost"));
- }
-}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs
index b17b61211c18..f8547f375f13 100644
--- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs
@@ -1,5 +1,6 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Collections.Generic;
+using System.Text.Json;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Xunit;
@@ -16,17 +17,27 @@ public class OpenAIAssistantDefinitionTests
[Fact]
public void VerifyOpenAIAssistantDefinitionInitialState()
{
- OpenAIAssistantDefinition definition = new();
+ // Arrange
+ OpenAIAssistantDefinition definition = new("testmodel");
- Assert.Null(definition.Id);
+ // Assert
+ Assert.Equal(string.Empty, definition.Id);
+ Assert.Equal("testmodel", definition.ModelId);
Assert.Null(definition.Name);
- Assert.Null(definition.ModelId);
Assert.Null(definition.Instructions);
Assert.Null(definition.Description);
Assert.Null(definition.Metadata);
- Assert.Null(definition.FileIds);
+ Assert.Null(definition.ExecutionOptions);
+ Assert.Null(definition.Temperature);
+ Assert.Null(definition.TopP);
+ Assert.False(definition.EnableFileSearch);
+ Assert.Null(definition.VectorStoreId);
+ Assert.Null(definition.CodeInterpreterFileIds);
Assert.False(definition.EnableCodeInterpreter);
- Assert.False(definition.EnableRetrieval);
+ Assert.False(definition.EnableJsonResponse);
+
+ // Act and Assert
+ ValidateSerialization(definition);
}
///
@@ -35,28 +46,80 @@ public void VerifyOpenAIAssistantDefinitionInitialState()
[Fact]
public void VerifyOpenAIAssistantDefinitionAssignment()
{
+ // Arrange
OpenAIAssistantDefinition definition =
- new()
+ new("testmodel")
{
Id = "testid",
Name = "testname",
- ModelId = "testmodel",
Instructions = "testinstructions",
Description = "testdescription",
- FileIds = ["id"],
+ EnableFileSearch = true,
+ VectorStoreId = "#vs",
Metadata = new Dictionary() { { "a", "1" } },
+ Temperature = 2,
+ TopP = 0,
+ ExecutionOptions =
+ new()
+ {
+ MaxCompletionTokens = 1000,
+ MaxPromptTokens = 1000,
+ ParallelToolCallsEnabled = false,
+ TruncationMessageCount = 12,
+ },
+ CodeInterpreterFileIds = ["file1"],
EnableCodeInterpreter = true,
- EnableRetrieval = true,
+ EnableJsonResponse = true,
};
+ // Assert
Assert.Equal("testid", definition.Id);
Assert.Equal("testname", definition.Name);
Assert.Equal("testmodel", definition.ModelId);
Assert.Equal("testinstructions", definition.Instructions);
Assert.Equal("testdescription", definition.Description);
+ Assert.True(definition.EnableFileSearch);
+ Assert.Equal("#vs", definition.VectorStoreId);
+ Assert.Equal(2, definition.Temperature);
+ Assert.Equal(0, definition.TopP);
+ Assert.NotNull(definition.ExecutionOptions);
+ Assert.Equal(1000, definition.ExecutionOptions.MaxCompletionTokens);
+ Assert.Equal(1000, definition.ExecutionOptions.MaxPromptTokens);
+ Assert.Equal(12, definition.ExecutionOptions.TruncationMessageCount);
+ Assert.False(definition.ExecutionOptions.ParallelToolCallsEnabled);
Assert.Single(definition.Metadata);
- Assert.Single(definition.FileIds);
+ Assert.Single(definition.CodeInterpreterFileIds);
Assert.True(definition.EnableCodeInterpreter);
- Assert.True(definition.EnableRetrieval);
+ Assert.True(definition.EnableJsonResponse);
+
+ // Act and Assert
+ ValidateSerialization(definition);
+ }
+
+ private static void ValidateSerialization(OpenAIAssistantDefinition source)
+ {
+ string json = JsonSerializer.Serialize(source);
+
+ OpenAIAssistantDefinition? target = JsonSerializer.Deserialize(json);
+
+ Assert.NotNull(target);
+ Assert.Equal(source.Id, target.Id);
+ Assert.Equal(source.Name, target.Name);
+ Assert.Equal(source.ModelId, target.ModelId);
+ Assert.Equal(source.Instructions, target.Instructions);
+ Assert.Equal(source.Description, target.Description);
+ Assert.Equal(source.EnableFileSearch, target.EnableFileSearch);
+ Assert.Equal(source.VectorStoreId, target.VectorStoreId);
+ Assert.Equal(source.Temperature, target.Temperature);
+ Assert.Equal(source.TopP, target.TopP);
+ Assert.Equal(source.EnableFileSearch, target.EnableFileSearch);
+ Assert.Equal(source.VectorStoreId, target.VectorStoreId);
+ Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter);
+ Assert.Equal(source.ExecutionOptions?.MaxCompletionTokens, target.ExecutionOptions?.MaxCompletionTokens);
+ Assert.Equal(source.ExecutionOptions?.MaxPromptTokens, target.ExecutionOptions?.MaxPromptTokens);
+ Assert.Equal(source.ExecutionOptions?.TruncationMessageCount, target.ExecutionOptions?.TruncationMessageCount);
+ Assert.Equal(source.ExecutionOptions?.ParallelToolCallsEnabled, target.ExecutionOptions?.ParallelToolCallsEnabled);
+ AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds);
+ AssertCollection.Equal(source.Metadata, target.Metadata);
}
}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs
new file mode 100644
index 000000000000..99cbe012f183
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs
@@ -0,0 +1,100 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+///
+/// Unit testing of .
+///
+public class OpenAIAssistantInvocationOptionsTests
+{
+ ///
+ /// Verify initial state.
+ ///
+ [Fact]
+ public void OpenAIAssistantInvocationOptionsInitialState()
+ {
+ // Arrange
+ OpenAIAssistantInvocationOptions options = new();
+
+ // Assert
+ Assert.Null(options.ModelName);
+ Assert.Null(options.Metadata);
+ Assert.Null(options.Temperature);
+ Assert.Null(options.TopP);
+ Assert.Null(options.ParallelToolCallsEnabled);
+ Assert.Null(options.MaxCompletionTokens);
+ Assert.Null(options.MaxPromptTokens);
+ Assert.Null(options.TruncationMessageCount);
+ Assert.Null(options.EnableJsonResponse);
+ Assert.False(options.EnableCodeInterpreter);
+ Assert.False(options.EnableFileSearch);
+
+ // Act and Assert
+ ValidateSerialization(options);
+ }
+
+ ///
+ /// Verify initialization.
+ ///
+ [Fact]
+ public void OpenAIAssistantInvocationOptionsAssignment()
+ {
+ // Arrange
+ OpenAIAssistantInvocationOptions options =
+ new()
+ {
+ ModelName = "testmodel",
+ Metadata = new Dictionary() { { "a", "1" } },
+ MaxCompletionTokens = 1000,
+ MaxPromptTokens = 1000,
+ ParallelToolCallsEnabled = false,
+ TruncationMessageCount = 12,
+ Temperature = 2,
+ TopP = 0,
+ EnableCodeInterpreter = true,
+ EnableJsonResponse = true,
+ EnableFileSearch = true,
+ };
+
+ // Assert
+ Assert.Equal("testmodel", options.ModelName);
+ Assert.Equal(2, options.Temperature);
+ Assert.Equal(0, options.TopP);
+ Assert.Equal(1000, options.MaxCompletionTokens);
+ Assert.Equal(1000, options.MaxPromptTokens);
+ Assert.Equal(12, options.TruncationMessageCount);
+ Assert.False(options.ParallelToolCallsEnabled);
+ Assert.Single(options.Metadata);
+ Assert.True(options.EnableCodeInterpreter);
+ Assert.True(options.EnableJsonResponse);
+ Assert.True(options.EnableFileSearch);
+
+ // Act and Assert
+ ValidateSerialization(options);
+ }
+
+ private static void ValidateSerialization(OpenAIAssistantInvocationOptions source)
+ {
+ // Act
+ string json = JsonSerializer.Serialize(source);
+
+ OpenAIAssistantInvocationOptions? target = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(target);
+ Assert.Equal(source.ModelName, target.ModelName);
+ Assert.Equal(source.Temperature, target.Temperature);
+ Assert.Equal(source.TopP, target.TopP);
+ Assert.Equal(source.MaxCompletionTokens, target.MaxCompletionTokens);
+ Assert.Equal(source.MaxPromptTokens, target.MaxPromptTokens);
+ Assert.Equal(source.TruncationMessageCount, target.TruncationMessageCount);
+ Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter);
+ Assert.Equal(source.EnableJsonResponse, target.EnableJsonResponse);
+ Assert.Equal(source.EnableFileSearch, target.EnableFileSearch);
+ AssertCollection.Equal(source.Metadata, target.Metadata);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs
new file mode 100644
index 000000000000..7799eb26c305
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using System.Net.Http;
+using Azure.Core;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Moq;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+///
+/// Unit testing of .
+///
+public class OpenAIClientProviderTests
+{
+ ///
+ /// Verify that provisioning of client for Azure OpenAI.
+ ///
+ [Fact]
+ public void VerifyOpenAIClientFactoryTargetAzureByKey()
+ {
+ // Arrange
+ OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI("key", new Uri("https://localhost"));
+
+ // Assert
+ Assert.NotNull(provider.Client);
+ }
+
+ ///
+ /// Verify that provisioning of client for Azure OpenAI.
+ ///
+ [Fact]
+ public void VerifyOpenAIClientFactoryTargetAzureByCredential()
+ {
+ // Arrange
+ Mock mockCredential = new();
+ OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost"));
+
+ // Assert
+ Assert.NotNull(provider.Client);
+ }
+
+ ///
+ /// Verify that provisioning of client for OpenAI.
+ ///
+ [Theory]
+ [InlineData(null)]
+ [InlineData("http://myproxy:9819")]
+ public void VerifyOpenAIClientFactoryTargetOpenAINoKey(string? endpoint)
+ {
+ // Arrange
+ OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(endpoint != null ? new Uri(endpoint) : null);
+
+ // Assert
+ Assert.NotNull(provider.Client);
+ }
+
+ ///
+ /// Verify that provisioning of client for OpenAI.
+ ///
+ [Theory]
+ [InlineData("key", null)]
+ [InlineData("key", "http://myproxy:9819")]
+ public void VerifyOpenAIClientFactoryTargetOpenAIByKey(string key, string? endpoint)
+ {
+ // Arrange
+ OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null);
+
+ // Assert
+ Assert.NotNull(provider.Client);
+ }
+
+ ///
+ /// Verify that the factory can create a client with http proxy.
+ ///
+ [Fact]
+ public void VerifyOpenAIClientFactoryWithHttpClient()
+ {
+ // Arrange
+ using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") };
+ OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(httpClient: httpClient);
+
+ // Assert
+ Assert.NotNull(provider.Client);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs
new file mode 100644
index 000000000000..1689bec1f828
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs
@@ -0,0 +1,75 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.Generic;
+using System.Text.Json;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+///
+/// Unit testing of .
+///
+public class OpenAIThreadCreationOptionsTests
+{
+ ///
+ /// Verify initial state.
+ ///
+ [Fact]
+ public void OpenAIThreadCreationOptionsInitialState()
+ {
+ // Arrange
+ OpenAIThreadCreationOptions options = new();
+
+ // Assert
+ Assert.Null(options.Messages);
+ Assert.Null(options.Metadata);
+ Assert.Null(options.VectorStoreId);
+ Assert.Null(options.CodeInterpreterFileIds);
+
+ // Act and Assert
+ ValidateSerialization(options);
+ }
+
+ ///
+ /// Verify initialization.
+ ///
+ [Fact]
+ public void OpenAIThreadCreationOptionsAssignment()
+ {
+ // Arrange
+ OpenAIThreadCreationOptions options =
+ new()
+ {
+ Messages = [new ChatMessageContent(AuthorRole.User, "test")],
+ VectorStoreId = "#vs",
+ Metadata = new Dictionary() { { "a", "1" } },
+ CodeInterpreterFileIds = ["file1"],
+ };
+
+ // Assert
+ Assert.Single(options.Messages);
+ Assert.Single(options.Metadata);
+ Assert.Equal("#vs", options.VectorStoreId);
+ Assert.Single(options.CodeInterpreterFileIds);
+
+ // Act and Assert
+ ValidateSerialization(options);
+ }
+
+ private static void ValidateSerialization(OpenAIThreadCreationOptions source)
+ {
+ // Act
+ string json = JsonSerializer.Serialize(source);
+
+ OpenAIThreadCreationOptions? target = JsonSerializer.Deserialize(json);
+
+ // Assert
+ Assert.NotNull(target);
+ Assert.Equal(source.VectorStoreId, target.VectorStoreId);
+ AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds);
+ AssertCollection.Equal(source.Messages, target.Messages, m => m.Items.Count); // ChatMessageContent already validated for deep serialization
+ AssertCollection.Equal(source.Metadata, target.Metadata);
+ }
+}
diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs
new file mode 100644
index 000000000000..e75a962dfc5e
--- /dev/null
+++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs
@@ -0,0 +1,71 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Xunit;
+
+namespace SemanticKernel.Agents.UnitTests.OpenAI;
+
+///
+/// Unit testing of .
+///
+public class RunPollingOptionsTests
+{
+ ///
+ /// Verify initial state.
+ ///
+ [Fact]
+ public void RunPollingOptionsInitialStateTest()
+ {
+ // Arrange
+ RunPollingOptions options = new();
+
+ // Assert
+ Assert.Equal(RunPollingOptions.DefaultPollingInterval, options.RunPollingInterval);
+ Assert.Equal(RunPollingOptions.DefaultPollingBackoff, options.RunPollingBackoff);
+ Assert.Equal(RunPollingOptions.DefaultMessageSynchronizationDelay, options.MessageSynchronizationDelay);
+ Assert.Equal(RunPollingOptions.DefaultPollingBackoffThreshold, options.RunPollingBackoffThreshold);
+ }
+
+ /// s
+ /// Verify initialization.
+ ///
+ [Fact]
+ public void RunPollingOptionsAssignmentTest()
+ {
+ // Arrange
+ RunPollingOptions options =
+ new()
+ {
+ RunPollingInterval = TimeSpan.FromSeconds(3),
+ RunPollingBackoff = TimeSpan.FromSeconds(4),
+ RunPollingBackoffThreshold = 8,
+ MessageSynchronizationDelay = TimeSpan.FromSeconds(5),
+ };
+
+ // Assert
+ Assert.Equal(3, options.RunPollingInterval.TotalSeconds);
+ Assert.Equal(4, options.RunPollingBackoff.TotalSeconds);
+ Assert.Equal(5, options.MessageSynchronizationDelay.TotalSeconds);
+ Assert.Equal(8, options.RunPollingBackoffThreshold);
+ }
+
+ /// s
+ /// Verify initialization.
+ ///
+ [Fact]
+ public void RunPollingOptionsGetIntervalTest()
+ {
+ // Arrange
+ RunPollingOptions options =
+ new()
+ {
+ RunPollingInterval = TimeSpan.FromSeconds(3),
+ RunPollingBackoff = TimeSpan.FromSeconds(4),
+ RunPollingBackoffThreshold = 8,
+ };
+
+ // Assert
+ Assert.Equal(options.RunPollingInterval, options.GetPollingInterval(8));
+ Assert.Equal(options.RunPollingBackoff, options.GetPollingInterval(9));
+ }
+}
diff --git a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs
index a8d446dad360..c099f7d609e4 100644
--- a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs
+++ b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs
@@ -4,7 +4,6 @@
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Experimental.Agents.Exceptions;
using Microsoft.SemanticKernel.Experimental.Agents.Internal;
using Microsoft.SemanticKernel.Http;
@@ -92,7 +91,7 @@ private static void AddHeaders(this HttpRequestMessage request, OpenAIRestContex
{
request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant);
request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent);
- request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIChatCompletionService)));
+ request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(IAgent)));
if (context.HasVersion)
{
diff --git a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs
index 1928f219c903..218ef3e3ddfc 100644
--- a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs
+++ b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs
@@ -163,13 +163,12 @@ private IEnumerable> ExecuteStep(ThreadRunStepModel step,
private async Task ProcessFunctionStepAsync(string callId, ThreadRunStepModel.FunctionDetailsModel functionDetails, CancellationToken cancellationToken)
{
var result = await InvokeFunctionCallAsync().ConfigureAwait(false);
- var toolResult = result as string ?? JsonSerializer.Serialize(result);
return
new ToolResultModel
{
CallId = callId,
- Output = toolResult!,
+ Output = ParseFunctionResult(result),
};
async Task
- [Theory(Skip = "No supported endpoint configured.")]
+ [Theory/*(Skip = "No supported endpoint configured.")*/]
[InlineData("What is the special soup?", "Clam Chowder")]
public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAnswerContains)
{
@@ -58,22 +54,20 @@ public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAn
Assert.NotNull(azureOpenAIConfiguration);
await this.ExecuteAgentAsync(
- new(azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.Endpoint),
+ OpenAIClientProvider.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)),
azureOpenAIConfiguration.ChatDeploymentName!,
input,
expectedAnswerContains);
}
private async Task ExecuteAgentAsync(
- OpenAIAssistantConfiguration config,
+ OpenAIClientProvider config,
string modelName,
string input,
string expected)
{
// Arrange
- this._kernelBuilder.Services.AddSingleton(this._logger);
-
- Kernel kernel = this._kernelBuilder.Build();
+ Kernel kernel = new();
KernelPlugin plugin = KernelPluginFactory.CreateFromType();
kernel.Plugins.Add(plugin);
@@ -82,10 +76,9 @@ private async Task ExecuteAgentAsync(
await OpenAIAssistantAgent.CreateAsync(
kernel,
config,
- new()
+ new(modelName)
{
Instructions = "Answer questions about the menu.",
- ModelId = modelName,
});
AgentGroupChat chat = new();
@@ -102,15 +95,6 @@ await OpenAIAssistantAgent.CreateAsync(
Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase);
}
- private readonly XunitLogger _logger = new(output);
- private readonly RedirectOutput _testOutputHelper = new(output);
-
- public void Dispose()
- {
- this._logger.Dispose();
- this._testOutputHelper.Dispose();
- }
-
public sealed class MenuPlugin
{
[KernelFunction, Description("Provides a list of specials from the menu.")]
diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs
new file mode 100644
index 000000000000..e86c1b77f4c1
--- /dev/null
+++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs
@@ -0,0 +1,129 @@
+// Copyright (c) Microsoft. All rights reserved.
+using System.Collections.ObjectModel;
+using System.Diagnostics;
+using Microsoft.SemanticKernel;
+using Microsoft.SemanticKernel.Agents.OpenAI;
+using Microsoft.SemanticKernel.ChatCompletion;
+using OpenAI.Files;
+
+///
+/// Base class for samples that demonstrate the usage of agents.
+///
+public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output)
+{
+ ///
+ /// Metadata key to indicate the assistant as created for a sample.
+ ///
+ protected const string AssistantSampleMetadataKey = "sksample";
+
+ ///
+ /// Metadata to indicate the assistant as created for a sample.
+ ///
+ ///
+ /// While the samples do attempt delete the assistants it creates, it is possible
+ /// that some assistants may remain. This metadata can be used to identify and sample
+ /// agents for clean-up.
+ ///
+ protected static readonly ReadOnlyDictionary AssistantSampleMetadata =
+ new(new Dictionary
+ {
+ { AssistantSampleMetadataKey, bool.TrueString }
+ });
+
+ ///
+ /// Provide a according to the configuration settings.
+ ///
+ protected OpenAIClientProvider GetClientProvider()
+ =>
+ this.UseOpenAIConfig ?
+ OpenAIClientProvider.ForOpenAI(this.ApiKey) :
+ OpenAIClientProvider.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!));
+
+ ///
+ /// Common method to write formatted agent chat content to the console.
+ ///
+ protected void WriteAgentChatMessage(ChatMessageContent message)
+ {
+ // Include ChatMessageContent.AuthorName in output, if present.
+ string authorExpression = message.Role == AuthorRole.User ? string.Empty : $" - {message.AuthorName ?? "*"}";
+ // Include TextContent (via ChatMessageContent.Content), if present.
+ string contentExpression = string.IsNullOrWhiteSpace(message.Content) ? string.Empty : message.Content;
+ bool isCode = message.Metadata?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false;
+ string codeMarker = isCode ? "\n [CODE]\n" : " ";
+ Console.WriteLine($"\n# {message.Role}{authorExpression}:{codeMarker}{contentExpression}");
+
+ // Provide visibility for inner content (that isn't TextContent).
+ foreach (KernelContent item in message.Items)
+ {
+ if (item is AnnotationContent annotation)
+ {
+ Console.WriteLine($" [{item.GetType().Name}] {annotation.Quote}: File #{annotation.FileId}");
+ }
+ else if (item is FileReferenceContent fileReference)
+ {
+ Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}");
+ }
+ else if (item is ImageContent image)
+ {
+ Console.WriteLine($" [{item.GetType().Name}] {image.Uri?.ToString() ?? image.DataUri ?? $"{image.Data?.Length} bytes"}");
+ }
+ else if (item is FunctionCallContent functionCall)
+ {
+ Console.WriteLine($" [{item.GetType().Name}] {functionCall.Id}");
+ }
+ else if (item is FunctionResultContent functionResult)
+ {
+ Console.WriteLine($" [{item.GetType().Name}] {functionResult.CallId}");
+ }
+ }
+ }
+
+ protected async Task DownloadResponseContentAsync(FileClient client, ChatMessageContent message)
+ {
+ foreach (KernelContent item in message.Items)
+ {
+ if (item is AnnotationContent annotation)
+ {
+ await this.DownloadFileContentAsync(client, annotation.FileId!);
+ }
+ }
+ }
+
+ protected async Task DownloadResponseImageAsync(FileClient client, ChatMessageContent message)
+ {
+ foreach (KernelContent item in message.Items)
+ {
+ if (item is FileReferenceContent fileReference)
+ {
+ await this.DownloadFileContentAsync(client, fileReference.FileId, launchViewer: true);
+ }
+ }
+ }
+
+ private async Task DownloadFileContentAsync(FileClient client, string fileId, bool launchViewer = false)
+ {
+ OpenAIFileInfo fileInfo = client.GetFile(fileId);
+ if (fileInfo.Purpose == OpenAIFilePurpose.AssistantsOutput)
+ {
+ string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(fileInfo.Filename));
+ if (launchViewer)
+ {
+ filePath = Path.ChangeExtension(filePath, ".png");
+ }
+
+ BinaryData content = await client.DownloadFileAsync(fileId);
+ File.WriteAllBytes(filePath, content.ToArray());
+ Console.WriteLine($" File #{fileId} saved to: {filePath}");
+
+ if (launchViewer)
+ {
+ Process.Start(
+ new ProcessStartInfo
+ {
+ FileName = "cmd.exe",
+ Arguments = $"/C start {filePath}"
+ });
+ }
+ }
+ }
+}
diff --git a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props
index 0c47e16d8d93..df5205c40a82 100644
--- a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props
+++ b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props
@@ -1,5 +1,8 @@
-
+
+
\ No newline at end of file
diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs
index f9e6f9f3d71f..fd27b35a4b0f 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs
@@ -44,7 +44,7 @@ public AnnotationContent()
/// Initializes a new instance of the class.
///
/// The model ID used to generate the content.
- /// Inner content,
+ /// Inner content
/// Additional metadata
public AnnotationContent(
string? modelId = null,
diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs
index 16ac0cd7828e..925d74d0c731 100644
--- a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs
+++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs
@@ -28,7 +28,7 @@ public FileReferenceContent()
///
/// The identifier of the referenced file.
/// The model ID used to generate the content.
- /// Inner content,
+ /// Inner content
/// Additional metadata
public FileReferenceContent(
string fileId,