Skip to content

Commit aabfc9c

Browse files
markwallace-microsoftcrickmanSergeyMenshykh
authored
.Net: Feature OpenAI Response Agent (#11498)
### Motivation and Context Semantic Kernel `Agent` implementation backed by the Open AI Response API. ### Description <!-- Describe your changes, the overall approach, the underlying design. These notes will help understanding how your code works. Thanks! --> Add agent, samples, and tests for `OpenAIResponseAgent` ### 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 😄 --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Chris Rickman <crickman@microsoft.com> Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com>
1 parent b8c4486 commit aabfc9c

25 files changed

+2934
-2
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
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.ChatCompletion;
6+
7+
namespace GettingStarted.OpenAIResponseAgents;
8+
9+
/// <summary>
10+
/// This example demonstrates using <see cref="OpenAIResponseAgent"/>.
11+
/// </summary>
12+
public class Step01_OpenAIResponseAgent(ITestOutputHelper output) : BaseResponsesAgentTest(output)
13+
{
14+
[Fact]
15+
public async Task UseOpenAIResponseAgentAsync()
16+
{
17+
// Define the agent
18+
OpenAIResponseAgent agent = new(this.Client)
19+
{
20+
Name = "ResponseAgent",
21+
Instructions = "Answer all queries in English and French.",
22+
};
23+
24+
// Invoke the agent and output the response
25+
var responseItems = agent.InvokeAsync("What is the capital of France?");
26+
await foreach (ChatMessageContent responseItem in responseItems)
27+
{
28+
WriteAgentChatMessage(responseItem);
29+
}
30+
}
31+
32+
[Fact]
33+
public async Task UseOpenAIResponseAgentStreamingAsync()
34+
{
35+
// Define the agent
36+
OpenAIResponseAgent agent = new(this.Client)
37+
{
38+
Name = "ResponseAgent",
39+
Instructions = "Answer all queries in English and French.",
40+
};
41+
42+
// Invoke the agent and output the response
43+
var responseItems = agent.InvokeStreamingAsync("What is the capital of France?");
44+
await WriteAgentStreamMessageAsync(responseItems);
45+
}
46+
47+
[Fact]
48+
public async Task UseOpenAIResponseAgentWithThreadedConversationAsync()
49+
{
50+
// Define the agent
51+
OpenAIResponseAgent agent = new(this.Client)
52+
{
53+
Name = "ResponseAgent",
54+
Instructions = "Answer all queries in the users preferred language.",
55+
};
56+
57+
string[] messages =
58+
[
59+
"My name is Bob and my preferred language is French.",
60+
"What is the capital of France?",
61+
"What is the capital of Spain?",
62+
"What is the capital of Italy?"
63+
];
64+
65+
// Initial thread can be null as it will be automatically created
66+
AgentThread? agentThread = null;
67+
68+
// Invoke the agent and output the response
69+
foreach (string message in messages)
70+
{
71+
Console.Write($"Agent Thread Id: {agentThread?.Id}");
72+
var responseItems = agent.InvokeAsync(new ChatMessageContent(AuthorRole.User, message), agentThread);
73+
await foreach (AgentResponseItem<ChatMessageContent> responseItem in responseItems)
74+
{
75+
// Update the thread so the previous response id is used
76+
agentThread = responseItem.Thread;
77+
78+
WriteAgentChatMessage(responseItem.Message);
79+
}
80+
}
81+
}
82+
83+
[Fact]
84+
public async Task UseOpenAIResponseAgentWithThreadedConversationStreamingAsync()
85+
{
86+
// Define the agent
87+
OpenAIResponseAgent agent = new(this.Client)
88+
{
89+
Name = "ResponseAgent",
90+
Instructions = "Answer all queries in the users preferred language.",
91+
};
92+
93+
string[] messages =
94+
[
95+
"My name is Bob and my preferred language is French.",
96+
"What is the capital of France?",
97+
"What is the capital of Spain?",
98+
"What is the capital of Italy?"
99+
];
100+
101+
// Initial thread can be null as it will be automatically created
102+
AgentThread? agentThread = null;
103+
104+
// Invoke the agent and output the response
105+
foreach (string message in messages)
106+
{
107+
Console.Write($"Agent Thread Id: {agentThread?.Id}");
108+
var responseItems = agent.InvokeStreamingAsync(new ChatMessageContent(AuthorRole.User, message), agentThread);
109+
110+
// Update the thread so the previous response id is used
111+
agentThread = await WriteAgentStreamMessageAsync(responseItems);
112+
}
113+
}
114+
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.SemanticKernel;
4+
using Microsoft.SemanticKernel.Agents;
5+
using Microsoft.SemanticKernel.Agents.OpenAI;
6+
using Microsoft.SemanticKernel.ChatCompletion;
7+
8+
namespace GettingStarted.OpenAIResponseAgents;
9+
10+
/// <summary>
11+
/// This example demonstrates how to manage conversation state during a model interaction using <see cref="OpenAIResponseAgent"/>.
12+
/// OpenAI provides a few ways to manage conversation state, which is important for preserving information across multiple messages or turns in a conversation.
13+
/// </summary>
14+
public class Step02_OpenAIResponseAgent_ConversationState(ITestOutputHelper output) : BaseResponsesAgentTest(output)
15+
{
16+
[Fact]
17+
public async Task ManuallyConstructPastConversationAsync()
18+
{
19+
// Define the agent
20+
OpenAIResponseAgent agent = new(this.Client)
21+
{
22+
StoreEnabled = false,
23+
};
24+
25+
ICollection<ChatMessageContent> messages =
26+
[
27+
new ChatMessageContent(AuthorRole.User, "knock knock."),
28+
new ChatMessageContent(AuthorRole.Assistant, "Who's there?"),
29+
new ChatMessageContent(AuthorRole.User, "Orange.")
30+
];
31+
foreach (ChatMessageContent message in messages)
32+
{
33+
WriteAgentChatMessage(message);
34+
}
35+
36+
// Invoke the agent and output the response
37+
var responseItems = agent.InvokeAsync(messages);
38+
await foreach (ChatMessageContent responseItem in responseItems)
39+
{
40+
WriteAgentChatMessage(responseItem);
41+
}
42+
}
43+
44+
[Fact]
45+
public async Task ManuallyManageConversationStateWithResponsesChatCompletionApiAsync()
46+
{
47+
// Define the agent
48+
OpenAIResponseAgent agent = new(this.Client)
49+
{
50+
StoreEnabled = false,
51+
};
52+
53+
string[] messages =
54+
[
55+
"Tell me a joke?",
56+
"Tell me another?",
57+
];
58+
59+
// Invoke the agent and output the response
60+
AgentThread? agentThread = null;
61+
foreach (string message in messages)
62+
{
63+
var userMessage = new ChatMessageContent(AuthorRole.User, message);
64+
WriteAgentChatMessage(userMessage);
65+
66+
var responseItems = agent.InvokeAsync(userMessage, agentThread);
67+
await foreach (AgentResponseItem<ChatMessageContent> responseItem in responseItems)
68+
{
69+
agentThread = responseItem.Thread;
70+
WriteAgentChatMessage(responseItem.Message);
71+
}
72+
}
73+
}
74+
75+
[Fact]
76+
public async Task ManageConversationStateWithResponseApiAsync()
77+
{
78+
// Define the agent
79+
OpenAIResponseAgent agent = new(this.Client)
80+
{
81+
StoreEnabled = true,
82+
};
83+
84+
string[] messages =
85+
[
86+
"Tell me a joke?",
87+
"Explain why this is funny.",
88+
];
89+
90+
// Invoke the agent and output the response
91+
AgentThread? agentThread = null;
92+
foreach (string message in messages)
93+
{
94+
var userMessage = new ChatMessageContent(AuthorRole.User, message);
95+
WriteAgentChatMessage(userMessage);
96+
97+
var responseItems = agent.InvokeAsync(userMessage, agentThread);
98+
await foreach (AgentResponseItem<ChatMessageContent> responseItem in responseItems)
99+
{
100+
agentThread = responseItem.Thread;
101+
WriteAgentChatMessage(responseItem.Message);
102+
}
103+
}
104+
105+
// Display the contents in the latest thread
106+
if (agentThread is not null)
107+
{
108+
this.Output.WriteLine("\n\nResponse Thread Messages\n");
109+
var responseAgentThread = agentThread as OpenAIResponseAgentThread;
110+
var threadMessages = responseAgentThread?.GetMessagesAsync();
111+
if (threadMessages is not null)
112+
{
113+
await foreach (var threadMessage in threadMessages)
114+
{
115+
WriteAgentChatMessage(threadMessage);
116+
}
117+
}
118+
119+
await agentThread.DeleteAsync();
120+
}
121+
}
122+
}

dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
<ItemGroup>
3636
<ProjectReference Include="..\..\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj" />
3737
<ProjectReference Include="..\Abstractions\Agents.Abstractions.csproj" />
38+
<ProjectReference Include="..\Core\Agents.Core.csproj" />
3839
</ItemGroup>
3940

4041
<ItemGroup>

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (c) Microsoft. All rights reserved.
2+
using System;
23
using System.Collections.Generic;
34
using System.Linq;
45
using Microsoft.SemanticKernel.Agents.OpenAI.Internal;
56
using OpenAI.Assistants;
7+
using OpenAI.Responses;
68

79
namespace Microsoft.SemanticKernel.Agents.OpenAI;
810

@@ -33,4 +35,21 @@ public static IEnumerable<ThreadInitializationMessage> ToThreadInitializationMes
3335
{
3436
return messages.Select(message => message.ToThreadInitializationMessage());
3537
}
38+
39+
/// <summary>
40+
/// Converts a <see cref="ChatMessageContent"/> instance to a <see cref="ResponseItem"/>.
41+
/// </summary>
42+
/// <param name="message">The chat message content to convert.</param>
43+
/// <returns>A <see cref="ResponseItem"/> instance.</returns>
44+
public static ResponseItem ToResponseItem(this ChatMessageContent message)
45+
{
46+
return message.Role.Label switch
47+
{
48+
"system" => ResponseItem.CreateSystemMessageItem(message.Content),
49+
"user" => ResponseItem.CreateUserMessageItem(message.Content),
50+
"developer" => ResponseItem.CreateDeveloperMessageItem(message.Content),
51+
"assistant" => ResponseItem.CreateAssistantMessageItem(message.Content),
52+
_ => throw new NotSupportedException($"Unsupported role {message.Role.Label}. Only system, user, developer or assistant roles are allowed."),
53+
};
54+
}
3655
}

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

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright (c) Microsoft. All rights reserved.
22
using System;
33
using OpenAI.Assistants;
4+
using OpenAI.Responses;
45

56
namespace Microsoft.SemanticKernel.Agents.OpenAI;
67

@@ -33,4 +34,38 @@ public static FunctionToolDefinition ToToolDefinition(this KernelFunction functi
3334
Description = function.Description
3435
};
3536
}
37+
38+
/// <summary>
39+
/// Converts a <see cref="KernelFunction"/> into a <see cref="ResponseTool"/> representation.
40+
/// </summary>
41+
/// <remarks>If the <paramref name="function"/> has parameters, they are included in the resulting <see
42+
/// cref="ResponseTool"/> as a serialized parameter specification. Otherwise, the parameters are set to <see
43+
/// langword="null"/>.</remarks>
44+
/// <param name="function">The <see cref="KernelFunction"/> to convert.</param>
45+
/// <param name="pluginName">An optional plugin name to associate with the function. If not provided, the function's default plugin name is
46+
/// used.</param>
47+
/// <param name="functionSchemaIsStrict">A value indicating whether the function's schema should be treated as strict. If <see langword="true"/>, the
48+
/// schema will enforce stricter validation rules.</param>
49+
/// <returns>A <see cref="ResponseTool"/> that represents the specified <see cref="KernelFunction"/>.</returns>
50+
public static ResponseTool ToResponseTool(this KernelFunction function, string? pluginName = null, bool functionSchemaIsStrict = false)
51+
{
52+
if (function.Metadata.Parameters.Count > 0)
53+
{
54+
BinaryData parameterData = function.Metadata.CreateParameterSpec();
55+
return ResponseTool.CreateFunctionTool(
56+
functionName: FunctionName.ToFullyQualifiedName(function.Name, pluginName ?? function.PluginName),
57+
functionDescription: function.Description,
58+
functionParameters: parameterData,
59+
functionSchemaIsStrict: functionSchemaIsStrict);
60+
}
61+
62+
return ResponseTool.CreateFunctionTool(
63+
functionName: FunctionName.ToFullyQualifiedName(function.Name, pluginName ?? function.PluginName),
64+
functionDescription: function.Description,
65+
functionParameters: s_emptyFunctionParameters);
66+
}
67+
68+
#region private
69+
private static readonly BinaryData s_emptyFunctionParameters = BinaryData.FromString("{}");
70+
#endregion
3671
}

0 commit comments

Comments
 (0)