Skip to content

Commit a696dab

Browse files
.Net: Fix issue 12775 (#12776)
### Motivation and Context - Closes #12775 ### 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 44418d1 commit a696dab

File tree

3 files changed

+353
-12
lines changed

3 files changed

+353
-12
lines changed

dotnet/samples/GettingStartedWithAgents/OpenAIResponse/Step04_OpenAIResponseAgent_Tools.cs

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public async Task InvokeAgentWithFunctionToolsAsync()
2828

2929
// Create a plugin that defines the tools to be used by the agent.
3030
KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
31-
var tools = plugin.Select(f => f.ToToolDefinition(plugin.Name));
3231
agent.Kernel.Plugins.Add(plugin);
3332

3433
ICollection<ChatMessageContent> messages =
@@ -127,4 +126,43 @@ public async Task InvokeAgentWithFileSearchAsync()
127126
this.FileClient.DeleteFile(file.Id, noThrowOptions);
128127
this.VectorStoreClient.DeleteVectorStore(createStoreOp.VectorStoreId, noThrowOptions);
129128
}
129+
130+
[Fact]
131+
public async Task InvokeAgentWithMultipleToolsAsync()
132+
{
133+
// Define the agent
134+
OpenAIResponseAgent agent = new(this.Client)
135+
{
136+
StoreEnabled = false,
137+
};
138+
139+
// Create a plugin that defines the tools to be used by the agent.
140+
KernelPlugin plugin = KernelPluginFactory.CreateFromType<MenuPlugin>();
141+
agent.Kernel.Plugins.Add(plugin);
142+
143+
ICollection<ChatMessageContent> messages =
144+
[
145+
new ChatMessageContent(AuthorRole.User, "What is the special soup and its price?"),
146+
new ChatMessageContent(AuthorRole.User, "What is the special drink and its price?"),
147+
];
148+
foreach (ChatMessageContent message in messages)
149+
{
150+
WriteAgentChatMessage(message);
151+
}
152+
153+
// ResponseCreationOptions allows you to specify tools for the agent.
154+
ResponseCreationOptions creationOptions = new();
155+
creationOptions.Tools.Add(ResponseTool.CreateWebSearchTool());
156+
OpenAIResponseAgentInvokeOptions invokeOptions = new()
157+
{
158+
ResponseCreationOptions = creationOptions,
159+
};
160+
161+
// Invoke the agent and output the response
162+
var responseItems = agent.InvokeAsync(messages, options: invokeOptions);
163+
await foreach (ChatMessageContent responseItem in responseItems)
164+
{
165+
WriteAgentChatMessage(responseItem);
166+
}
167+
}
130168
}

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

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,47 @@ internal static ResponseCreationOptions CreateOptions(
1616
AgentThread agentThread,
1717
AgentInvokeOptions? invokeOptions)
1818
{
19+
var instructions = $"{agent.Instructions}{(string.IsNullOrEmpty(agent.Instructions) || string.IsNullOrEmpty(invokeOptions?.AdditionalInstructions) ? "" : "\n")}{invokeOptions?.AdditionalInstructions}";
20+
ResponseCreationOptions creationOptions;
1921
if (invokeOptions is OpenAIResponseAgentInvokeOptions responseAgentInvokeOptions &&
2022
responseAgentInvokeOptions.ResponseCreationOptions is not null)
2123
{
22-
// Use the options provided by the caller
23-
return responseAgentInvokeOptions.ResponseCreationOptions;
24+
creationOptions = new ResponseCreationOptions
25+
{
26+
EndUserId = responseAgentInvokeOptions.ResponseCreationOptions.EndUserId ?? agent.GetDisplayName(),
27+
Instructions = responseAgentInvokeOptions.ResponseCreationOptions.Instructions ?? instructions,
28+
StoredOutputEnabled = responseAgentInvokeOptions.ResponseCreationOptions.StoredOutputEnabled ?? agent.StoreEnabled,
29+
Background = responseAgentInvokeOptions.ResponseCreationOptions.Background,
30+
ReasoningOptions = responseAgentInvokeOptions.ResponseCreationOptions.ReasoningOptions,
31+
MaxOutputTokenCount = responseAgentInvokeOptions.ResponseCreationOptions.MaxOutputTokenCount,
32+
TextOptions = responseAgentInvokeOptions.ResponseCreationOptions.TextOptions,
33+
TruncationMode = responseAgentInvokeOptions.ResponseCreationOptions.TruncationMode,
34+
ParallelToolCallsEnabled = responseAgentInvokeOptions.ResponseCreationOptions.ParallelToolCallsEnabled,
35+
ToolChoice = responseAgentInvokeOptions.ResponseCreationOptions.ToolChoice,
36+
Temperature = responseAgentInvokeOptions.ResponseCreationOptions.Temperature,
37+
TopP = responseAgentInvokeOptions.ResponseCreationOptions.TopP,
38+
PreviousResponseId = responseAgentInvokeOptions.ResponseCreationOptions.PreviousResponseId,
39+
};
40+
creationOptions.Tools.AddRange(responseAgentInvokeOptions.ResponseCreationOptions.Tools);
41+
responseAgentInvokeOptions.ResponseCreationOptions.Metadata.ToList().ForEach(kvp => creationOptions.Metadata[kvp.Key] = kvp.Value);
2442
}
25-
26-
var responseTools = agent.GetKernel(invokeOptions).Plugins
27-
.SelectMany(kp => kp.Select(kf => kf.ToResponseTool(kp.Name)));
28-
29-
var creationOptions = new ResponseCreationOptions
43+
else
3044
{
31-
EndUserId = agent.GetDisplayName(),
32-
Instructions = $"{agent.Instructions}\n{invokeOptions?.AdditionalInstructions}",
33-
StoredOutputEnabled = agent.StoreEnabled,
34-
};
45+
creationOptions = new ResponseCreationOptions
46+
{
47+
EndUserId = agent.GetDisplayName(),
48+
Instructions = instructions,
49+
StoredOutputEnabled = agent.StoreEnabled,
50+
};
51+
}
3552

