Skip to content

Commit

Permalink
.Net Agents - Fix and streamlining for OpenAIAssistantAgent (#10583)
Browse files Browse the repository at this point in the history
### Motivation and Context
<!-- Thank you for your contribution to the semantic-kernel repo!
Please help reviewers and future users, providing the following
information:
  1. Why is this change required?
  2. What problem does it solve?
  3. What scenario does it contribute to?
  4. If it fixes an open issue, please link to the issue here.
-->

Adjust design approach for `OpenAIAssistantAgent` to match that of
`AzureAIAgent`, specifically around exposing the public constructor and
not requiring the use of static factory methods. This approach enables
all features present in the underlying SDK and addresses several
initialization bugs based on the hidden/private constructor.

> Obsolete methods are maintained for compatibility, but will not be
graduated. Eventually, it is likely they will be removed.

### Description
<!-- Describe your changes, the overall approach, the underlying design.
These notes will help understanding how your code works. Thanks! -->

**Changes:**
- Exposed public constructor for `OpenAIAssistantAgent`
- Now support all assistant features (available in the SDK)
- Deprecated factory methods and functional wrappers
- Exposed extension methods to facilitate common operations
- Adjusted all samples and tests
- No single sample increased its line count...most have less lines of
code

**Fixes:**
- Fixes: #10110
- Fixes: #10160
- Fixes: #10390
- Fixes: #10397
- Fixes: #8222
- Fixes: #6795


### Contribution Checklist
<!-- Before submitting this PR, please make sure: -->

- [X] The code builds clean without any errors or warnings
- [X] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [X] All unit tests pass, and I have added new tests where possible
- [X] I didn't break anyone 😄
  • Loading branch information
crickman authored Feb 19, 2025
1 parent b8835d2 commit e183399
Show file tree
Hide file tree
Showing 65 changed files with 2,097 additions and 1,283 deletions.
56 changes: 7 additions & 49 deletions dotnet/samples/Concepts/Agents/AzureAIAgent_FileManipulation.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics;
using Azure.AI.Projects;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
Expand All @@ -13,19 +12,16 @@ namespace Agents;
/// <summary>
/// Demonstrate using code-interpreter to manipulate and generate csv files with <see cref="AzureAIAgent"/> .
/// </summary>
public class AzureAIAgent_FileManipulation(ITestOutputHelper output) : BaseAgentsTest(output)
public class AzureAIAgent_FileManipulation(ITestOutputHelper output) : BaseAzureAgentTest(output)
{
[Fact]
public async Task AnalyzeCSVFileUsingAzureAIAgentAsync()
{
AzureAIClientProvider clientProvider = this.GetAzureProvider();
AgentsClient client = clientProvider.Client.GetAgentsClient();

await using Stream stream = EmbeddedResource.ReadStream("sales.csv")!;
AgentFile fileInfo = await client.UploadFileAsync(stream, AgentFilePurpose.Agents, "sales.csv");
AgentFile fileInfo = await this.AgentsClient.UploadFileAsync(stream, AgentFilePurpose.Agents, "sales.csv");

// Define the agent
Agent definition = await client.CreateAgentAsync(
Agent definition = await this.AgentsClient.CreateAgentAsync(
TestConfiguration.AzureAI.ChatModelId,
tools: [new CodeInterpreterToolDefinition()],
toolResources:
Expand All @@ -36,7 +32,7 @@ public async Task AnalyzeCSVFileUsingAzureAIAgentAsync()
FileIds = { fileInfo.Id },
}
});
AzureAIAgent agent = new(definition, clientProvider);
AzureAIAgent agent = new(definition, this.AgentsClient);

// Create a chat for agent interaction.
AgentGroupChat chat = new();
Expand All @@ -50,8 +46,8 @@ public async Task AnalyzeCSVFileUsingAzureAIAgentAsync()
}
finally
{
await client.DeleteAgentAsync(agent.Id);
await client.DeleteFileAsync(fileInfo.Id);
await this.AgentsClient.DeleteAgentAsync(agent.Id);
await this.AgentsClient.DeleteFileAsync(fileInfo.Id);
await chat.ResetAsync();
}

Expand All @@ -65,45 +61,7 @@ async Task InvokeAgentAsync(string input)
await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
this.WriteAgentChatMessage(response);
await this.DownloadContentAsync(client, response);
}
}
}

