Skip to content

Commit 1b735c7

Browse files
.Net: Avoid sending duplicate function tools when creating a thread (#10436)
### Motivation and Context Same issue that was fixed here #10413 but this time the fix is for OpenAI Assistants. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> ### Contribution Checklist <!-- Before submitting this PR, please make sure: --> - [ ] The code builds clean without any errors or warnings - [ ] 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 - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone 😄
1 parent a9e0c09 commit 1b735c7

File tree

6 files changed

+158
-9
lines changed

6 files changed

+158
-9
lines changed

dotnet/samples/GettingStartedWithAgents/AzureAIAgent/Step06_AzureAIAgent_Functions.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
using System.ComponentModel;
33
using Azure.AI.Projects;
44
using Microsoft.SemanticKernel;
5-
using Microsoft.SemanticKernel.Agents;
65
using Microsoft.SemanticKernel.Agents.AzureAI;
76
using Microsoft.SemanticKernel.ChatCompletion;
87
using Agent = Azure.AI.Projects.Agent;
98

109
namespace GettingStarted.AzureAgents;
1110

1211
/// <summary>
13-
/// This example demonstrates similarity between using <see cref="AzureAIAgent"/>
14-
/// and <see cref="ChatCompletionAgent"/> (see: Step 2).
12+
/// This example demonstrates how to define function tools for an <see cref="AzureAIAgent"/>
13+
/// when the agent is created. This is useful if you want to retrieve the agent later and
14+
/// then dynamically check what function tools it requires.
1515
/// </summary>
1616
public class Step06_AzureAIAgent_Functions(ITestOutputHelper output) : BaseAgentsTest(output)
1717
{
@@ -37,12 +37,12 @@ public async Task UseSingleAgentWithFunctionToolsAsync()
3737
description: null,
3838
instructions: HostInstructions,
3939
tools: tools);
40-
Microsoft.SemanticKernel.Agents.AzureAI.AzureAIAgent agent = new(definition, clientProvider)
40+
AzureAIAgent agent = new(definition, clientProvider)
4141
{
4242
Kernel = new Kernel(),
4343
};
4444

45-
// Initialize plugin and add to the agent's Kernel (same as direct Kernel usage).
45+
// Add plugin to the agent's Kernel (same as direct Kernel usage).
4646
agent.Kernel.Plugins.Add(plugin);
4747

4848
// Create a thread for the agent conversation.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using System.ComponentModel;
4+
using Microsoft.SemanticKernel;
5+
using Microsoft.SemanticKernel.Agents.OpenAI;
6+
using Microsoft.SemanticKernel.ChatCompletion;
7+
using OpenAI.Assistants;
8+
9+
namespace GettingStarted.OpenAIAssistants;
10+
11+
/// <summary>
12+
/// This example demonstrates how to define function tools for an <see cref="OpenAIAssistantAgent"/>
13+
/// when the assistant is created. This is useful if you want to retrieve the assistant later and
14+
/// then dynamically check what function tools it requires.
15+
/// </summary>
16+
public class Step05_AssistantTool_Function(ITestOutputHelper output) : BaseAgentsTest(output)
17+
{
18+
private const string HostName = "Host";
19+
private const string HostInstructions = "Answer questions about the menu.";
20+
21+
[Fact]
22+
public async Task UseSingleAssistantWithFunctionToolsAsync()
23+
{
24+
// Define the agent
25+
OpenAIClientProvider provider = this.GetClientProvider();
26+
var client = provider.Client.GetAssistantClient();
27+
AssistantCreationOptions creationOptions =
28+
new()
29+
{
30+
Name = HostName,
31+
Instructions = HostInstructions,
32+
Metadata = AssistantSampleMetadata,
33+
};
34+
35+
// In this sample the function tools are added to the assistant this is
36+
// important if you want to retrieve the assistant later and then dynamically check
37+
// what function tools it requires.
38+
KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
39+
plugin.Select(f => f.ToToolDefinition(plugin.Name)).ToList().ForEach(td => creationOptions.Tools.Add(td));
40+
41+
OpenAIAssistantAgent agent =
42+
await OpenAIAssistantAgent.CreateAsync(
43+
clientProvider: this.GetClientProvider(),
44+
modelId: this.Model,
45+
creationOptions: creationOptions,
46+
kernel: new Kernel());
47+
48+
// Add plugin to the agent's Kernel (same as direct Kernel usage).
49+
agent.Kernel.Plugins.Add(plugin);
50+
51+
// Create a thread for the agent conversation.
52+
string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata });
53+
54+
// Respond to user input
55+
try
56+
{
57+
await InvokeAgentAsync("Hello");
58+
await InvokeAgentAsync("What is the special soup and its price?");
59+
await InvokeAgentAsync("What is the special drink and its price?");
60+
await InvokeAgentAsync("Thank you");
61+
}
62+
finally
63+
{
64+
await agent.DeleteThreadAsync(threadId);
65+
await agent.DeleteAsync();
66+
}
67+
68+
// Local function to invoke agent and display the conversation messages.
69+
async Task InvokeAgentAsync(string input)
70+
{
71+
ChatMessageContent message = new(AuthorRole.User, input);
72+
await agent.AddChatMessageAsync(threadId, message);
73+
this.WriteAgentChatMessage(message);
74+
75+
await foreach (ChatMessageContent response in agent.InvokeAsync(threadId))
76+
{
77+
this.WriteAgentChatMessage(response);
78+
}
79+
}
80+
}
81+
private sealed class MenuPlugin
82+
{
83+
[KernelFunction, Description("Provides a list of specials from the menu.")]
84+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")]
85+
public string GetSpecials() =>
86+
"""
87+
Special Soup: Clam Chowder
88+
Special Salad: Cobb Salad
89+
Special Drink: Chai Tea
90+
""";
91+
92+
[KernelFunction, Description("Provides the price of the requested menu item.")]
93+
public string GetItemPrice(
94+
[Description("The name of the menu item.")]
95+
string menuItem) =>
96+
"$9.99";
97+
}
98+
}

dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44

55
namespace Microsoft.SemanticKernel.Agents.OpenAI;
66

7-
internal static class KernelFunctionExtensions
7+
/// <summary>
8+
/// Extensions for <see cref="KernelFunction"/> to support OpenAI specific operations.
9+
/// </summary>
10+
public static class KernelFunctionExtensions
811
{
912
/// <summary>
1013
/// Convert <see cref="KernelFunction"/> to an OpenAI tool model.

dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,14 @@ public static async IAsyncEnumerable<ChatMessageContent> GetMessagesAsync(Assist
165165

166166
logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId);
167167

168-
ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))];
168+
List<ToolDefinition> tools = new(agent.Tools);
169+
170+
// Add unique functions from the Kernel which are not already present in the agent's tools
171+
var functionToolNames = new HashSet<string>(tools.OfType<FunctionToolDefinition>().Select(t => t.FunctionName));
172+
var functionTools = kernel.Plugins
173+
.SelectMany(kp => kp.Select(kf => kf.ToToolDefinition(kp.Name)))
174+
.Where(tool => !functionToolNames.Contains(tool.FunctionName));
175+
tools.AddRange(functionTools);
169176

170177
string? instructions = await agent.GetInstructionsAsync(kernel, arguments, cancellationToken).ConfigureAwait(false);
171178

dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent
5959
internal IReadOnlyList<ToolDefinition> Tools => this._assistant.Tools;
6060

6161
/// <summary>
62-
/// Defines a new <see cref="OpenAIAssistantAgent"/>.
62+
/// Create a new <see cref="OpenAIAssistantAgent"/>.
6363
/// </summary>
6464
/// <param name="clientProvider">The OpenAI client provider for accessing the API service.</param>
6565
/// <param name="capabilities">The assistant's capabilities.</param>
@@ -106,7 +106,7 @@ public async static Task<OpenAIAssistantAgent> CreateFromTemplateAsync(
106106
}
107107

108108
/// <summary>
109-
/// Defines a new <see cref="OpenAIAssistantAgent"/>.
109+
/// Create a new <see cref="OpenAIAssistantAgent"/>.
110110
/// </summary>
111111
/// <param name="clientProvider">The OpenAI client provider for accessing the API service.</param>
112112
/// <param name="definition">The assistant definition.</param>
@@ -142,6 +142,44 @@ public static async Task<OpenAIAssistantAgent> CreateAsync(
142142
};
143143
}
144144

145+
/// <summary>
146+
/// Create a new <see cref="OpenAIAssistantAgent"/>.
147+
/// </summary>
148+
/// <param name="clientProvider">OpenAI client provider for accessing the API service.</param>
149+
/// <param name="modelId">OpenAI model id.</param>
150+
/// <param name="creationOptions">The assistant creation options.</param>
151+
/// <param name="kernel">The <see cref="Kernel"/> containing services, plugins, and other state for use throughout the operation.</param>
152+
/// <param name="defaultArguments">Optional default arguments, including any <see cref="PromptExecutionSettings"/>.</param>
153+
/// <param name="cancellationToken">The <see cref="CancellationToken"/> to monitor for cancellation requests. The default is <see cref="CancellationToken.None"/>.</param>
154+
/// <returns>An <see cref="OpenAIAssistantAgent"/> instance</returns>
155+
public static async Task<OpenAIAssistantAgent> CreateAsync(
156+
OpenAIClientProvider clientProvider,
157+
string modelId,
158+
AssistantCreationOptions creationOptions,
159+
Kernel kernel,
160+
KernelArguments? defaultArguments = null,
161+
CancellationToken cancellationToken = default)
162+
{
163+
// Validate input
164+
Verify.NotNull(kernel, nameof(kernel));
165+
Verify.NotNull(clientProvider, nameof(clientProvider));
166+
Verify.NotNull(creationOptions, nameof(creationOptions));
167+
168+
// Create the client
169+
AssistantClient client = CreateClient(clientProvider);
170+
171+
// Create the assistant
172+
Assistant model = await client.CreateAssistantAsync(modelId, creationOptions, cancellationToken).ConfigureAwait(false);
173+
174+
// Instantiate the agent
175+
return
176+
new OpenAIAssistantAgent(model, clientProvider, client)
177+
{
178+
Kernel = kernel,
179+
Arguments = defaultArguments
180+
};
181+
}
182+
145183
/// <summary>
146184
/// Retrieves a list of assistant <see cref="OpenAIAssistantDefinition">definitions</see>.
147185
/// </summary>

dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI;
2020
/// <summary>
2121
/// Unit testing of <see cref="OpenAIAssistantAgent"/>.
2222
/// </summary>
23+
#pragma warning disable CS0419 // Ambiguous reference in cref attribute
2324
public sealed class OpenAIAssistantAgentTests : IDisposable
2425
{
2526
private readonly HttpMessageHandlerStub _messageHandlerStub;
@@ -906,3 +907,5 @@ public void MyFunction(int index)
906907
{ }
907908
}
908909
}
910+
#pragma warning restore CS0419 // Ambiguous reference in cref attribute
911+

0 commit comments

Comments
 (0)