3653
if (agent.StoreEnabled && agentThread.Id is not null)
3754
{
3855
creationOptions.PreviousResponseId = agentThread.Id;
3956
}
4057

58+
var responseTools = agent.GetKernel(invokeOptions).Plugins
59+
.SelectMany(kp => kp.Select(kf => kf.ToResponseTool(kp.Name)));
4160
if (responseTools is not null && responseTools.Any())
4261
{
4362
creationOptions.Tools.AddRange(responseTools);
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
using Microsoft.SemanticKernel;
3+
using Microsoft.SemanticKernel.Agents;
4+
using Microsoft.SemanticKernel.Agents.OpenAI;
5+
using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
6+
using Moq;
7+
using OpenAI.Responses;
8+
using Xunit;
9+
10+
namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal;
11+
12+
/// <summary>
13+
/// Unit testing of <see cref="ResponseCreationOptionsFactory"/>.
14+
/// </summary>
15+
public class ResponseCreationOptionsFactoryTests
16+
{
17+
/// <summary>
18+
/// Verify response options creation with null invoke options.
19+
/// </summary>
20+
[Fact]
21+
public void CreateOptionsWithNullInvokeOptionsTest()
22+
{
23+
// Arrange
24+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: false);
25+
var mockThread = CreateMockAgentThread(null);
26+
27+
// Act
28+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
29+
30+
// Assert
31+
Assert.NotNull(options);
32+
Assert.Equal("Test Agent", options.EndUserId);
33+
Assert.Equal("You are a helpful assistant.", options.Instructions);
34+
Assert.False(options.StoredOutputEnabled);
35+
Assert.Null(options.ReasoningOptions);
36+
Assert.Null(options.MaxOutputTokenCount);
37+
Assert.Null(options.TextOptions);
38+
Assert.Null(options.TruncationMode);
39+
Assert.Null(options.ParallelToolCallsEnabled);
40+
Assert.Null(options.ToolChoice);
41+
Assert.Empty(options.Tools);
42+
Assert.Null(options.PreviousResponseId);
43+
}
44+
45+
/// <summary>
46+
/// Verify response options creation with store enabled and thread ID.
47+
/// </summary>
48+
[Fact]
49+
public void CreateOptionsWithStoreEnabledAndThreadIdTest()
50+
{
51+
// Arrange
52+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: true);
53+
var mockThread = CreateMockAgentThread("thread-123");
54+
55+
// Act
56+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
57+
58+
// Assert
59+
Assert.NotNull(options);
60+
Assert.Equal("Test Agent", options.EndUserId);
61+
Assert.Equal("You are a helpful assistant.", options.Instructions);
62+
Assert.True(options.StoredOutputEnabled);
63+
Assert.Equal("thread-123", options.PreviousResponseId);
64+
}
65+
66+
/// <summary>
67+
/// Verify response options creation with additional instructions from invoke options.
68+
/// </summary>
69+
[Fact]
70+
public void CreateOptionsWithAdditionalInstructionsTest()
71+
{
72+
// Arrange
73+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: false);
74+
var mockThread = CreateMockAgentThread(null);
75+
var invokeOptions = new AgentInvokeOptions
76+
{
77+
AdditionalInstructions = "Be more concise."
78+
};
79+
80+
// Act
81+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, invokeOptions);
82+
83+
// Assert
84+
Assert.NotNull(options);
85+
Assert.Equal("Test Agent", options.EndUserId);
86+
Assert.Equal("You are a helpful assistant.\nBe more concise.", options.Instructions);
87+
Assert.False(options.StoredOutputEnabled);
88+
}
89+
90+
/// <summary>
91+
/// Verify response options creation with OpenAIResponseAgentInvokeOptions with full ResponseCreationOptions.
92+
/// </summary>
93+
[Fact]
94+
public void CreateOptionsWithResponseAgentInvokeOptionsTest()
95+
{
96+
// Arrange
97+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: false);
98+
var mockThread = CreateMockAgentThread(null);
99+
var responseCreationOptions = new ResponseCreationOptions
100+
{
101+
EndUserId = "custom-user",
102+
Instructions = "Custom instructions",
103+
StoredOutputEnabled = true,
104+
MaxOutputTokenCount = 1000,
105+
ParallelToolCallsEnabled = true,
106+
ToolChoice = ResponseToolChoice.CreateAutoChoice(),
107+
Temperature = 0.7f,
108+
TopP = 0.9f,
109+
PreviousResponseId = "previous-response-id",
110+
};
111+
responseCreationOptions.Tools.Add(ResponseTool.CreateWebSearchTool());
112+
113+
var invokeOptions = new OpenAIResponseAgentInvokeOptions
114+
{
115+
AdditionalInstructions = "Additional instructions",
116+
ResponseCreationOptions = responseCreationOptions
117+
};
118+
119+
// Act
120+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, invokeOptions);
121+
122+
// Assert
123+
Assert.NotNull(options);
124+
Assert.Equal("custom-user", options.EndUserId);
125+
Assert.Equal("Custom instructions", options.Instructions);
126+
Assert.True(options.StoredOutputEnabled);
127+
Assert.Equal(1000, options.MaxOutputTokenCount);
128+
Assert.True(options.ParallelToolCallsEnabled);
129+
Assert.NotNull(options.ToolChoice);
130+
Assert.Single(options.Tools);
131+
Assert.Equal(0.7f, options.Temperature);
132+
Assert.Equal(0.9f, options.TopP);
133+
Assert.Equal("previous-response-id", options.PreviousResponseId);
134+
}
135+
136+
/// <summary>
137+
/// Verify response options creation with ResponseCreationOptions having null values that fallback to agent defaults.
138+
/// </summary>
139+
[Fact]
140+
public void CreateOptionsWithResponseCreationOptionsNullFallbackTest()
141+
{
142+
// Arrange
143+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: true);
144+
var mockThread = CreateMockAgentThread(null);
145+
var responseCreationOptions = new ResponseCreationOptions
146+
{
147+
EndUserId = null, // Should fallback to agent display name
148+
Instructions = null, // Should fallback to agent instructions + additional
149+
StoredOutputEnabled = null // Should fallback to agent store enabled
150+
};
151+
152+
var invokeOptions = new OpenAIResponseAgentInvokeOptions
153+
{
154+
AdditionalInstructions = "Be helpful",
155+
ResponseCreationOptions = responseCreationOptions
156+
};
157+
158+
// Act
159+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, invokeOptions);
160+
161+
// Assert
162+
Assert.NotNull(options);
163+
Assert.Equal("Test Agent", options.EndUserId);
164+
Assert.Equal("You are a helpful assistant.\nBe helpful", options.Instructions);
165+
Assert.True(options.StoredOutputEnabled);
166+
}
167+
168+
/// <summary>
169+
/// Verify response options creation when agent has null instructions.
170+
/// </summary>
171+
[Fact]
172+
public void CreateOptionsWithNullAgentInstructionsTest()
173+
{
174+
// Arrange
175+
var mockAgent = CreateMockAgent("Test Agent", null, storeEnabled: false);
176+
var mockThread = CreateMockAgentThread(null);
177+
178+
var invokeOptions = new AgentInvokeOptions
179+
{
180+
AdditionalInstructions = "Be helpful"
181+
};
182+
183+
// Act
184+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, invokeOptions);
185+
186+
// Assert
187+
Assert.NotNull(options);
188+
Assert.Equal("Be helpful", options.Instructions);
189+
}
190+
191+
/// <summary>
192+
/// Verify response options creation when both agent instructions and additional instructions are null.
193+
/// </summary>
194+
[Fact]
195+
public void CreateOptionsWithAllNullInstructionsTest()
196+
{
197+
// Arrange
198+
var mockAgent = CreateMockAgent("Test Agent", null, storeEnabled: false);
199+
var mockThread = CreateMockAgentThread(null);
200+
201+
// Act
202+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
203+
204+
// Assert
205+
Assert.NotNull(options);
206+
Assert.Equal("", options.Instructions);
207+
}
208+
209+
/// <summary>
210+
/// Verify response options creation when agent store is disabled but thread ID exists.
211+
/// </summary>
212+
[Fact]
213+
public void CreateOptionsWithStoreDisabledButThreadIdExistsTest()
214+
{
215+
// Arrange
216+
var mockAgent = CreateMockAgent("Test Agent", "You are a helpful assistant.", storeEnabled: false);
217+
var mockThread = CreateMockAgentThread("thread-123");
218+
219+
// Act
220+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
221+
222+
// Assert
223+
Assert.NotNull(options);
224+
Assert.False(options.StoredOutputEnabled);
225+
Assert.Null(options.PreviousResponseId); // Should not set previous response ID when store is disabled
226+
}
227+
228+
/// <summary>
229+
/// Verify response options creation with empty agent name fallback to "UnnamedAgent".
230+
/// </summary>
231+
[Fact]
232+
public void CreateOptionsWithEmptyAgentNameTest()
233+
{
234+
// Arrange
235+
var mockAgent = CreateMockAgent("", "You are a helpful assistant.", storeEnabled: false);
236+
var mockThread = CreateMockAgentThread(null);
237+
238+
// Act
239+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
240+
241+
// Assert
242+
Assert.NotNull(options);
243+
Assert.Equal("UnnamedAgent", options.EndUserId); // Empty name should fallback to "UnnamedAgent"
244+
}
245+
246+
/// <summary>
247+
/// Verify response options creation with null agent name fallback to "UnnamedAgent".
248+
/// </summary>
249+
[Fact]
250+
public void CreateOptionsWithNullAgentNameTest()
251+
{
252+
// Arrange
253+
var mockAgent = CreateMockAgent(null, "You are a helpful assistant.", storeEnabled: false);
254+
var mockThread = CreateMockAgentThread(null);
255+
256+
// Act
257+
var options = ResponseCreationOptionsFactory.CreateOptions(mockAgent, mockThread.Object, null);
258+
259+
// Assert
260+
Assert.NotNull(options);
261+
Assert.Equal("UnnamedAgent", options.EndUserId); // Null name should fallback to "UnnamedAgent"
262+
}
263+
264+
private static OpenAIResponseAgent CreateMockAgent(string? name, string? instructions, bool storeEnabled)
265+
{
266+
var mockClient = new Mock<OpenAIResponseClient>();
267+
var mockAgent = new OpenAIResponseAgent(mockClient.Object)
268+
{
269+
Name = name ?? "UnnamedAgent",
270+
Instructions = instructions ?? string.Empty,
271+
StoreEnabled = storeEnabled,
272+
Kernel = new Kernel() // Empty kernel for testing
273+
};
274+
275+
return mockAgent;
276+
}
277+
278+
private static Mock<AgentThread> CreateMockAgentThread(string? threadId)
279+
{
280+
var mockThread = new Mock<AgentThread>();
281+
mockThread.Setup(t => t.Id).Returns(threadId);
282+
return mockThread;
283+
}
284+
}

0 commit comments

Comments
 (0)