private async Task DownloadContentAsync(AgentsClient client, ChatMessageContent message)
{
foreach (KernelContent item in message.Items)
{
if (item is AnnotationContent annotation)
{
await this.DownloadFileAsync(client, annotation.FileId!);
}
}
}

private async Task DownloadFileAsync(AgentsClient client, string fileId, bool launchViewer = false)
{
AgentFile fileInfo = client.GetFile(fileId);
if (fileInfo.Purpose == AgentFilePurpose.AgentsOutput)
{
string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(fileInfo.Filename));
if (launchViewer)
{
filePath = Path.ChangeExtension(filePath, ".png");
}

BinaryData content = await client.GetFileContentAsync(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}"
});
await this.DownloadContentAsync(response);
}
}
}
Expand Down
26 changes: 10 additions & 16 deletions dotnet/samples/Concepts/Agents/AzureAIAgent_Streaming.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ namespace Agents;
/// <summary>
/// Demonstrate consuming "streaming" message for <see cref="AzureAIAgent"/>.
/// </summary>
public class AzureAIAgent_Streaming(ITestOutputHelper output) : BaseAgentsTest(output)
public class AzureAIAgent_Streaming(ITestOutputHelper output) : BaseAzureAgentTest(output)
{
[Fact]
public async Task UseStreamingAgentAsync()
Expand All @@ -20,17 +20,15 @@ public async Task UseStreamingAgentAsync()
const string AgentInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound.";

// Define the agent
AzureAIClientProvider clientProvider = this.GetAzureProvider();
AgentsClient client = clientProvider.Client.GetAgentsClient();
Agent definition = await client.CreateAgentAsync(
Agent definition = await this.AgentsClient.CreateAgentAsync(
TestConfiguration.AzureAI.ChatModelId,
AgentName,
null,
AgentInstructions);
AzureAIAgent agent = new(definition, clientProvider);
AzureAIAgent agent = new(definition, this.AgentsClient);

// Create a thread for the agent conversation.
AgentThread thread = await client.CreateThreadAsync(metadata: AssistantSampleMetadata);
AgentThread thread = await this.AgentsClient.CreateThreadAsync(metadata: SampleMetadata);

// Respond to user input
await InvokeAgentAsync(agent, thread.Id, "Fortune favors the bold.");
Expand All @@ -48,14 +46,12 @@ public async Task UseStreamingAssistantAgentWithPluginAsync()
const string AgentInstructions = "Answer questions about the menu.";

// Define the agent
AzureAIClientProvider clientProvider = this.GetAzureProvider();
AgentsClient client = clientProvider.Client.GetAgentsClient();
Agent definition = await client.CreateAgentAsync(
Agent definition = await this.AgentsClient.CreateAgentAsync(
TestConfiguration.AzureAI.ChatModelId,
AgentName,
null,
AgentInstructions);
AzureAIAgent agent = new(definition, clientProvider)
AzureAIAgent agent = new(definition, this.AgentsClient)
{
Kernel = new Kernel(),
};
Expand All @@ -65,7 +61,7 @@ public async Task UseStreamingAssistantAgentWithPluginAsync()
agent.Kernel.Plugins.Add(plugin);

// Create a thread for the agent conversation.
AgentThread thread = await client.CreateThreadAsync(metadata: AssistantSampleMetadata);
AgentThread thread = await this.AgentsClient.CreateThreadAsync(metadata: SampleMetadata);

// Respond to user input
await InvokeAgentAsync(agent, thread.Id, "What is the special soup and its price?");
Expand All @@ -82,21 +78,19 @@ public async Task UseStreamingAssistantWithCodeInterpreterAsync()
const string AgentInstructions = "Solve math problems with code.";

// Define the agent
AzureAIClientProvider clientProvider = this.GetAzureProvider();
AgentsClient client = clientProvider.Client.GetAgentsClient();
Agent definition = await client.CreateAgentAsync(
Agent definition = await this.AgentsClient.CreateAgentAsync(
TestConfiguration.AzureAI.ChatModelId,
AgentName,
null,
AgentInstructions,
[new CodeInterpreterToolDefinition()]);
AzureAIAgent agent = new(definition, clientProvider)
AzureAIAgent agent = new(definition, this.AgentsClient)
{
Kernel = new Kernel(),
};

// Create a thread for the agent conversation.
AgentThread thread = await client.CreateThreadAsync(metadata: AssistantSampleMetadata);
AgentThread thread = await this.AgentsClient.CreateThreadAsync(metadata: SampleMetadata);

// Respond to user input
await InvokeAgentAsync(agent, thread.Id, "Is 191 a prime number?");
Expand Down
31 changes: 11 additions & 20 deletions dotnet/samples/Concepts/Agents/DeclarativeAgents.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.ChatCompletion;
Expand All @@ -9,11 +8,13 @@ namespace Agents;

public class DeclarativeAgents(ITestOutputHelper output) : BaseAgentsTest(output)
{
[InlineData("SchedulingAssistant.json", "Read the body of my last five emails, if any contain a meeting request for today, check that it's already on my calendar, if not, call out which email it is.")]
[InlineData(
"SchedulingAssistant.json",
"Read the body of my last five emails, if any contain a meeting request for today, check that it's already on my calendar, if not, call out which email it is.")]
[Theory]
public async Task LoadsAgentFromDeclarativeAgentManifestAsync(string agentFileName, string input)
{
var kernel = CreateKernel();
var kernel = this.CreateKernelWithChatCompletion();
kernel.AutoFunctionInvocationFilters.Add(new ExpectedSchemaFunctionFilter());
var manifestLookupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Resources", "DeclarativeAgents");
var manifestFilePath = Path.Combine(manifestLookupDirectory, agentFileName);
Expand All @@ -30,9 +31,8 @@ public async Task LoadsAgentFromDeclarativeAgentManifestAsync(string agentFileNa
Assert.NotNull(agent.Instructions);
Assert.NotEmpty(agent.Instructions);

ChatMessageContent message = new(AuthorRole.User, input);
ChatHistory chatHistory = [message];
StringBuilder sb = new();
ChatHistory chatHistory = [new ChatMessageContent(AuthorRole.User, input)];

var kernelArguments = new KernelArguments(new PromptExecutionSettings
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
Expand All @@ -42,23 +42,14 @@ public async Task LoadsAgentFromDeclarativeAgentManifestAsync(string agentFileNa
}
)
});
await foreach (ChatMessageContent response in agent.InvokeAsync(chatHistory, kernelArguments))
{
chatHistory.Add(response);
sb.Append(response.Content);
}
Assert.NotEmpty(chatHistory.Skip(1));
}
private Kernel CreateKernel()
{
IKernelBuilder builder = Kernel.CreateBuilder();

base.AddChatCompletionToKernel(builder);

return builder.Build();
var responses = await agent.InvokeAsync(chatHistory, kernelArguments).ToArrayAsync();
Assert.NotEmpty(responses);
}

private sealed class ExpectedSchemaFunctionFilter : IAutoFunctionInvocationFilter
{//TODO: this eventually needs to be added to all CAP or DA but we're still discussing where should those facilitators live
{
//TODO: this eventually needs to be added to all CAP or DA but we're still discussing where should those facilitators live
public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func<AutoFunctionInvocationContext, Task> next)
{
await next(context);
Expand Down
23 changes: 12 additions & 11 deletions dotnet/samples/Concepts/Agents/MixedChat_Agents.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@
using Microsoft.SemanticKernel.Agents.Chat;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
using OpenAI.Assistants;

namespace Agents;
/// <summary>
/// Demonstrate that two different agent types are able to participate in the same conversation.
/// In this case a <see cref="ChatCompletionAgent"/> and <see cref="OpenAIAssistantAgent"/> participate.
/// </summary>
public class MixedChat_Agents(ITestOutputHelper output) : BaseAgentsTest(output)
public class MixedChat_Agents(ITestOutputHelper output) : BaseAssistantTest(output)
{
private const string ReviewerName = "ArtDirector";
private const string ReviewerInstructions =
Expand Down Expand Up @@ -44,16 +45,16 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync()
Kernel = this.CreateKernelWithChatCompletion(),
};

OpenAIAssistantAgent agentWriter =
await OpenAIAssistantAgent.CreateAsync(
clientProvider: this.GetClientProvider(),
definition: new OpenAIAssistantDefinition(this.Model)
{
Instructions = CopyWriterInstructions,
Name = CopyWriterName,
Metadata = AssistantSampleMetadata,
},
kernel: new Kernel());
// Define the assistant
Assistant assistant =
await this.AssistantClient.CreateAssistantAsync(
this.Model,
name: CopyWriterName,
instructions: CopyWriterInstructions,
metadata: SampleMetadata);

// Create the agent
OpenAIAssistantAgent agentWriter = new(assistant, this.AssistantClient);

// Create a chat for agent interaction.
AgentGroupChat chat =
Expand Down
43 changes: 17 additions & 26 deletions dotnet/samples/Concepts/Agents/MixedChat_Files.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using Microsoft.SemanticKernel.Agents;
using Microsoft.SemanticKernel.Agents.OpenAI;
using Microsoft.SemanticKernel.ChatCompletion;
using OpenAI.Files;
using OpenAI.Assistants;
using Resources;

namespace Agents;
Expand All @@ -12,36 +12,27 @@ namespace Agents;
/// Demonstrate <see cref="ChatCompletionAgent"/> agent interacts with
/// <see cref="OpenAIAssistantAgent"/> when it produces file output.
/// </summary>
public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output)
public class MixedChat_Files(ITestOutputHelper output) : BaseAssistantTest(output)
{
private const string SummaryInstructions = "Summarize the entire conversation for the user in natural language.";

[Fact]
public async Task AnalyzeFileAndGenerateReportAsync()
{
OpenAIClientProvider provider = this.GetClientProvider();

OpenAIFileClient fileClient = provider.Client.GetOpenAIFileClient();

OpenAIFile uploadFile =
await fileClient.UploadFileAsync(
new BinaryData(await EmbeddedResource.ReadAllAsync("30-user-context.txt")),
"30-user-context.txt",
FileUploadPurpose.Assistants);

Console.WriteLine(this.ApiKey);
await using Stream stream = EmbeddedResource.ReadStream("30-user-context.txt")!;
string fileId = await this.Client.UploadAssistantFileAsync(stream, "30-user-context.txt");

// Define the agents
OpenAIAssistantAgent analystAgent =
await OpenAIAssistantAgent.CreateAsync(
provider,
definition: new OpenAIAssistantDefinition(this.Model)
{
EnableCodeInterpreter = true,
CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant code-interpreter
Metadata = AssistantSampleMetadata,
},
kernel: new Kernel());
// Define the assistant
Assistant assistant =
await this.AssistantClient.CreateAssistantAsync(
this.Model,
enableCodeInterpreter: true,
codeInterpreterFileIds: [fileId],
metadata: SampleMetadata);

// Create the agent
OpenAIAssistantAgent analystAgent = new(assistant, this.AssistantClient);

ChatCompletionAgent summaryAgent =
new()
Expand All @@ -66,8 +57,8 @@ Create a tab delimited file report of the ordered (descending) frequency distrib
}
finally
{
await analystAgent.DeleteAsync();
await fileClient.DeleteFileAsync(uploadFile.Id);
await this.AssistantClient.DeleteAssistantAsync(analystAgent.Id);
await this.Client.DeleteFileAsync(fileId);
}

// Local function to invoke agent and display the conversation messages.
Expand All @@ -83,7 +74,7 @@ async Task InvokeAgentAsync(Agent agent, string? input = null)
await foreach (ChatMessageContent response in chat.InvokeAsync(agent))
{
this.WriteAgentChatMessage(response);
await this.DownloadResponseContentAsync(fileClient, response);
await this.DownloadResponseContentAsync(response);
}
}
}
Expand Down
Loading

0 comments on commit e183399

Please sign in to comment.