chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:21:23 +08:00
commit b957a53def
5423 changed files with 863745 additions and 0 deletions
@@ -0,0 +1,193 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Headers;
using System.Web;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.CredentialManagers;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Microsoft.SemanticKernel.Plugins.OpenApi.Extensions;
namespace Plugins;
/// <summary>
/// These examples demonstrate how to use API Manifest plugins to call Microsoft Graph and NASA APIs.
/// API Manifest plugins are created from the OpenAPI document and the manifest file.
/// The manifest file contains the API dependencies and their execution parameters.
/// The manifest file also contains the authentication information for the APIs, however this is not used by the extension method and MUST be setup separately at the moment, which the example demonstrates.
///
/// Important stages being demonstrated:
/// 1. Load APIManifest plugins
/// 2. Configure authentication for the APIs
/// 3. Call functions from the loaded plugins
///
/// Running this test requires the following configuration in `dotnet\samples\Concepts\bin\Debug\net10.0\appsettings.Development.json`:
///
/// ```json
/// {
/// "MSGraph": {
/// "ClientId": "clientId",
/// "TenantId": "tenantId",
/// "Scopes": [
/// "Calendars.Read",
/// "Contacts.Read",
/// "Files.Read.All",
/// "Mail.Read",
/// "User.Read"
/// ],
/// "RedirectUri": "http://localhost"
/// }
/// }
///```
///
/// Replace the clientId and TenantId by your own values.
///
/// To create the application registration:
/// 1. Go to https://aad.portal.azure.com
/// 2. Select create a new application registration
/// 3. Select new public client (add the redirect URI).
/// 4. Navigate to API access, add the listed Microsoft Graph delegated scopes.
/// 5. Grant consent after adding the scopes.
///
/// During the first run, your browser will open to get the token.
///
/// </summary>
/// <param name="output">The output helper to use to the test can emit status information</param>
public class ApiManifestBasedPlugins(ITestOutputHelper output) : BaseTest(output)
{
private static readonly PromptExecutionSettings s_promptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowStrictSchemaAdherence = true
}
)
};
public static readonly IEnumerable<object[]> s_parameters =
[
// function names are sanitized operationIds from the OpenAPI document
["MessagesPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drive_root_GetChildrenContent", new KernelArguments(s_promptExecutionSettings) { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments(s_promptExecutionSettings) { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],
#region Multiple API dependencies (multiple auth requirements) scenario within the same plugin
// Graph API uses MSAL
["AstronomyPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "AstronomyPlugin"],
// Astronomy API uses API key authentication
["AstronomyPlugin", "apod", new KernelArguments(s_promptExecutionSettings) { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
#endregion
];
[Theory, MemberData(nameof(s_parameters))]
public async Task RunApiManifestPluginAsync(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
{
WriteSampleHeadingToConsole(pluginToTest, functionToTest, arguments, pluginsToLoad);
var kernel = Kernel.CreateBuilder().Build();
await AddApiManifestPluginsAsync(kernel, pluginsToLoad);
var result = await kernel.InvokeAsync(pluginToTest, functionToTest, arguments);
Console.WriteLine("--------------------");
Console.WriteLine($"\nResult:\n{result}\n");
Console.WriteLine("--------------------");
}
private void WriteSampleHeadingToConsole(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
{
Console.WriteLine();
Console.WriteLine("======== [ApiManifest Plugins Sample] ========");
Console.WriteLine($"======== Loading Plugins: {string.Join(" ", pluginsToLoad)} ========");
Console.WriteLine($"======== Calling Plugin Function: {pluginToTest}.{functionToTest} with parameters {arguments?.Select(x => x.Key + " = " + x.Value).Aggregate((x, y) => x + ", " + y)} ========");
Console.WriteLine();
}
private async Task AddApiManifestPluginsAsync(Kernel kernel, params string[] pluginNames)
{
#pragma warning disable SKEXP0050
if (TestConfiguration.MSGraph.Scopes is null)
{
throw new InvalidOperationException("Missing Scopes configuration for Microsoft Graph API.");
}
LocalUserMSALCredentialManager credentialManager = await LocalUserMSALCredentialManager.CreateAsync().ConfigureAwait(false);
var token = await credentialManager.GetTokenAsync(
TestConfiguration.MSGraph.ClientId,
TestConfiguration.MSGraph.TenantId,
TestConfiguration.MSGraph.Scopes.ToArray(),
TestConfiguration.MSGraph.RedirectUri).ConfigureAwait(false);
#pragma warning restore SKEXP0050
BearerAuthenticationProviderWithCancellationToken authenticationProvider = new(() => Task.FromResult(token));
#pragma warning disable SKEXP0040
// Microsoft Graph API execution parameters
var graphOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: authenticationProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"),
enableDynamicOperationPayload: false,
enablePayloadNamespacing: false);
// NASA API execution parameters
var nasaOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: async (request, cancellationToken) =>
{
var uriBuilder = new UriBuilder(request.RequestUri ?? throw new InvalidOperationException("The request URI is null."));
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["api_key"] = "DEMO_KEY";
uriBuilder.Query = query.ToString();
request.RequestUri = uriBuilder.Uri;
},
enableDynamicOperationPayload: false,
enablePayloadNamespacing: false);
var apiManifestPluginParameters = new ApiManifestPluginParameters(
functionExecutionParameters: new()
{
{ "microsoft.graph", graphOpenApiFunctionExecutionParameters },
{ "nasa", nasaOpenApiFunctionExecutionParameters }
});
var manifestLookupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Resources", "Plugins", "ApiManifestPlugins");
foreach (var pluginName in pluginNames)
{
try
{
KernelPlugin plugin =
await kernel.ImportPluginFromApiManifestAsync(
pluginName,
Path.Combine(manifestLookupDirectory, pluginName, "apimanifest.json"),
apiManifestPluginParameters)
.ConfigureAwait(false);
Console.WriteLine($">> {pluginName} is created.");
#pragma warning restore SKEXP0040
}
catch (Exception ex)
{
kernel.LoggerFactory.CreateLogger("Plugin Creation").LogError(ex, "Plugin creation failed. Message: {0}", ex.Message);
throw new AggregateException($"Plugin creation failed for {pluginName}", ex);
}
}
}
}
/// <summary>
/// Retrieves a token via the provided delegate and applies it to HTTP requests using the
/// "bearer" authentication scheme.
/// </summary>
public class BearerAuthenticationProviderWithCancellationToken(Func<Task<string>> bearerToken)
{
private readonly Func<Task<string>> _bearerToken = bearerToken;
/// <summary>
/// Applies the token to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
/// <param name="cancellationToken"></param>
public async Task AuthenticateRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
var token = await this._bearerToken().ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
@@ -0,0 +1,260 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using xRetry;
namespace Plugins;
public class ConversationSummaryPlugin(ITestOutputHelper output) : BaseTest(output)
{
private const string ChatTranscript =
@"
John: Hello, how are you?
Jane: I'm fine, thanks. How are you?
John: I'm doing well, writing some example code.
Jane: That's great! I'm writing some example code too.
John: What are you writing?
Jane: I'm writing a chatbot.
John: That's cool. I'm writing a chatbot too.
Jane: What language are you writing it in?
John: I'm writing it in C#.
Jane: I'm writing it in Python.
John: That's cool. I need to learn Python.
Jane: I need to learn C#.
John: Can I try out your chatbot?
Jane: Sure, here's the link.
John: Thanks!
Jane: You're welcome.
Jane: Look at this poem my chatbot wrote:
Jane: Roses are red
Jane: Violets are blue
Jane: I'm writing a chatbot
Jane: What about you?
John: That's cool. Let me see if mine will write a poem, too.
John: Here's a poem my chatbot wrote:
John: The singularity of the universe is a mystery.
John: The universe is a mystery.
John: The universe is a mystery.
John: The universe is a mystery.
John: Looks like I need to improve mine, oh well.
Jane: You might want to try using a different model.
Jane: I'm using the GPT-3 model.
John: I'm using the GPT-2 model. That makes sense.
John: Here is a new poem after updating the model.
John: The universe is a mystery.
John: The universe is a mystery.
John: The universe is a mystery.
John: Yikes, it's really stuck isn't it. Would you help me debug my code?
Jane: Sure, what's the problem?
John: I'm not sure. I think it's a bug in the code.
Jane: I'll take a look.
Jane: I think I found the problem.
Jane: It looks like you're not passing the right parameters to the model.
John: Thanks for the help!
Jane: I'm now writing a bot to summarize conversations. I want to make sure it works when the conversation is long.
John: So you need to keep talking with me to generate a long conversation?
Jane: Yes, that's right.
John: Ok, I'll keep talking. What should we talk about?
Jane: I don't know, what do you want to talk about?
John: I don't know, it's nice how CoPilot is doing most of the talking for us. But it definitely gets stuck sometimes.
Jane: I agree, it's nice that CoPilot is doing most of the talking for us.
Jane: But it definitely gets stuck sometimes.
John: Do you know how long it needs to be?
Jane: I think the max length is 1024 tokens. Which is approximately 1024*4= 4096 characters.
John: That's a lot of characters.
Jane: Yes, it is.
John: I'm not sure how much longer I can keep talking.
Jane: I think we're almost there. Let me check.
Jane: I have some bad news, we're only half way there.
John: Oh no, I'm not sure I can keep going. I'm getting tired.
Jane: I'm getting tired too.
John: Maybe there is a large piece of text we can use to generate a long conversation.
Jane: That's a good idea. Let me see if I can find one. Maybe Lorem Ipsum?
John: Yeah, that's a good idea.
Jane: I found a Lorem Ipsum generator.
Jane: Here's a 4096 character Lorem Ipsum text:
Jane: Lorem ipsum dolor sit amet, con
Jane: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc sit amet aliquam
Jane: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc sit amet aliquam
Jane: Darn, it's just repeating stuff now.
John: I think we're done.
Jane: We're not though! We need like 1500 more characters.
John: Oh Cananda, our home and native land.
Jane: True patriot love in all thy sons command.
John: With glowing hearts we see thee rise.
Jane: The True North strong and free.
John: From far and wide, O Canada, we stand on guard for thee.
Jane: God keep our land glorious and free.
John: O Canada, we stand on guard for thee.
Jane: O Canada, we stand on guard for thee.
Jane: That was fun, thank you. Let me check now.
Jane: I think we need about 600 more characters.
John: Oh say can you see?
Jane: By the dawn's early light.
John: What so proudly we hailed.
Jane: At the twilight's last gleaming.
John: Whose broad stripes and bright stars.
Jane: Through the perilous fight.
John: O'er the ramparts we watched.
Jane: Were so gallantly streaming.
John: And the rockets' red glare.
Jane: The bombs bursting in air.
John: Gave proof through the night.
Jane: That our flag was still there.
John: Oh say does that star-spangled banner yet wave.
Jane: O'er the land of the free.
John: And the home of the brave.
Jane: Are you a Seattle Kraken Fan?
John: Yes, I am. I love going to the games.
Jane: I'm a Seattle Kraken Fan too. Who is your favorite player?
John: I like watching all the players, but I think my favorite is Matty Beniers.
Jane: Yeah, he's a great player. I like watching him too. I also like watching Jaden Schwartz.
John: Adam Larsson is another good one. The big cat!
Jane: WE MADE IT! It's long enough. Thank you!
John: You're welcome. I'm glad we could help. Goodbye!
Jane: Goodbye!
";
[RetryFact(typeof(HttpOperationException))]
public async Task RunAsync()
{
await ConversationSummaryPluginAsync();
await GetConversationActionItemsAsync();
await GetConversationTopicsAsync();
}
private async Task ConversationSummaryPluginAsync()
{
Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Summarize ========");
Kernel kernel = InitializeKernel();
KernelPlugin conversationSummaryPlugin = kernel.ImportPluginFromType<Microsoft.SemanticKernel.Plugins.Core.ConversationSummaryPlugin>();
FunctionResult summary = await kernel.InvokeAsync(
conversationSummaryPlugin["SummarizeConversation"], new() { ["input"] = ChatTranscript });
Console.WriteLine("Generated Summary:");
Console.WriteLine(summary.GetValue<string>());
}
private async Task GetConversationActionItemsAsync()
{
Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Action Items ========");
Kernel kernel = InitializeKernel();
KernelPlugin conversationSummary = kernel.ImportPluginFromType<Microsoft.SemanticKernel.Plugins.Core.ConversationSummaryPlugin>();
FunctionResult summary = await kernel.InvokeAsync(
conversationSummary["GetConversationActionItems"], new() { ["input"] = ChatTranscript });
Console.WriteLine("Generated Action Items:");
Console.WriteLine(summary.GetValue<string>());
}
private async Task GetConversationTopicsAsync()
{
Console.WriteLine("======== SamplePlugins - Conversation Summary Plugin - Topics ========");
Kernel kernel = InitializeKernel();
KernelPlugin conversationSummary = kernel.ImportPluginFromType<Microsoft.SemanticKernel.Plugins.Core.ConversationSummaryPlugin>();
FunctionResult summary = await kernel.InvokeAsync(
conversationSummary["GetConversationTopics"], new() { ["input"] = ChatTranscript });
Console.WriteLine("Generated Topics:");
Console.WriteLine(summary.GetValue<string>());
}
private Kernel InitializeKernel()
{
Kernel kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
endpoint: TestConfiguration.AzureOpenAI.Endpoint,
apiKey: TestConfiguration.AzureOpenAI.ApiKey,
modelId: TestConfiguration.AzureOpenAI.ChatModelId)
.Build();
return kernel;
}
}
/* Example Output:
======== SamplePlugins - Conversation Summary Plugin - Summarize ========
Generated Summary:
A possible summary is:
- John and Jane are both writing chatbots in different languages and share their links and poems.
- John's chatbot has a problem with writing repetitive poems and Jane helps him debug his code.
- Jane is writing a bot to summarize conversations and needs to generate a long conversation with John to test it.
- They use CoPilot to do most of the talking for them and comment on its limitations.
- They estimate the max length of the conversation to be 4096 characters.
A possible summary is:
- John and Jane are trying to generate a long conversation for some purpose.
- They are getting tired and bored of talking and look for ways to fill up the text.
- They use a Lorem Ipsum generator, but it repeats itself after a while.
- They sing the national anthems of Canada and the United States, and then talk about their favorite Seattle Kraken hockey players.
- They finally reach their desired length of text and say goodbye to each other.
======== SamplePlugins - Conversation Summary Plugin - Action Items ========
Generated Action Items:
{
"actionItems": [
{
"owner": "John",
"actionItem": "Improve chatbot's poem generation",
"dueDate": "",
"status": "In Progress",
"notes": "Using GPT-3 model"
},
{
"owner": "Jane",
"actionItem": "Write a bot to summarize conversations",
"dueDate": "",
"status": "In Progress",
"notes": "Testing with long conversations"
}
]
}
{
"action_items": []
}
======== SamplePlugins - Conversation Summary Plugin - Topics ========
Generated Topics:
{
"topics": [
"Chatbot",
"Code",
"Poem",
"Model",
"GPT-3",
"GPT-2",
"Bug",
"Parameters",
"Summary",
"CoPilot",
"Tokens",
"Characters"
]
}
{
"topics": [
"Long conversation",
"Lorem Ipsum",
"O Canada",
"Star-Spangled Banner",
"Seattle Kraken",
"Matty Beniers",
"Jaden Schwartz",
"Adam Larsson"
]
}
*/
@@ -0,0 +1,300 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Web;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors.CredentialManagers;
using Microsoft.SemanticKernel.Plugins.OpenApi;
using Microsoft.SemanticKernel.Plugins.OpenApi.Extensions;
namespace Plugins;
/// <summary>
/// These examples demonstrate how to use Copilot Agent plugins to call Microsoft Graph and NASA APIs.
/// Copilot Agent Plugins are created from the OpenAPI document and the manifest file.
/// The manifest file contains the API dependencies and their execution parameters.
/// The manifest file also contains the authentication information for the APIs, however this is not used by the extension method and MUST be setup separately at the moment, which the example demonstrates.
///
/// Important stages being demonstrated:
/// 1. Load Copilot Agent Plugins
/// 2. Configure authentication for the APIs
/// 3. Call functions from the loaded plugins
///
/// Running this test requires the following configuration in `dotnet\samples\Concepts\bin\Debug\net10.0\appsettings.Development.json`:
///
/// ```json
/// {
/// "MSGraph": {
/// "ClientId": "clientId",
/// "TenantId": "tenantId",
/// "Scopes": [
/// "Calendars.Read",
/// "Contacts.Read",
/// "Files.Read.All",
/// "Mail.Read",
/// "User.Read"
/// ],
/// "RedirectUri": "http://localhost"
/// }
/// }
///```
///
/// Replace the clientId and TenantId by your own values.
///
/// To create the application registration:
/// 1. Go to https://aad.portal.azure.com
/// 2. Select create a new application registration
/// 3. Select new public client (add the redirect URI).
/// 4. Navigate to API access, add the listed Microsoft Graph delegated scopes.
/// 5. Grant consent after adding the scopes.
///
/// During the first run, your browser will open to get the token.
///
/// </summary>
/// <param name="output">The output helper to use to the test can emit status information</param>
public class CopilotAgentBasedPlugins(ITestOutputHelper output) : BaseTest(output)
{
private static readonly PromptExecutionSettings s_promptExecutionSettings = new()
{
FunctionChoiceBehavior = FunctionChoiceBehavior.Auto(
options: new FunctionChoiceBehaviorOptions
{
AllowStrictSchemaAdherence = true
}
)
};
public static readonly IEnumerable<object[]> s_parameters =
[
// function names are sanitized operationIds from the OpenAPI document
["MessagesPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "MessagesPlugin"],
["DriveItemPlugin", "drives_GetItemsContent", new KernelArguments(s_promptExecutionSettings) { { "driveItem-Id", "test.txt" } }, "DriveItemPlugin", "MessagesPlugin"],
["ContactsPlugin", "me_ListContacts", new KernelArguments(s_promptExecutionSettings) { { "_count", "true" } }, "ContactsPlugin", "MessagesPlugin"],
["CalendarPlugin", "me_calendar_ListEvents", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "CalendarPlugin", "MessagesPlugin"],
// Multiple API dependencies (multiple auth requirements) scenario within the same plugin
// Graph API uses MSAL
["AstronomyPlugin", "me_ListMessages", new KernelArguments(s_promptExecutionSettings) { { "_top", "1" } }, "AstronomyPlugin"],
// Astronomy API uses API key authentication
["AstronomyPlugin", "apod", new KernelArguments(s_promptExecutionSettings) { { "_date", "2022-02-02" } }, "AstronomyPlugin"],
];
[Theory, MemberData(nameof(s_parameters))]
public async Task RunCopilotAgentPluginAsync(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
{
WriteSampleHeadingToConsole(pluginToTest, functionToTest, arguments, pluginsToLoad);
var kernel = new Kernel();
await AddCopilotAgentPluginsAsync(kernel, pluginsToLoad);
var result = await kernel.InvokeAsync(pluginToTest, functionToTest, arguments);
Console.WriteLine("--------------------");
Console.WriteLine($"\nResult:\n{result}\n");
Console.WriteLine("--------------------");
}
private void WriteSampleHeadingToConsole(string pluginToTest, string functionToTest, KernelArguments? arguments, params string[] pluginsToLoad)
{
Console.WriteLine();
Console.WriteLine("======== [CopilotAgent Plugins Sample] ========");
Console.WriteLine($"======== Loading Plugins: {string.Join(" ", pluginsToLoad)} ========");
Console.WriteLine($"======== Calling Plugin Function: {pluginToTest}.{functionToTest} with parameters {arguments?.Select(x => x.Key + " = " + x.Value).Aggregate((x, y) => x + ", " + y)} ========");
Console.WriteLine();
}
private static readonly HashSet<string> s_fieldsToIgnore = new(
[
"@odata.type",
"attachments",
"allowNewTimeProposals",
"bccRecipients",
"bodyPreview",
"calendar",
"categories",
"ccRecipients",
"changeKey",
"conversationId",
"coordinates",
"conversationIndex",
"createdDateTime",
"discriminator",
"lastModifiedDateTime",
"locations",
"extensions",
"flag",
"from",
"hasAttachments",
"iCalUId",
"id",
"inferenceClassification",
"internetMessageHeaders",
"instances",
"isCancelled",
"isDeliveryReceiptRequested",
"isDraft",
"isOrganizer",
"isRead",
"isReadReceiptRequested",
"multiValueExtendedProperties",
"onlineMeeting",
"onlineMeetingProvider",
"onlineMeetingUrl",
"organizer",
"originalStart",
"parentFolderId",
"range",
"receivedDateTime",
"recurrence",
"replyTo",
"sender",
"sentDateTime",
"seriesMasterId",
"singleValueExtendedProperties",
"transactionId",
"time",
"uniqueBody",
"uniqueId",
"uniqueIdType",
"webLink",
],
StringComparer.OrdinalIgnoreCase
);
private const string RequiredPropertyName = "required";
private const string PropertiesPropertyName = "properties";
/// <summary>
/// Trims the properties from the request body schema.
/// Most models in strict mode enforce a limit on the properties.
/// </summary>
/// <param name="schema">Source schema</param>
/// <returns>the trimmed schema for the request body</returns>
private static KernelJsonSchema? TrimPropertiesFromRequestBody(KernelJsonSchema? schema)
{
if (schema is null)
{
return null;
}
var originalSchema = JsonSerializer.Serialize(schema.RootElement);
var node = JsonNode.Parse(originalSchema);
if (node is not JsonObject jsonNode)
{
return schema;
}
TrimPropertiesFromJsonNode(jsonNode);
return KernelJsonSchema.Parse(node.ToString());
}
private static void TrimPropertiesFromJsonNode(JsonNode jsonNode)
{
if (jsonNode is not JsonObject jsonObject)
{
return;
}
if (jsonObject.TryGetPropertyValue(RequiredPropertyName, out var requiredRawValue) && requiredRawValue is JsonArray requiredArray)
{
jsonNode[RequiredPropertyName] = new JsonArray(requiredArray.Where(x => x is not null).Select(x => x!.GetValue<string>()).Where(x => !s_fieldsToIgnore.Contains(x)).Select(x => JsonValue.Create(x)).ToArray());
}
if (jsonObject.TryGetPropertyValue(PropertiesPropertyName, out var propertiesRawValue) && propertiesRawValue is JsonObject propertiesObject)
{
var properties = propertiesObject.Where(x => s_fieldsToIgnore.Contains(x.Key)).Select(static x => x.Key).ToArray();
foreach (var property in properties)
{
propertiesObject.Remove(property);
}
}
foreach (var subProperty in jsonObject)
{
if (subProperty.Value is not null)
{
TrimPropertiesFromJsonNode(subProperty.Value);
}
}
}
private static readonly RestApiParameterFilter s_restApiParameterFilter = (RestApiParameterFilterContext context) =>
{
if (("me_sendMail".Equals(context.Operation.Id, StringComparison.OrdinalIgnoreCase) ||
("me_calendar_CreateEvents".Equals(context.Operation.Id, StringComparison.OrdinalIgnoreCase)) &&
"payload".Equals(context.Parameter.Name, StringComparison.OrdinalIgnoreCase)))
{
context.Parameter.Schema = TrimPropertiesFromRequestBody(context.Parameter.Schema);
return context.Parameter;
}
return context.Parameter;
};
internal static async Task<CopilotAgentPluginParameters> GetAuthenticationParametersAsync()
{
if (TestConfiguration.MSGraph.Scopes is null)
{
throw new InvalidOperationException("Missing Scopes configuration for Microsoft Graph API.");
}
LocalUserMSALCredentialManager credentialManager = await LocalUserMSALCredentialManager.CreateAsync().ConfigureAwait(false);
var token = await credentialManager.GetTokenAsync(
TestConfiguration.MSGraph.ClientId,
TestConfiguration.MSGraph.TenantId,
TestConfiguration.MSGraph.Scopes.ToArray(),
TestConfiguration.MSGraph.RedirectUri).ConfigureAwait(false);
#pragma warning restore SKEXP0050
BearerAuthenticationProviderWithCancellationToken authenticationProvider = new(() => Task.FromResult(token));
#pragma warning disable SKEXP0040
// Microsoft Graph API execution parameters
var graphOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: authenticationProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri("https://graph.microsoft.com/v1.0"),
enableDynamicOperationPayload: false,
enablePayloadNamespacing: false)
{
ParameterFilter = s_restApiParameterFilter
};
// NASA API execution parameters
var nasaOpenApiFunctionExecutionParameters = new OpenApiFunctionExecutionParameters(
authCallback: async (request, cancellationToken) =>
{
var uriBuilder = new UriBuilder(request.RequestUri ?? throw new InvalidOperationException("The request URI is null."));
var query = HttpUtility.ParseQueryString(uriBuilder.Query);
query["api_key"] = "DEMO_KEY";
uriBuilder.Query = query.ToString();
request.RequestUri = uriBuilder.Uri;
},
enableDynamicOperationPayload: false,
enablePayloadNamespacing: false);
var apiManifestPluginParameters = new CopilotAgentPluginParameters
{
FunctionExecutionParameters = new()
{
{ "https://graph.microsoft.com/v1.0", graphOpenApiFunctionExecutionParameters },
{ "https://api.nasa.gov/planetary", nasaOpenApiFunctionExecutionParameters }
}
};
return apiManifestPluginParameters;
}
private async Task AddCopilotAgentPluginsAsync(Kernel kernel, params string[] pluginNames)
{
#pragma warning disable SKEXP0050
var apiManifestPluginParameters = await GetAuthenticationParametersAsync().ConfigureAwait(false);
var manifestLookupDirectory = Path.Combine(Directory.GetCurrentDirectory(), "..", "..", "..", "Resources", "Plugins", "CopilotAgentPlugins");
foreach (var pluginName in pluginNames)
{
try
{
#pragma warning disable CA1308 // Normalize strings to uppercase
await kernel.ImportPluginFromCopilotAgentPluginAsync(
pluginName,
Path.Combine(manifestLookupDirectory, pluginName, $"{pluginName[..^6].ToLowerInvariant()}-apiplugin.json"),
apiManifestPluginParameters)
.ConfigureAwait(false);
#pragma warning restore CA1308 // Normalize strings to uppercase
Console.WriteLine($">> {pluginName} is created.");
#pragma warning restore SKEXP0040
}
catch (Exception ex)
{
Console.WriteLine("Plugin creation failed. Message: {0}", ex.Message);
throw new AggregateException($"Plugin creation failed for {pluginName}", ex);
}
}
}
}
@@ -0,0 +1,107 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// Examples to show how to create plugins from OpenAPI specs.
/// </summary>
public class CreatePluginFromOpenApiSpec_Github(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// Example to show how to consume operation extensions and other metadata from an OpenAPI spec.
/// Try modifying the sample schema to simulate the other cases by
/// 1. Changing the value of x-openai-isConsequential to true and see how the function execution is skipped.
/// 2. Removing the x-openai-isConsequential property and see how the function execution is skipped.
/// </summary>
[Fact]
public async Task RunOpenAIPluginWithMetadataAsync()
{
Kernel kernel = new();
// This HTTP client is optional. SK will fallback to a default internal one if omitted.
using HttpClient httpClient = new();
// Create a sample OpenAPI schema that calls the github versions api, and has an operation extension property.
// The x-openai-isConsequential property is the operation extension property.
var schema = """
{
"openapi": "3.0.1",
"info": {
"title": "Github Versions API",
"version": "1.0.0"
},
"servers": [ { "url": "https://api.github.com" } ],
"paths": {
"/versions": {
"get": {
"x-openai-isConsequential": false,
"operationId": "getVersions",
"responses": {
"200": {
"description": "OK"
}
}
}
}
}
}
""";
var schemaStream = new MemoryStream();
WriteStringToStream(schemaStream, schema);
// Import an Open API plugin from a stream.
var plugin = await kernel.CreatePluginFromOpenApiAsync("GithubVersionsApi", schemaStream, new OpenApiFunctionExecutionParameters(httpClient));
// Get the function to be invoked and its metadata and extension properties.
var function = plugin["getVersions"];
function.Metadata.AdditionalProperties.TryGetValue("operation-extensions", out var extensionsObject);
var operationExtensions = extensionsObject as Dictionary<string, object?>;
// *******************************************************************************************************************************
// ******* Use case 1: Consume the x-openai-isConsequential extension value to determine if the function has consequences *******
// ******* and only invoke the function if it is consequence free. *******
// *******************************************************************************************************************************
if (operationExtensions is null || !operationExtensions.TryGetValue("x-openai-isConsequential", out var isConsequential) || isConsequential is null)
{
Console.WriteLine("We cannot determine if the function has consequences, since the isConsequential extension is not provided, so safer not to run it.");
}
else if ((isConsequential as bool?) == true)
{
Console.WriteLine("This function may have unwanted consequences, so safer not to run it.");
}
else
{
// Invoke the function and output the result.
var functionResult = await kernel.InvokeAsync(function);
var result = functionResult.GetValue<RestApiOperationResponse>();
Console.WriteLine($"Function execution result: {result?.Content}");
}
// *******************************************************************************************************************************
// ******* Use case 2: Consume the http method type to determine if this is a read or write operation and only execute if *******
// ******* it is a read operation. *******
// *******************************************************************************************************************************
if (function.Metadata.AdditionalProperties.TryGetValue("method", out var method) && method as string is "GET")
{
// Invoke the function and output the result.
var functionResult = await kernel.InvokeAsync(function);
var result = functionResult.GetValue<RestApiOperationResponse>();
Console.WriteLine($"Function execution result: {result?.Content}");
}
else
{
Console.WriteLine("This is a write operation, so safer not to run it.");
}
}
private static void WriteStringToStream(MemoryStream stream, string input)
{
using var writer = new StreamWriter(stream, leaveOpen: true);
writer.Write(input);
writer.Flush();
stream.Position = 0;
}
}
@@ -0,0 +1,211 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.Identity.Client;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
public class CreatePluginFromOpenApiSpec_Jira(ITestOutputHelper output) : BaseTest(output)
{
private static readonly JsonSerializerOptions s_jsonOptionsCache = new()
{
WriteIndented = true
};
/// <summary>
/// This sample shows how to connect the Semantic Kernel to Jira as an Open API plugin based on the Open API schema.
/// This format of registering the plugin and its operations, and subsequently executing those operations can be applied
/// to an Open API plugin that follows the Open API Schema.
/// To use this example, there are a few requirements:
/// 1. You must have a Jira instance that you can authenticate to with your email and api key.
/// Follow the instructions here to get your api key:
/// https://support.atlassian.com/atlassian-account/docs/manage-api-tokens-for-your-atlassian-account/
/// 2. You must create a new project in your Jira instance and create two issues named TEST-1 and TEST-2 respectively.
/// Follow the instructions here to create a new project and issues:
/// https://support.atlassian.com/jira-software-cloud/docs/create-a-new-project/
/// https://support.atlassian.com/jira-software-cloud/docs/create-an-issue-and-a-sub-task/
/// 3. You can find your domain under the "Products" tab in your account management page.
/// To go to your account management page, click on your profile picture in the top right corner of your Jira
/// instance then select "Manage account".
/// 4. Configure the secrets as described by the ReadMe.md in the dotnet/samples/Concepts folder.
/// </summary>
[Fact(Skip = "Setup credentials")]
public async Task RunAsync()
{
Kernel kernel = new();
// Change <your-domain> to a jira instance you have access to with your authentication credentials
string serverUrl = $"https://{TestConfiguration.Jira.Domain}.atlassian.net/rest/api/latest/";
KernelPlugin jiraFunctions;
var tokenProvider = new BasicAuthenticationProvider(() =>
{
string s = $"{TestConfiguration.Jira.Email}:{TestConfiguration.Jira.ApiKey}";
return Task.FromResult(s);
});
using HttpClient httpClient = new();
// The bool useLocalFile can be used to toggle the ingestion method for the openapi schema between a file path and a URL
bool useLocalFile = true;
if (useLocalFile)
{
var apiPluginFile = "./../../../../Plugins/JiraPlugin/openapi.json";
jiraFunctions = await kernel.ImportPluginFromOpenApiAsync(
"jiraPlugin",
apiPluginFile,
new OpenApiFunctionExecutionParameters(
authCallback: tokenProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri(serverUrl)
)
);
}
else
{
var apiPluginRawFileURL = new Uri("https://raw.githubusercontent.com/microsoft/PowerPlatformConnectors/dev/certified-connectors/JIRA/apiDefinition.swagger.json");
jiraFunctions = await kernel.ImportPluginFromOpenApiAsync(
"jiraPlugin",
apiPluginRawFileURL,
new OpenApiFunctionExecutionParameters(
httpClient, tokenProvider.AuthenticateRequestAsync,
serverUrlOverride: new Uri(serverUrl)
)
);
}
var arguments = new KernelArguments
{
// GetIssue Function
// Set Properties for the Get Issue operation in the openAPI.swagger.json
// Make sure the issue exists in your Jira instance or it will return a 404
["issueKey"] = "TEST-1"
};
// Run operation via the semantic kernel
var result = await kernel.InvokeAsync(jiraFunctions["GetIssue"], arguments);
Console.WriteLine("\n\n\n");
var formattedContent = JsonSerializer.Serialize(
result.GetValue<RestApiOperationResponse>(), s_jsonOptionsCache);
Console.WriteLine($"GetIssue jiraPlugin response: \n{formattedContent}");
// AddComment Function
arguments["issueKey"] = "TEST-2";
arguments[RestApiOperation.PayloadArgumentName] = """{"body": "Here is a rad comment"}""";
// Run operation via the semantic kernel
result = await kernel.InvokeAsync(jiraFunctions["AddComment"], arguments);
Console.WriteLine("\n\n\n");
formattedContent = JsonSerializer.Serialize(result.GetValue<RestApiOperationResponse>(), s_jsonOptionsCache);
Console.WriteLine($"AddComment jiraPlugin response: \n{formattedContent}");
}
#region Example of authentication providers
/// <summary>
/// Retrieves authentication content (e.g. username/password, API key) via the provided delegate and
/// applies it to HTTP requests using the "basic" authentication scheme.
/// </summary>
public class BasicAuthenticationProvider(Func<Task<string>> credentials)
{
private readonly Func<Task<string>> _credentials = credentials;
/// <summary>
/// Applies the authentication content to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
/// <param name="cancellationToken">The cancellation token.</param>
public async Task AuthenticateRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
{
// Base64 encode
string encodedContent = Convert.ToBase64String(Encoding.UTF8.GetBytes(await this._credentials().ConfigureAwait(false)));
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", encodedContent);
}
}
/// <summary>
/// Retrieves a token via the provided delegate and applies it to HTTP requests using the
/// "bearer" authentication scheme.
/// </summary>
public class BearerAuthenticationProvider(Func<Task<string>> bearerToken)
{
private readonly Func<Task<string>> _bearerToken = bearerToken;
/// <summary>
/// Applies the token to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var token = await this._bearerToken().ConfigureAwait(false);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
}
}
/// <summary>
/// Uses the Microsoft Authentication Library (MSAL) to authenticate HTTP requests.
/// </summary>
public class InteractiveMsalAuthenticationProvider(string clientId, string tenantId, string[] scopes, Uri redirectUri) : BearerAuthenticationProvider(() => GetTokenAsync(clientId, tenantId, scopes, redirectUri))
{
/// <summary>
/// Gets an access token using the Microsoft Authentication Library (MSAL).
/// </summary>
/// <param name="clientId">Client ID of the caller.</param>
/// <param name="tenantId">Tenant ID of the target resource.</param>
/// <param name="scopes">Requested scopes.</param>
/// <param name="redirectUri">Redirect URI.</param>
/// <returns>Access token.</returns>
private static async Task<string> GetTokenAsync(string clientId, string tenantId, string[] scopes, Uri redirectUri)
{
IPublicClientApplication app = PublicClientApplicationBuilder.Create(clientId)
.WithRedirectUri(redirectUri.ToString())
.WithTenantId(tenantId)
.Build();
IEnumerable<IAccount> accounts = await app.GetAccountsAsync().ConfigureAwait(false);
AuthenticationResult result;
try
{
result = await app.AcquireTokenSilent(scopes, accounts.FirstOrDefault())
.ExecuteAsync().ConfigureAwait(false);
}
catch (MsalUiRequiredException)
{
// A MsalUiRequiredException happened on AcquireTokenSilent.
// This indicates you need to call AcquireTokenInteractive to acquire a token
result = await app.AcquireTokenInteractive(scopes)
.ExecuteAsync().ConfigureAwait(false);
}
return result.AccessToken;
}
}
/// <summary>
/// Retrieves authentication content (scheme and value) via the provided delegate and applies it to HTTP requests.
/// </summary>
public sealed class CustomAuthenticationProvider(Func<Task<string>> header, Func<Task<string>> value)
{
private readonly Func<Task<string>> _header = header;
private readonly Func<Task<string>> _value = value;
/// <summary>
/// Applies the header and value to the provided HTTP request message.
/// </summary>
/// <param name="request">The HTTP request message.</param>
public async Task AuthenticateRequestAsync(HttpRequestMessage request)
{
var header = await this._header().ConfigureAwait(false);
var value = await this._value().ConfigureAwait(false);
request.Headers.Add(header, value);
}
}
#endregion
}
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
public class CreatePluginFromOpenApiSpec_Klarna(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// This sample shows how to invoke an OpenApi plugin.
/// </summary>
/// <remarks>
/// You must provide the plugin name and a URI to the Open API manifest before running this sample.
/// </remarks>
[Fact(Skip = "Run it only after filling the template below")]
public async Task InvokeOpenApiPluginAsync()
{
Kernel kernel = new();
// This HTTP client is optional. SK will fallback to a default internal one if omitted.
using HttpClient httpClient = new();
// Import an Open AI plugin via URI
var plugin = await kernel.ImportPluginFromOpenApiAsync("<plugin name>", new Uri("<OpenApi-plugin>"), new OpenApiFunctionExecutionParameters(httpClient));
// Add arguments for required parameters, arguments for optional ones can be skipped.
var arguments = new KernelArguments { ["<parameter-name>"] = "<parameter-value>" };
// Run
var functionResult = await kernel.InvokeAsync(plugin["<function-name>"], arguments);
var result = functionResult.GetValue<RestApiOperationResponse>();
Console.WriteLine($"Function execution result: {result?.Content}");
}
/// <summary>
/// This sample shows how to invoke the Klarna Get Products function as an OpenAPI plugin.
/// </summary>
[Fact]
public async Task InvokeKlarnaGetProductsAsOpenApiPluginAsync()
{
Kernel kernel = new();
var plugin = await kernel.ImportPluginFromOpenApiAsync("Klarna", new Uri("https://www.klarna.com/us/shopping/public/openai/v0/api-docs/"));
var arguments = new KernelArguments
{
["q"] = "Laptop", // Category or product that needs to be searched for.
["size"] = "3", // Number of products to return
["budget"] = "200", // Maximum price of the matching product in local currency
["countryCode"] = "US" // ISO 3166 country code with 2 characters based on the user location.
};
// Currently, only US, GB, DE, SE and DK are supported.
var functionResult = await kernel.InvokeAsync(plugin["productsUsingGET"], arguments);
var result = functionResult.GetValue<RestApiOperationResponse>();
Console.WriteLine($"Function execution result: {result?.Content}");
}
/// <summary>
/// This sample shows how to use a delegating handler when invoking an OpenAPI function.
/// </summary>
/// <remarks>
/// An instances of <see cref="OpenApiKernelFunctionContext"/> will be set in the `HttpRequestMessage.Options` (for .NET 5.0 or higher) or
/// in the `HttpRequestMessage.Properties` dictionary (for .NET Standard) with the key `KernelFunctionContextKey`.
/// The <see cref="OpenApiKernelFunctionContext"/> contains the <see cref="Kernel"/>, <see cref="KernelFunction"/> and <see cref="KernelArguments"/>.
/// </remarks>
[Fact]
public async Task UseDelegatingHandlerWhenInvokingAnOpenApiFunctionAsync()
{
using var httpHandler = new HttpClientHandler();
using var customHandler = new CustomHandler(httpHandler);
using HttpClient httpClient = new(customHandler);
Kernel kernel = new();
var plugin = await kernel.ImportPluginFromOpenApiAsync("Klarna", new Uri("https://www.klarna.com/us/shopping/public/openai/v0/api-docs/"), new OpenApiFunctionExecutionParameters(httpClient));
var arguments = new KernelArguments
{
["q"] = "Laptop", // Category or product that needs to be searched for.
["size"] = "3", // Number of products to return
["budget"] = "200", // Maximum price of the matching product in local currency
["countryCode"] = "US" // ISO 3166 country code with 2 characters based on the user location.
};
// Currently, only US, GB, DE, SE and DK are supported.
var functionResult = await kernel.InvokeAsync(plugin["productsUsingGET"], arguments);
var result = functionResult.GetValue<RestApiOperationResponse>();
Console.WriteLine($"Function execution result: {result?.Content}");
}
/// <summary>
/// Custom delegating handler to modify the <see cref="HttpRequestMessage"/> before sending it.
/// </summary>
private sealed class CustomHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler)
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
#if NET
request.Options.TryGetValue(OpenApiKernelFunctionContext.KernelFunctionContextKey, out var functionContext);
#else
request.Properties.TryGetValue(OpenApiKernelFunctionContext.KernelFunctionContextKey, out var functionContext);
#endif
// Function context is only set when the Plugin is invoked via the Kernel
if (functionContext is not null)
{
// Modify the HttpRequestMessage
request.Headers.Add("Kernel-Function-Name", functionContext?.Function?.Name);
}
// Call the next handler in the pipeline
return await base.SendAsync(request, cancellationToken);
}
}
}
@@ -0,0 +1,83 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// Sample shows how to create a <see cref="KernelPlugin"/> from an Open API manifest.
/// </summary>
public sealed class CreatePluginFromOpenApiSpec_RepairService(ITestOutputHelper output) : BaseTest(output)
{
[Fact]
public async Task ShowCreatingRepairServicePluginAsync()
{
// Arrange
var kernel = new Kernel();
using var stream = System.IO.File.OpenRead("Resources/Plugins/RepairServicePlugin/repair-service.json");
using HttpClient httpClient = new();
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync(
"RepairService",
stream,
new OpenApiFunctionExecutionParameters(httpClient) { IgnoreNonCompliantErrors = true, EnableDynamicPayload = false });
kernel.Plugins.Add(plugin);
var arguments = new KernelArguments
{
["payload"] = """{ "title": "Engine oil change", "description": "Need to drain the old engine oil and replace it with fresh oil.", "assignedTo": "", "date": "", "image": "" }"""
};
// Create Repair
var result = await plugin["createRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
// List All Repairs
result = await plugin["listRepairs"].InvokeAsync(kernel);
var repairs = JsonSerializer.Deserialize<Repair[]>(result.ToString());
Assert.True(repairs?.Length > 0);
var id = repairs[repairs.Length - 1].Id;
// Update Repair
arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id}, \"assignedTo\": \"Karin Blair\", \"date\": \"2024-04-16\", \"image\": \"https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg\" }}"
};
result = await plugin["updateRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
// Delete Repair
arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id} }}"
};
result = await plugin["deleteRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
}
private sealed class Repair
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("assignedTo")]
public string? AssignedTo { get; set; }
[JsonPropertyName("date")]
public string? Date { get; set; }
[JsonPropertyName("image")]
public string? Image { get; set; }
}
}
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
namespace Plugins;
/// <summary>
/// This sample shows how to create templated plugins from file directories.
/// </summary>
public class CreatePromptPluginFromDirectory(ITestOutputHelper output) : BaseTest(output)
{
[Fact]
public async Task ImportAndUsePromptPluginFromDirectoryWithOpenAI()
{
// Get the current directory of the application
var pluginDirectory = Path.Combine(AppContext.BaseDirectory, "Plugins", "FunPlugin");
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
CreateFileBasedPluginTemplate(pluginDirectory);
var funPlugin = kernel.ImportPluginFromPromptDirectoryYaml(pluginDirectory, "FunPlugin");
// Invoke the plugin with a prompt
var result = await kernel.InvokeAsync(funPlugin["Joke"], new()
{
["input"] = "Why did the chicken cross the road?",
["style"] = "dad joke"
});
Console.WriteLine(result);
}
/// <summary>
/// After running this method, a new importable plugin directory structure will be created at the application root.
/// <code>
/// ./Plugins/FunPlugin/
/// joke.yml
/// </code>
/// Within the <c>FunPlugin</c> directory, any yml file will be imported as a distinct prompt function for the <see cref="KernelPlugin"/>.
/// </summary>
private static void CreateFileBasedPluginTemplate(string pluginRootDirectory)
{
// Create the sub-directory for the plugin function "Joke"
var pluginRelativeDirectory = Path.Combine(pluginRootDirectory, "Joke");
const string PluginYmlFileContent =
"""
name: Joke
template: |
WRITE EXACTLY ONE JOKE or HUMOROUS STORY ABOUT THE TOPIC BELOW
JOKE MUST BE:
- G RATED
- WORKPLACE/FAMILY SAFE
NO SEXISM, RACISM OR OTHER BIAS/BIGOTRY
BE CREATIVE AND FUNNY. I WANT TO LAUGH.
Incorporate the style suggestion, if provided: {{$style}}
+++++
{{$input}}
+++++
template_format: semantic-kernel
description: A function that generates a story about a topic.
input_variables:
- name: input
description: Joke subject.
is_required: true
- name: style
description: Give a hint about the desired joke style.
is_required: true
output_variable:
description: The generated funny joke.
execution_settings:
default:
temperature: 0.9
max_tokens: 1000
top_p: 0.0
presence_penalty: 0.0
frequency_penalty: 0.0
""";
// Create the directory structure
if (!Directory.Exists(pluginRootDirectory))
{
Directory.CreateDirectory(pluginRootDirectory);
}
// Create the config.json file if not exists
var ymlFilePath = Path.Combine(pluginRootDirectory, "joke.yml");
File.WriteAllText(ymlFilePath, PluginYmlFileContent);
}
}
@@ -0,0 +1,108 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.AI.CrewAI;
namespace Plugins;
/// <summary>
/// This example shows how to interact with an existing CrewAI Enterprise Crew directly or as a plugin.
/// These examples require a valid CrewAI Enterprise deployment with an endpoint, auth token, and known inputs.
/// </summary>
public class CrewAI_Plugin(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// Shows how to kickoff an existing CrewAI Enterprise Crew and wait for it to complete.
/// </summary>
[Fact]
public async Task UsingCrewAIEnterpriseAsync()
{
string crewAIEndpoint = TestConfiguration.CrewAI.Endpoint;
string crewAIAuthToken = TestConfiguration.CrewAI.AuthToken;
var crew = new CrewAIEnterprise(
endpoint: new Uri(crewAIEndpoint),
authTokenProvider: async () => crewAIAuthToken);
// The required inputs for the Crew must be known in advance. This example is modeled after the
// Enterprise Content Marketing Crew Template and requires the following inputs:
var inputs = new
{
company = "CrewAI",
topic = "Agentic products for consumers",
};
// Invoke directly with our inputs
var kickoffId = await crew.KickoffAsync(inputs);
Console.WriteLine($"CrewAI Enterprise Crew kicked off with ID: {kickoffId}");
// Wait for completion
var result = await crew.WaitForCrewCompletionAsync(kickoffId);
Console.WriteLine("CrewAI Enterprise Crew completed with the following result:");
Console.WriteLine(result);
}
/// <summary>
/// Shows how to kickoff an existing CrewAI Enterprise Crew as a plugin.
/// </summary>
[Fact]
public async Task UsingCrewAIEnterpriseAsPluginAsync()
{
string crewAIEndpoint = TestConfiguration.CrewAI.Endpoint;
string crewAIAuthToken = TestConfiguration.CrewAI.AuthToken;
string openAIModelId = TestConfiguration.OpenAI.ChatModelId;
string openAIApiKey = TestConfiguration.OpenAI.ApiKey;
if (openAIModelId is null || openAIApiKey is null)
{
Console.WriteLine("OpenAI credentials not found. Skipping example.");
return;
}
// Setup the Kernel and AI Services
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: openAIModelId,
apiKey: openAIApiKey)
.Build();
var crew = new CrewAIEnterprise(
endpoint: new Uri(crewAIEndpoint),
authTokenProvider: async () => crewAIAuthToken);
// The required inputs for the Crew must be known in advance. This example is modeled after the
// Enterprise Content Marketing Crew Template and requires string inputs for the company and topic.
// We need to describe the type and purpose of each input to allow the LLM to invoke the crew as expected.
var crewPluginDefinitions = new[]
{
new CrewAIInputMetadata(Name: "company", Description: "The name of the company that should be researched", Type: typeof(string)),
new CrewAIInputMetadata(Name: "topic", Description: "The topic that should be researched", Type: typeof(string)),
};
// Create the CrewAI Plugin. This builds a plugin that can be added to the Kernel and invoked like any other plugin.
// The plugin will contain the following functions:
// - Kickoff: Starts the Crew with the specified inputs and returns the Id of the scheduled kickoff.
// - KickoffAndWait: Starts the Crew with the specified inputs and waits for the Crew to complete before returning the result.
// - WaitForCrewCompletion: Waits for the specified Crew kickoff to complete and returns the result.
// - GetCrewKickoffStatus: Gets the status of the specified Crew kickoff.
var crewPlugin = crew.CreateKernelPlugin(
name: "EnterpriseContentMarketingCrew",
description: "Conducts thorough research on the specified company and topic to identify emerging trends, analyze competitor strategies, and gather data-driven insights.",
inputMetadata: crewPluginDefinitions);
// Add the plugin to the Kernel
kernel.Plugins.Add(crewPlugin);
// Invoke the CrewAI Plugin directly as shown below, or use automaic function calling with an LLM.
var kickoffAndWaitFunction = crewPlugin["KickoffAndWait"];
var result = await kernel.InvokeAsync(
function: kickoffAndWaitFunction,
arguments: new()
{
["company"] = "CrewAI",
["topic"] = "Consumer AI Products"
});
Console.WriteLine(result);
}
}
@@ -0,0 +1,80 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.AI;
using Microsoft.SemanticKernel;
namespace Plugins;
/// <summary>
/// This example shows how to create a mutable <see cref="KernelPlugin"/>.
/// </summary>
public class CustomMutablePlugin(ITestOutputHelper output) : BaseTest(output)
{
[Fact]
public async Task RunAsync()
{
var plugin = new MutableKernelPlugin("Plugin");
plugin.AddFunction(KernelFunctionFactory.CreateFromMethod(() => "Plugin.Function", "Function"));
var kernel = new Kernel();
kernel.Plugins.Add(plugin);
var result = await kernel.InvokeAsync(kernel.Plugins["Plugin"]["Function"]);
Console.WriteLine($"Result: {result}");
}
/// <summary>
/// Provides an <see cref="KernelPlugin"/> implementation around a collection of functions.
/// </summary>
public class MutableKernelPlugin : KernelPlugin
{
/// <summary>The collection of functions associated with this plugin.</summary>
private readonly Dictionary<string, KernelFunction> _functions;
/// <summary>Initializes the new plugin from the provided name, description, and function collection.</summary>
/// <param name="name">The name for the plugin.</param>
/// <param name="description">A description of the plugin.</param>
/// <param name="functions">The initial functions to be available as part of the plugin.</param>
/// <exception cref="ArgumentNullException"><paramref name="functions"/> contains a null function.</exception>
/// <exception cref="ArgumentException"><paramref name="functions"/> contains two functions with the same name.</exception>
public MutableKernelPlugin(string name, string? description = null, IEnumerable<KernelFunction>? functions = null) : base(name, description)
{
this._functions = new Dictionary<string, KernelFunction>(StringComparer.OrdinalIgnoreCase);
if (functions is not null)
{
foreach (KernelFunction f in functions)
{
ArgumentNullException.ThrowIfNull(f);
var cloned = f.Clone(name);
this._functions.Add(cloned.Name, cloned);
}
}
}
/// <inheritdoc/>
public override int FunctionCount => this._functions.Count;
/// <inheritdoc/>
public override bool TryGetFunction(string name, [NotNullWhen(true)] out KernelFunction? function) =>
this._functions.TryGetValue(name, out function);
/// <summary>Adds a function to the plugin.</summary>
/// <param name="function">The function to add.</param>
/// <exception cref="ArgumentNullException"><paramref name="function"/> is null.</exception>
/// <exception cref="ArgumentNullException"><paramref name="function"/>'s <see cref="AITool.Name"/> is null.</exception>
/// <exception cref="ArgumentException">A function with the same <see cref="AITool.Name"/> already exists in this plugin.</exception>
public void AddFunction(KernelFunction function)
{
ArgumentNullException.ThrowIfNull(function);
var cloned = function.Clone(this.Name);
this._functions.Add(cloned.Name, cloned);
}
/// <inheritdoc/>
public override IEnumerator<KernelFunction> GetEnumerator() => this._functions.Values.GetEnumerator();
}
}
@@ -0,0 +1,173 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Plugins.Core;
namespace Plugins;
public class DescribeAllPluginsAndFunctions(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// Print a list of all the functions imported into the kernel, including function descriptions,
/// list of parameters, parameters descriptions, etc.
/// See the end of the file for a sample of what the output looks like.
/// </summary>
[Fact]
public Task RunAsync()
{
var kernel = Kernel.CreateBuilder()
.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey)
.Build();
// Import a native plugin
kernel.ImportPluginFromType<StaticTextPlugin>();
// Import another native plugin
kernel.ImportPluginFromType<TextPlugin>("AnotherTextPlugin");
// Import a semantic plugin
string folder = RepoFiles.SamplePluginsPath();
kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin"));
// Define a prompt function inline, without naming
var sFun1 = kernel.CreateFunctionFromPrompt("tell a joke about {{$input}}", new OpenAIPromptExecutionSettings() { MaxTokens = 150 });
// Define a prompt function inline, with plugin name
var sFun2 = kernel.CreateFunctionFromPrompt(
"write a novel about {{$input}} in {{$language}} language",
new OpenAIPromptExecutionSettings() { MaxTokens = 150 },
functionName: "Novel",
description: "Write a bedtime story");
var functions = kernel.Plugins.GetFunctionsMetadata();
Console.WriteLine("**********************************************");
Console.WriteLine("****** Registered plugins and functions ******");
Console.WriteLine("**********************************************");
Console.WriteLine();
foreach (KernelFunctionMetadata func in functions)
{
PrintFunction(func);
}
return Task.CompletedTask;
}
private void PrintFunction(KernelFunctionMetadata func)
{
Console.WriteLine($"Plugin: {func.PluginName}");
Console.WriteLine($" {func.Name}: {func.Description}");
if (func.Parameters.Count > 0)
{
Console.WriteLine(" Params:");
foreach (var p in func.Parameters)
{
Console.WriteLine($" - {p.Name}: {p.Description}");
Console.WriteLine($" default: '{p.DefaultValue}'");
}
}
Console.WriteLine();
}
}
/** Sample output:
**********************************************
****** Registered plugins and functions ******
**********************************************
Plugin: StaticTextPlugin
Uppercase: Change all string chars to uppercase
Params:
- input: Text to uppercase
default: ''
Plugin: StaticTextPlugin
AppendDay: Append the day variable
Params:
- input: Text to append to
default: ''
- day: Value of the day to append
default: ''
Plugin: AnotherTextPlugin
Trim: Trim whitespace from the start and end of a string.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
TrimStart: Trim whitespace from the start of a string.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
TrimEnd: Trim whitespace from the end of a string.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
Uppercase: Convert a string to uppercase.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
Lowercase: Convert a string to lowercase.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
Length: Get the length of a string.
Params:
- input:
default: ''
Plugin: AnotherTextPlugin
Concat: Concat two strings into one.
Params:
- input: First input to concatenate with
default: ''
- input2: Second input to concatenate with
default: ''
Plugin: AnotherTextPlugin
Echo: Echo the input string. Useful for capturing plan input for use in multiple functions.
Params:
- text: Input string to echo.
default: ''
Plugin: SummarizePlugin
MakeAbstractReadable: Given a scientific white paper abstract, rewrite it to make it more readable
Params:
- input:
default: ''
Plugin: SummarizePlugin
Notegen: Automatically generate compact notes for any text or text document.
Params:
- input:
default: ''
Plugin: SummarizePlugin
Summarize: Summarize given text or any text document
Params:
- input: Text to summarize
default: ''
Plugin: SummarizePlugin
Topics: Analyze given text or document and extract key topics worth remembering
Params:
- input:
default: ''
*/
@@ -0,0 +1,130 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using xRetry;
namespace Plugins;
public class GroundednessChecks(ITestOutputHelper output) : BaseTest(output)
{
[RetryFact(typeof(HttpOperationException))]
public async Task GroundednessCheckingAsync()
{
Console.WriteLine("\n======== Groundedness Checks ========");
var kernel = Kernel.CreateBuilder()
.AddAzureOpenAIChatCompletion(
deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName,
endpoint: TestConfiguration.AzureOpenAI.Endpoint,
apiKey: TestConfiguration.AzureOpenAI.ApiKey,
modelId: TestConfiguration.AzureOpenAI.ChatModelId)
.Build();
string folder = RepoFiles.SamplePluginsPath();
var summarizePlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "SummarizePlugin"));
var groundingPlugin = kernel.ImportPluginFromPromptDirectory(Path.Combine(folder, "GroundingPlugin"));
var create_summary = summarizePlugin["Summarize"];
var entityExtraction = groundingPlugin["ExtractEntities"];
var reference_check = groundingPlugin["ReferenceCheckEntities"];
var entity_excision = groundingPlugin["ExciseEntities"];
var summaryText = @"
My father, a respected resident of Milan, was a close friend of a merchant named Beaufort who, after a series of
misfortunes, moved to Zurich in poverty. My father was upset by his friend's troubles and sought him out,
finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and
his daughter, Mary. Mary procured work to eek out a living, but after ten months her father died, leaving
her a beggar. My father came to her aid and two years later they married.
";
KernelArguments variables = new()
{
["input"] = summaryText,
["topic"] = "people and places",
["example_entities"] = "John, Jane, mother, brother, Paris, Rome"
};
var extractionResult = (await kernel.InvokeAsync(entityExtraction, variables)).ToString();
Console.WriteLine("======== Extract Entities ========");
Console.WriteLine(extractionResult);
variables["input"] = extractionResult;
variables["reference_context"] = GroundingText;
var groundingResult = (await kernel.InvokeAsync(reference_check, variables)).ToString();
Console.WriteLine("\n======== Reference Check ========");
Console.WriteLine(groundingResult);
variables["input"] = summaryText;
variables["ungrounded_entities"] = groundingResult;
var excisionResult = await kernel.InvokeAsync(entity_excision, variables);
Console.WriteLine("\n======== Excise Entities ========");
Console.WriteLine(excisionResult.GetValue<string>());
}
private const string GroundingText = """
"I am by birth a Genevese, and my family is one of the most distinguished of that republic.
My ancestors had been for many years counsellors and syndics, and my father had filled several public situations
with honour and reputation.He was respected by all who knew him for his integrity and indefatigable attention
to public business.He passed his younger days perpetually occupied by the affairs of his country; a variety
of circumstances had prevented his marrying early, nor was it until the decline of life that he became a husband
and the father of a family.
As the circumstances of his marriage illustrate his character, I cannot refrain from relating them.One of his
most intimate friends was a merchant who, from a flourishing state, fell, through numerous mischances, into poverty.
This man, whose name was Beaufort, was of a proud and unbending disposition and could not bear to live in poverty
and oblivion in the same country where he had formerly been distinguished for his rank and magnificence. Having
paid his debts, therefore, in the most honourable manner, he retreated with his daughter to the town of Lucerne,
where he lived unknown and in wretchedness.My father loved Beaufort with the truest friendship and was deeply
grieved by his retreat in these unfortunate circumstances.He bitterly deplored the false pride which led his friend
to a conduct so little worthy of the affection that united them.He lost no time in endeavouring to seek him out,
with the hope of persuading him to begin the world again through his credit and assistance.
Beaufort had taken effectual measures to conceal himself, and it was ten months before my father discovered his
abode.Overjoyed at this discovery, he hastened to the house, which was situated in a mean street near the Reuss.
But when he entered, misery and despair alone welcomed him. Beaufort had saved but a very small sum of money from
the wreck of his fortunes, but it was sufficient to provide him with sustenance for some months, and in the meantime
he hoped to procure some respectable employment in a merchant's house. The interval was, consequently, spent in
inaction; his grief only became more deep and rankling when he had leisure for reflection, and at length it took
so fast hold of his mind that at the end of three months he lay on a bed of sickness, incapable of any exertion.
His daughter attended him with the greatest tenderness, but she saw with despair that their little fund was
rapidly decreasing and that there was no other prospect of support.But Caroline Beaufort possessed a mind of an
uncommon mould, and her courage rose to support her in her adversity. She procured plain work; she plaited straw
and by various means contrived to earn a pittance scarcely sufficient to support life.
Several months passed in this manner.Her father grew worse; her time was more entirely occupied in attending him;
her means of subsistence decreased; and in the tenth month her father died in her arms, leaving her an orphan and
a beggar.This last blow overcame her, and she knelt by Beaufort's coffin weeping bitterly, when my father entered
the chamber. He came like a protecting spirit to the poor girl, who committed herself to his care; and after the
interment of his friend he conducted her to Geneva and placed her under the protection of a relation.Two years
after this event Caroline became his wife."
""";
}
/* Example Output:
======== Groundedness Checks ========
======== Extract Entities ========
<entities>
- Milan
- Beaufort
- Zurich
- Mary
</entities>
======== Reference Check ========
<ungrounded_entities>
- Milan
- Zurich
- Mary
</ungrounded_entities>
======== Excise Entities ========
My father, a respected resident of a city, was a close friend of a merchant named Beaufort who, after a series of
misfortunes, moved to another city in poverty. My father was upset by his friend's troubles and sought him out,
finding him in a mean street. Beaufort had saved a small sum of money, but it was not enough to support him and
his daughter. The daughter procured work to eek out a living, but after ten months her father died, leaving
her a beggar. My father came to her aid and two years later they married.
*/
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.Grpc;
namespace Plugins;
// This example shows how to use gRPC plugins.
public class ImportPluginFromGrpc(ITestOutputHelper output) : BaseTest(output)
{
[Fact(Skip = "Setup crendentials")]
public async Task RunAsync()
{
Kernel kernel = new();
// Import a gRPC plugin using one of the following Kernel extension methods
// kernel.ImportGrpcPlugin
// kernel.ImportGrpcPluginFromDirectory
var plugin = kernel.ImportPluginFromGrpcFile("<path-to-.proto-file>", "<plugin-name>");
// Add arguments for required parameters, arguments for optional ones can be skipped.
var arguments = new KernelArguments
{
["address"] = "<gRPC-server-address>",
["payload"] = "<gRPC-request-message-as-json>"
};
// Run
var result = await kernel.InvokeAsync(plugin["<operation-name>"], arguments);
Console.WriteLine($"Plugin response: {result.GetValue<string>()}");
}
}
@@ -0,0 +1,61 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
namespace Plugins;
/// <summary>
/// This example shows how to use Microsoft Graph Plugin
/// These examples require a valid Microsoft account and delegated/application access for the Microsoft Graph used resources.
/// </summary>
public class MsGraph_CalendarPlugin(ITestOutputHelper output) : BaseTest(output)
{
private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
/// <summary>Shows how to use Microsoft Graph Calendar Plugin with AI Models.</summary>
[Fact]
public async Task UsingWithAIModel()
{
// Setup the Kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.Build();
using var graphClient = GetGraphClient();
var calendarConnector = new OutlookCalendarConnector(graphClient);
// Add the plugin to the Kernel
var graphPlugin = kernel.Plugins.AddFromObject(new CalendarPlugin(calendarConnector, jsonSerializerOptions: s_options));
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
string Prompt = $"""
1. Show me the next 10 calendar events I have
2. If I don't have any event named "Semantic Kernel", please create a new event named "Semantic Kernel"
starting at {DateTimeOffset.Now.AddHours(1)} with 1 hour of duration.
""";
// Invoke the OneDrive plugin multiple times
var result = await kernel.InvokePromptAsync(Prompt, new(settings));
Console.WriteLine(result);
}
private static GraphServiceClient GetGraphClient()
{
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions()
{
ClientId = TestConfiguration.MSGraph.ClientId,
TenantId = TestConfiguration.MSGraph.TenantId,
RedirectUri = TestConfiguration.MSGraph.RedirectUri,
});
return new GraphServiceClient(credential);
}
}
@@ -0,0 +1,58 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
namespace Plugins;
/// <summary>
/// This example shows how to use Microsoft Graph Plugin
/// These examples require a valid Microsoft account and delegated/application access for the used resources.
/// </summary>
public class MsGraph_EmailPlugin(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>Shows how to use Microsoft Graph Email Plugin with AI Models.</summary>
[Fact]
public async Task EmailPlugin_SendEmailToMyself()
{
// Setup the Kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.Build();
using var graphClient = GetGraphClient();
var emailConnector = new OutlookMailConnector(graphClient);
// Add the plugin to the Kernel
var graphPlugin = kernel.Plugins.AddFromObject(new Microsoft.SemanticKernel.Plugins.MsGraph.EmailPlugin(emailConnector));
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
const string Prompt = """
Using the tools available, please do the following:
1. Get my email address
2. Send an email to myself with the subject "FYI" and content "This is a very important email"
3. List 10 of my email messages
""";
// Invoke the Graph plugin with a prompt
var result = await kernel.InvokePromptAsync(Prompt, new(settings));
Console.WriteLine(result);
}
private static GraphServiceClient GetGraphClient()
{
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions()
{
ClientId = TestConfiguration.MSGraph.ClientId,
TenantId = TestConfiguration.MSGraph.TenantId,
RedirectUri = TestConfiguration.MSGraph.RedirectUri,
});
return new GraphServiceClient(credential);
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
namespace Plugins;
/// <summary>
/// This example shows how to use Microsoft Graph Plugin
/// These examples require a valid Microsoft account and delegated/application access for the used resources.
/// </summary>
public class MsGraph_OneDrivePlugin(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>Shows how to use Microsoft Graph OneDrive Plugin with AI Models.</summary>
[Fact]
public async Task UsingWithAIModel()
{
// Setup the Kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.Build();
using var graphClient = GetGraphClient();
var connector = new OneDriveConnector(graphClient);
// Add the plugin to the Kernel
var graphPlugin = kernel.Plugins.AddFromObject(new CloudDrivePlugin(connector));
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
const string Prompt = """
I need you to do the following things with the tools available:
1. Update the current file: "Resources/travelinfo.txt" to my OneDrive into the "Test" folder.
2. Generate a OneDrive Link for sharing the file
3. Summarize for me the contents of the uploaded file
4. Show me the generated shared link.
""";
// Invoke the OneDrive plugin multiple times
var result = await kernel.InvokePromptAsync(Prompt, new(settings));
Console.WriteLine($"Assistant: {result}");
}
private static GraphServiceClient GetGraphClient()
{
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions()
{
ClientId = TestConfiguration.MSGraph.ClientId,
TenantId = TestConfiguration.MSGraph.TenantId,
RedirectUri = TestConfiguration.MSGraph.RedirectUri,
});
return new GraphServiceClient(credential);
}
}
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
namespace Plugins;
/// <summary>
/// This example shows how to use Microsoft Graph Plugin
/// These examples require a valid Microsoft account and delegated/application access for the used resources.
/// </summary>
public class MsGraph_OrganizationHierarchyPlugin(ITestOutputHelper output) : BaseTest(output)
{
private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
/// <summary>Shows how to use Microsoft Graph Organization Hierarchy Plugin with AI Models.</summary>
[Fact]
public async Task UsingWithAIModel()
{
// Setup the Kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.Build();
using var graphClient = GetGraphClient();
var connector = new OrganizationHierarchyConnector(graphClient);
// Add the plugin to the Kernel
var graphPlugin = kernel.Plugins.AddFromObject(new OrganizationHierarchyPlugin(connector, s_options));
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
const string Prompt = "I need you to show my manager details as well as my direct reports using the tools available:";
// Invoke the OneDrive plugin multiple times
var result = await kernel.InvokePromptAsync(Prompt, new(settings));
Console.WriteLine($"Assistant: {result}");
}
private static GraphServiceClient GetGraphClient()
{
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions()
{
ClientId = TestConfiguration.MSGraph.ClientId,
TenantId = TestConfiguration.MSGraph.TenantId,
RedirectUri = TestConfiguration.MSGraph.RedirectUri,
});
return new GraphServiceClient(credential);
}
}
@@ -0,0 +1,59 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Azure.Identity;
using Microsoft.Graph;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.MsGraph;
using Microsoft.SemanticKernel.Plugins.MsGraph.Connectors;
namespace Plugins;
/// <summary>
/// This example shows how to use Microsoft Graph Plugin
/// These examples require a valid Microsoft account and delegated/application access for the used resources.
/// </summary>
public class MsGraph_TaskListPlugin(ITestOutputHelper output) : BaseTest(output)
{
private static readonly JsonSerializerOptions s_options = new() { WriteIndented = true };
/// <summary>Shows how to use Microsoft Graph To-Do Tasks Plugin with AI Models.</summary>
[Fact]
public async Task UsingWithAIModel()
{
// Setup the Kernel
Kernel kernel = Kernel.CreateBuilder()
.AddOpenAIChatClient(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey)
.Build();
using var graphClient = GetGraphClient();
var connector = new MicrosoftToDoConnector(graphClient);
// Add the plugin to the Kernel
var graphPlugin = kernel.Plugins.AddFromObject(new TaskListPlugin(connector, jsonSerializerOptions: s_options));
var settings = new PromptExecutionSettings { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
const string Prompt = """
1. Show me all the tasks I have
3. If I don't have a task named "Semantic Kernel", please create one
""";
// Invoke the OneDrive plugin multiple times
var result = await kernel.InvokePromptAsync(Prompt, new(settings));
Console.WriteLine($"Assistant: {result}");
}
private static GraphServiceClient GetGraphClient()
{
var credential = new InteractiveBrowserCredential(new InteractiveBrowserCredentialOptions()
{
ClientId = TestConfiguration.MSGraph.ClientId,
TenantId = TestConfiguration.MSGraph.TenantId,
RedirectUri = TestConfiguration.MSGraph.RedirectUri,
});
return new GraphServiceClient(credential);
}
}
@@ -0,0 +1,101 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// Sample shows how to register a custom HTTP content reader for an Open API plugin.
/// </summary>
public sealed class CustomHttpContentReaderForOpenApiPlugin(ITestOutputHelper output) : BaseTest(output)
{
[Fact]
public async Task ShowReadingJsonAsStreamAsync()
{
var kernel = new Kernel();
// Register the custom HTTP content reader
var executionParameters = new OpenApiFunctionExecutionParameters() { HttpResponseContentReader = ReadHttpResponseContentAsync };
// Create OpenAPI plugin
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("RepairService", "Resources/Plugins/RepairServicePlugin/repair-service.json", executionParameters);
// Create a repair so it can be read as a stream in the following step
var arguments = new KernelArguments
{
["title"] = "The Case of the Broken Gizmo",
["description"] = "It's broken. Send help!",
["assignedTo"] = "Tech Magician"
};
var createResult = await plugin["createRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(createResult.ToString());
// List relevant repairs
arguments = new KernelArguments
{
["assignedTo"] = "Tech Magician"
};
var listResult = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
using var reader = new StreamReader((Stream)listResult.GetValue<RestApiOperationResponse>()!.Content!);
var content = await reader.ReadToEndAsync();
var repairs = JsonSerializer.Deserialize<Repair[]>(content);
Console.WriteLine(content);
// Delete the repair
arguments = new KernelArguments
{
["id"] = repairs!.Where(r => r.AssignedTo == "Tech Magician").First().Id.ToString()
};
var deleteResult = await plugin["deleteRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(deleteResult.ToString());
}
/// <summary>
/// A custom HTTP content reader to change the default behavior of reading HTTP content.
/// </summary>
/// <param name="context">The HTTP response content reader context.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The HTTP response content.</returns>
private static async Task<object?> ReadHttpResponseContentAsync(HttpResponseContentReaderContext context, CancellationToken cancellationToken)
{
// Read JSON content as a stream rather than as a string, which is the default behavior
if (context.Response.Content.Headers.ContentType?.MediaType == "application/json")
{
return await context.Response.Content.ReadAsStreamAsync(cancellationToken);
}
// HTTP request and response properties can be used to decide how to read the content.
// The 'if' operator below is not relevant to the current example and is just for demonstration purposes.
if (context.Request.Headers.Contains("x-stream"))
{
return await context.Response.Content.ReadAsStreamAsync(cancellationToken);
}
// Return null to indicate that any other HTTP content not handled above should be read by the default reader.
return null;
}
private sealed class Repair
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("assignedTo")]
public string? AssignedTo { get; set; }
[JsonPropertyName("date")]
public string? Date { get; set; }
[JsonPropertyName("image")]
public string? Image { get; set; }
}
}
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// These samples show different ways OpenAPI document can be transformed to change its various aspects before creating a plugin out of it.
/// The transformations can be useful if the original OpenAPI document can't be consumed as is.
/// </summary>
public sealed class OpenApiPlugin_Customization : BaseTest
{
private readonly Kernel _kernel;
private readonly ITestOutputHelper _output;
private readonly HttpClient _httpClient;
public OpenApiPlugin_Customization(ITestOutputHelper output) : base(output)
{
IKernelBuilder builder = Kernel.CreateBuilder();
this._kernel = builder.Build();
this._output = output;
void RequestDataHandler(string requestData)
{
this._output.WriteLine("Request payload");
this._output.WriteLine(requestData);
}
// Create HTTP client with a stub handler to log the request data
this._httpClient = new(new StubHttpHandler(RequestDataHandler));
}
/// <summary>
/// This sample demonstrates how to assign argument names to parameters and variables that have the same name.
/// For example, in this sample, there are multiple parameters named 'id' in the 'getProductFromCart' operation.
/// * Region of the API in the server variable.
/// * User ID in the path.
/// * Subscription ID in the query string.
/// * Session ID in the header.
/// </summary>
[Fact]
public async Task HandleOpenApiDocumentHavingTwoParametersWithSameNameButRelatedToDifferentEntitiesAsync()
{
OpenApiDocumentParser parser = new();
using StreamReader sr = File.OpenText("Resources/Plugins/ProductsPlugin/openapi.json");
// Register the custom HTTP client with the stub handler
OpenApiFunctionExecutionParameters executionParameters = new() { HttpClient = this._httpClient };
// Parse the OpenAPI document
RestApiSpecification specification = await parser.ParseAsync(sr.BaseStream);
// Get the 'getProductFromCart' operation
RestApiOperation getProductFromCartOperation = specification.Operations.Single(o => o.Id == "getProductFromCart");
// Set the 'region' argument name to the 'id' server variable that represents the region of the API
RestApiServerVariable idServerVariable = getProductFromCartOperation.Servers[0].Variables["id"];
idServerVariable.ArgumentName = "region";
// Set the 'userId' argument name to the 'id' path parameter that represents the user ID
RestApiParameter idPathParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Path && p.Name == "id");
idPathParameter.ArgumentName = "userId";
// Set the 'subscriptionId' argument name to the 'id' query string parameter that represents the subscription ID
RestApiParameter idQueryStringParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Query && p.Name == "id");
idQueryStringParameter.ArgumentName = "subscriptionId";
// Set the 'sessionId' argument name to the 'id' header parameter that represents the session ID
RestApiParameter sessionIdHeaderParameter = getProductFromCartOperation.Parameters.Single(p => p.Location == RestApiParameterLocation.Header && p.Name == "id");
sessionIdHeaderParameter.ArgumentName = "sessionId";
// Import the transformed OpenAPI plugin specification
KernelPlugin plugin = this._kernel.ImportPluginFromOpenApi("Products_Plugin", specification, new OpenApiFunctionExecutionParameters(this._httpClient));
// Create arguments for the 'addProductToCart' operation using the new argument names defined earlier.
// Internally these will be mapped to the correct entity when invoking the Open API endpoint.
KernelArguments arguments = new()
{
["region"] = "en",
["subscriptionId"] = "subscription-12345",
["userId"] = "user-12345",
["sessionId"] = "session-12345",
};
// Invoke the 'addProductToCart' function
await this._kernel.InvokeAsync(plugin["getProductFromCart"], arguments);
// The REST API request details
// {
// "RequestUri": "https://api.example.com:443/eu/users/user-12345/cart?id=subscription-12345",
// "Method": "Get",
// "Headers": {
// "id": ["session-12345"]
// }
// }
}
private sealed class StubHttpHandler : DelegatingHandler
{
private readonly Action<string> _requestHandler;
private readonly JsonSerializerOptions _options;
public StubHttpHandler(Action<string> requestHandler) : base()
{
this._requestHandler = requestHandler;
this._options = new JsonSerializerOptions { WriteIndented = true };
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var requestData = new Dictionary<string, object>
{
{ "RequestUri", request.RequestUri! },
{ "Method", request.Method },
{ "Headers", request.Headers.ToDictionary(h => h.Key, h => h.Value) },
};
this._requestHandler(JsonSerializer.Serialize(requestData, this._options));
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("Success", System.Text.Encoding.UTF8, "application/json")
};
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._httpClient.Dispose();
}
}
@@ -0,0 +1,188 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// These samples show different ways OpenAPI operations can be filtered out from the OpenAPI document before creating a plugin out of it.
/// </summary>
public sealed class OpenApiPlugin_Filtering : BaseTest
{
private readonly Kernel _kernel;
private readonly ITestOutputHelper _output;
public OpenApiPlugin_Filtering(ITestOutputHelper output) : base(output)
{
IKernelBuilder builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
this._kernel = builder.Build();
this._output = output;
}
/// <summary>
/// This sample demonstrates how to filter out specified operations from an OpenAPI plugin based on an exclusion list.
/// In this scenario, only the `listRepairs` operation from the RepairService OpenAPI plugin is allowed to be invoked,
/// while operations such as `createRepair`, `updateRepair`, and `deleteRepair` are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ExcludeOperationsBasedOnExclusionListAsync()
{
// The RepairService OpenAPI plugin being imported below includes the following operations: `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, to meet our business requirements, we need to restrict state-modifying operations such as creating, updating, and deleting repairs, allowing only non-state-modifying operations like listing repairs.
// To enforce this restriction, we will exclude the `createRepair`, `updateRepair`, and `deleteRepair` operations from the OpenAPI document at the plugin import time.
List<string> operationsToExclude = ["createRepair", "updateRepair", "deleteRepair"];
OpenApiFunctionExecutionParameters executionParameters = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => !operationsToExclude.Contains(context.Id!)
};
// Import the RepairService OpenAPI plugin
await this._kernel.ImportPluginFromOpenApiAsync(
pluginName: "RepairService",
filePath: "Resources/Plugins/RepairServicePlugin/repair-service.json",
executionParameters: executionParameters);
// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));
this._output.WriteLine(result);
// The AI model output:
// I can call the following functions in the current context:
// 1. `functions.RepairService - listRepairs`: Returns a list of repairs with their details and images. It takes an optional parameter `assignedTo` to filter the repairs based on the assigned individual.
// I can also utilize the `multi_tool_use.parallel` function to execute multiple tools in parallel if required.
}
/// <summary>
/// This sample demonstrates how to include specified operations from an OpenAPI plugin based on an inclusion list.
/// In this scenario, only the `createRepair` and `updateRepair` operations from the RepairService OpenAPI plugin are allowed to be invoked,
/// while operations such as `listRepairs` and `deleteRepair` are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ImportOperationsBasedOnInclusionListAsync()
{
// The RepairService OpenAPI plugin, parsed and imported below, has the following operations: `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, for our business scenario, we only want to permit the AI model to invoke the `createRepair` and `updateRepair` operations, excluding all others.
// To accomplish this, we will define an inclusion list that specifies the allowed operations and filters out the rest.
List<string> operationsToInclude = ["createRepair", "updateRepair"];
// The selection predicate is initialized to evaluate each operation in the OpenAPI document and include only those specified in the inclusion list.
OpenApiFunctionExecutionParameters executionParameters = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => operationsToInclude.Contains(context.Id!)
};
// Import the RepairService OpenAPI plugin
await this._kernel.ImportPluginFromOpenApiAsync(
pluginName: "RepairService",
filePath: "Resources/Plugins/RepairServicePlugin/repair-service.json",
executionParameters: executionParameters);
// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));
this._output.WriteLine(result);
// The AI model output:
// Here are the functions I can call for you:
// 1. **RepairService - createRepair **:
// -Adds a new repair to the list with details about the repair.
// 2. **RepairService - updateRepair **:
// -Updates an existing repair in the list with new details.
// If you need to perform any repair - related actions such as creating or updating repair records, feel free to ask!
}
/// <summary>
/// This sample demonstrates how to selectively include certain operations from an OpenAPI plugin based on HTTP method used.
/// In this scenario, only `GET` operations from the RepairService OpenAPI plugin are allowed for invocation,
/// while `POST`, `PUT`, and `DELETE` operations are excluded.
/// Note: The filtering occurs at the pre-parsing stage, which is more efficient from a resource utilization perspective.
/// </summary>
[Fact]
public async Task ImportOperationsBasedOnMethodAsync()
{
// The parsed RepairService OpenAPI plugin includes operations such as `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, for our business requirements, we only permit non-state-modifying operations like listing repairs, excluding all others.
// To achieve this, we set up the selection predicate to evaluate each operation in the OpenAPI document, including only those with the `GET` method.
// Note: The selection predicate can assess operations based on operation ID, method, path, and description.
OpenApiFunctionExecutionParameters executionParameters = new()
{
OperationSelectionPredicate = (OperationSelectionPredicateContext context) => context.Method == "Get"
};
// Import the RepairService OpenAPI plugin
await this._kernel.ImportPluginFromOpenApiAsync(
pluginName: "RepairService",
filePath: "Resources/Plugins/RepairServicePlugin/repair-service.json",
executionParameters: executionParameters);
// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));
this._output.WriteLine(result);
// The AI model output:
// I can call the following function:
// 1. `RepairService - listRepairs`: This function returns a list of repairs with their details and images.
// It can accept an optional parameter `assignedTo` to filter the repairs assigned to a specific person.
}
/// <summary>
/// This example illustrates how to selectively exclude specific operations from an OpenAPI plugin based on the HTTP method used and the presence of a payload.
/// In this context, GET operations that are defined with a payload, which contradicts the HTTP semantic of being idempotent, are not imported.
/// Note: The filtering happens at the post-parsing stage, which is less efficient in terms of resource utilization.
/// </summary>
[Fact]
public async Task FilterOperationsAtPostParsingStageAsync()
{
OpenApiDocumentParser parser = new();
using StreamReader reader = System.IO.File.OpenText("Resources/Plugins/RepairServicePlugin/repair-service.json");
// Parse the OpenAPI document.
RestApiSpecification specification = await parser.ParseAsync(stream: reader.BaseStream);
// The parsed RepairService OpenAPI plugin includes operations like `listRepairs`, `createRepair`, `updateRepair`, and `deleteRepair`.
// However, based on our business requirements, we need to identify all GET operations that are defined as non-idempotent (i.e., have a payload),
// log a warning for each of them, and exclude these operations from the import.
// To do this, we will locate all GET operations that contain a payload.
// Note that the RepairService OpenAPI plugin does not have any GET operations with payloads, so no operations will be found in this case.
// However, the code below demonstrates how to identify and exclude such operations if they were present.
IEnumerable<RestApiOperation> operationsToExclude = specification.Operations.Where(o => o.Method == HttpMethod.Get && o.Payload is not null);
// Exclude operations that are declared as non-idempotent due to having a payload.
foreach (RestApiOperation operation in operationsToExclude)
{
this.Output.WriteLine($"Warning: The `{operation.Id}` operation with `{operation.Method}` has payload which contradicts to being idempotent. This operation will not be imported.");
specification.Operations.Remove(operation);
}
// Import the OpenAPI document specification.
this._kernel.ImportPluginFromOpenApi("RepairService", specification);
// Tell the AI model not to call any function and show the list of functions it can call instead.
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.None() };
FunctionResult result = await this._kernel.InvokePromptAsync(promptTemplate: "Show me the list of the functions you can call", arguments: new KernelArguments(settings));
this._output.WriteLine(result);
// The AI model output:
// I can call the following functions:
// 1. **RepairService - listRepairs **: Returns a list of repairs with their details and images.
// 2. **RepairService - createRepair **: Adds a new repair to the list with the given details and image URL.
// 3. **RepairService - updateRepair **: Updates an existing repair with new details and image URL.
// 4. **RepairService - deleteRepair **: Deletes an existing repair from the list using its ID.
}
}
@@ -0,0 +1,404 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// These samples demonstrate how SK can handle payloads for OpenAPI functions. Today, SK can handle payloads in the following ways:
/// 1. By accepting the payload from the caller. See the <see cref="InvokeOpenApiFunctionWithPayloadProvidedByCallerAsync"/> sample for more details.
/// 2. By constructing the payload based on the function's schema from leaf properties. See the <see cref="InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesAsync"/> sample for more details.
/// 3. By constructing the payload based on the function's schema from leaf properties with namespaces. See the <see cref="InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesWithNamespacesAsync"/> sample for more details.
/// </summary>
public sealed class OpenApiPlugin_PayloadHandling : BaseTest
{
private readonly Kernel _kernel;
private readonly ITestOutputHelper _output;
private readonly HttpClient _httpClient;
public OpenApiPlugin_PayloadHandling(ITestOutputHelper output) : base(output)
{
IKernelBuilder builder = Kernel.CreateBuilder();
builder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
this._kernel = builder.Build();
this._output = output;
void RequestPayloadHandler(string requestPayload)
{
this._output.WriteLine("Actual request payload");
this._output.WriteLine(requestPayload);
}
// Create HTTP client with a stub handler to log the request payload
this._httpClient = new(new StubHttpHandler(RequestPayloadHandler));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with a payload provided by the caller.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithPayloadProvidedByCallerAsync()
{
// Load an Open API document for the Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/EventPlugin/openapiV2.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Event_Utils", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = false // Disable dynamic payload construction
});
KernelFunction createMeetingFunction = plugin["createMeeting"];
// Function parameters metadata available via createMeetingFunction.Metadata.Parameters property:
// Parameter[0]
// Name: "payload"
// Description: "REST API request body."
// ParameterType: "{Name = "Object" FullName = "System.Object"}"
// Schema: {
// "type": "object",
// "properties": {
// "subject": {
// "type": "string"
// },
// "start": {
// "required": ["dateTime", "timeZone"],
// "type": "object",
// "properties": {
// "dateTime": {
// "type": "string",
// "description": "The start date and time of the meeting in ISO 8601 format.",
// "format": "date-time"
// },
// "timeZone": {
// "type": "string",
// "description": "The time zone in which the meeting starts."
// }
// }
// }
// "end": {
// "type": "object",
// "properties": Similar to the 'start' property one
// }
// "tags": {
// "type": "array",
// "items": {
// "required": ["name"],
// "type": "object",
// "properties": {
// "name": {
// "type": "string",
// "description": "A tag associated with the meeting for categorization."
// }
// }
// },
// "description": "A list of tags to help categorize the meeting."
// }
// }
// }
// Parameter[1]
// Name: "content_type"
// Description: "Content type of REST API request body."
// ParameterType: { Name = "String" FullName = "System.String" }
// Schema: { "type": "string" }
// Create the payload for the createEvent function.
string payload = """
{
"subject": "IT Meeting",
"start": {
"dateTime": "2023-10-01T10:00:00",
"timeZone": "UTC"
},
"end": {
"dateTime": "2023-10-01T11:00:00",
"timeZone": "UTC"
},
"tags": [
{ "name": "IT" },
{ "name": "Meeting" }
]
}
""";
// Create arguments for the createEvent function
KernelArguments arguments = new()
{
["payload"] = payload,
["content-type"] = "application/json"
};
// Example of how to invoke the createEvent function explicitly
await this._kernel.InvokeAsync(createMeetingFunction, arguments);
// Example of how to have the createEvent function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with arguments for payload leaf properties.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesAsync()
{
// Load an Open API document for the simplified Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/EventPlugin/openapiV1.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Event_Utils", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = true // Enable dynamic payload construction. It is enabled by default.
});
KernelFunction createMeetingFunction = plugin["createMeeting"];
// Function parameters metadata available via createMeetingFunction.Metadata.Parameters property:
// Parameter[0]
// Name: "subject"
// Description: "The subject or title of the meeting."
// ParameterType: { Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The subject or title of the meeting." }
// Parameter[1]
// Name: "dateTime"
// Description: "The start date and time of the meeting in ISO 8601 format."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The start date and time of the meeting in ISO 8601 format.", "format": "date-time" }
// Parameter[2]
// Name: "timeZone"
// Description: "The time zone in which the meeting is scheduled."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The time zone in which the meeting is scheduled." }
// Parameter[3]
// Name: "duration"
// Description: "Duration of the meeting in ISO 8601 format (e.g., 'PT1H' for 1 hour).."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "Duration of the meeting in ISO 8601 format (e.g., PT1H for 1 hour)." }
// Parameter[4]
// Name: "tags"
// Description: "A list of tags to help categorize the meeting."
// ParameterType: null
// Schema: { "type": "array", "items": { "required": ["name"], "type": "object", "properties": { "name": { "type": "string", "description": "A tag associated with the meeting for categorization." }}}, "description": "A list of tags to help categorize the meeting."}
// Create arguments for the createEvent function
KernelArguments arguments = new()
{
["subject"] = "IT Meeting",
["dateTime"] = "2023-10-01T10:00:00",
["timeZone"] = "UTC",
["duration"] = "PT1H",
["tags"] = """[ { "name": "IT" }, { "name": "Meeting" } ]"""
};
// Example of how to invoke the createEvent function explicitly
await this._kernel.InvokeAsync(createMeetingFunction, arguments);
// Example of how to have the createEvent function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with arguments for payload leaf properties with namespaces.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesWithNamespacesAsync()
{
// Load an Open API document for the Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/EventPlugin/openapiV2.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Event_Utils", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = true, // Enable dynamic payload construction. It is enabled by default.
EnablePayloadNamespacing = true // Enable payload namespacing.
});
KernelFunction createMeetingFunction = plugin["createMeeting"];
// Function parameters metadata available via createMeetingFunction.Metadata.Parameters property:
// Parameter[0]
// Name: "subject"
// Description: "The subject or title of the meeting."
// ParameterType: { Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The subject or title of the meeting." }
// Parameter[1]
// Name: "start_dateTime"
// Description: "The start date and time of the meeting in ISO 8601 format."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The start date and time of the meeting in ISO 8601 format.", "format": "date-time" }
// Parameter[2]
// Name: "start_timeZone"
// Description: "The time zone in which the meeting is scheduled."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The time zone in which the meeting is scheduled." }
// Parameter[3]
// Name: "end_dateTime"
// Description: "The end date and time of the meeting in ISO 8601 format."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The end date and time of the meeting in ISO 8601 format.", "format": "date-time" }
// Parameter[4]
// Name: "end_timeZone"
// Description: "The time zone in which the meeting ends."
// ParameterType: {Name = "String" FullName = "System.String"}
// Schema: { "type": "string", "description": "The time zone in which the meeting ends." }
// Parameter[5]
// Name: "tags"
// Description: "A list of tags to help categorize the meeting."
// ParameterType: null
// Schema: {
// "type": "array",
// "items": {
// "required": ["name"],
// "type": "object",
// "properties": {
// "name": {
// "type": "string",
// "description": "A tag associated with the meeting for categorization."
// }
// }
// },
// "description": "A list of tags to help categorize the meeting."
// }
// Create arguments for the createEvent function
KernelArguments arguments = new()
{
["subject"] = "IT Meeting",
["start.dateTime"] = "2023-10-01T10:00:00",
["start.timeZone"] = "UTC",
["end.dateTime"] = "2023-10-01T11:00:00",
["end.timeZone"] = "UTC",
["tags"] = """[ { "name": "IT" }, { "name": "Meeting" } ]"""
};
// Example of how to invoke the createEvent function explicitly
await this._kernel.InvokeAsync(createMeetingFunction, arguments);
// Example of how to have the createEvent function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using oneOf.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithArgumentsForPayloadOneOfAsync()
{
// Load an Open API document for the Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/PetsPlugin/oneOfV3.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Pets", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = false // Disable dynamic payload construction. It is enabled by default.
});
// Example of how to have the updatePater function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: Dog { breed=Husky, bark=false }");
await this._kernel.InvokePromptAsync("My new dog is a Husky, he is very quiet, please create my pet information.", new KernelArguments(settings));
Console.WriteLine("\nExpected payload: Dog { breed=Dingo, bark=true }");
await this._kernel.InvokePromptAsync("My dog is a Dingo, he is very noisy, he likes to hunt for rabbits, please update my pet information.", new KernelArguments(settings));
Console.WriteLine("\nExpected payload: Cat { age=15 }");
await this._kernel.InvokePromptAsync("My cat is 15 years old now, please update my pet information.", new KernelArguments(settings));
Console.WriteLine("\nExpected payload: Cat { hunts=true }");
await this._kernel.InvokePromptAsync("I have a feline pet, she goes out every night hunting mice, please update my pet information.", new KernelArguments(settings));
Console.WriteLine("\nExpected payload: Cat { age=3, hunts=true }");
Console.WriteLine(await this._kernel.InvokePromptAsync("I have a new 3 year old cat who chases birds and barks, please create my pet information.", new KernelArguments(settings)));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using allOf.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithArgumentsForPayloadAllOfAsync()
{
// Load an Open API document for the Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/PetsPlugin/allOfV3.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Pets", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = false // Disable dynamic payload construction. It is enabled by default.
});
// Example of how to have the updatePater function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: { pet_type=dog, breed=Husky, bark=false }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My new dog is a Husky, he is very quiet, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=dog, breed=Dingo, bark=true }");
// This prompt deliberately tries to confuse the LLM and it succeed, in this scenario the API must provide an error message so the LLM can correct the playload
Console.WriteLine(await this._kernel.InvokePromptAsync("My new dog is a Dingo, he is very noisy, he likes to hunt for rabbits, please create my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=cat, age=15 }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My cat is 15 years old now, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=cat, hunts=true }");
Console.WriteLine(await this._kernel.InvokePromptAsync("I have a feline pet, she goes out every night hunting mice, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=cat, age=3, hunts=true }");
Console.WriteLine(await this._kernel.InvokePromptAsync("I have a new 3 year old cat who chases birds and barks, please create my pet information.", new KernelArguments(settings)));
}
/// <summary>
/// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using anyOf.
/// </summary>
[Fact]
public async Task InvokeOpenApiFunctionWithArgumentsForPayloadAnyOfAsync()
{
// Load an Open API document for the Event Utils service
using Stream stream = File.OpenRead("Resources/Plugins/PetsPlugin/anyOfV3.json");
// Import an OpenAPI document as SK plugin
KernelPlugin plugin = await this._kernel.ImportPluginFromOpenApiAsync("Pets", stream, new OpenApiFunctionExecutionParameters(this._httpClient)
{
EnableDynamicPayload = false // Disable dynamic payload construction. It is enabled by default.
});
// Example of how to have the updatePater function invoked by the AI
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine("\nExpected payload: { pet_type=Dog, nickname=Fido }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My new dog is named Fido he is 2 years old, please create my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=Dog, nickname=Spot age=1 hunts=true }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My 1 year old dog is called Spot, he likes to hunt for rabbits, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=Cat, age=15 }");
Console.WriteLine(await this._kernel.InvokePromptAsync("My cat is 15 years old now, please update my pet information.", new KernelArguments(settings)));
Console.WriteLine("\nExpected payload: { pet_type=Cat, nick_name=Fluffy }");
Console.WriteLine(await this._kernel.InvokePromptAsync("I have a new feline pet called Fluffy, please create my pet information.", new KernelArguments(settings)));
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._httpClient.Dispose();
}
private sealed class StubHttpHandler : DelegatingHandler
{
private readonly Action<string> _requestPayloadHandler;
private readonly JsonSerializerOptions _options;
public StubHttpHandler(Action<string> requestPayloadHandler) : base()
{
this._requestPayloadHandler = requestPayloadHandler;
this._options = new JsonSerializerOptions { WriteIndented = true };
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage? request, CancellationToken cancellationToken)
{
var requestJson = await request!.Content!.ReadFromJsonAsync<JsonElement>(cancellationToken);
this._requestPayloadHandler(JsonSerializer.Serialize(requestJson, this._options));
return new HttpResponseMessage(System.Net.HttpStatusCode.OK)
{
Content = new StringContent("Success", Encoding.UTF8, "application/json")
};
}
}
}
@@ -0,0 +1,117 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Net;
using System.Text;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// Sample shows how to register the <see cref="RestApiOperationResponseFactory"/> to transform existing or create new <see cref="RestApiOperationResponse"/>.
/// </summary>
public sealed class OpenApiPlugin_RestApiOperationResponseFactory(ITestOutputHelper output) : BaseTest(output)
{
private readonly HttpClient _httpClient = new(new StubHttpHandler(InterceptRequestAndCustomizeResponseAsync));
[Fact]
public async Task IncludeResponseHeadersToOperationResponseAsync()
{
Kernel kernel = new();
// Register the operation response factory and the custom HTTP client
OpenApiFunctionExecutionParameters executionParameters = new()
{
RestApiOperationResponseFactory = IncludeHeadersIntoRestApiOperationResponseAsync,
HttpClient = this._httpClient
};
// Create OpenAPI plugin
KernelPlugin plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync("RepairService", "Resources/Plugins/RepairServicePlugin/repair-service.json", executionParameters);
// Create arguments for a new repair
KernelArguments arguments = new()
{
["title"] = "The Case of the Broken Gizmo",
["description"] = "It's broken. Send help!",
["assignedTo"] = "Tech Magician"
};
// Create the repair
FunctionResult createResult = await plugin["createRepair"].InvokeAsync(kernel, arguments);
// Get operation response that was modified
RestApiOperationResponse response = createResult.GetValue<RestApiOperationResponse>()!;
// Display the 'repair-id' header value
Console.WriteLine(response.Headers!["repair-id"].First());
}
/// <summary>
/// A custom factory to transform the operation response.
/// </summary>
/// <param name="context">The context for the <see cref="RestApiOperationResponseFactory"/>.</param>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>The transformed operation response.</returns>
private static async Task<RestApiOperationResponse> IncludeHeadersIntoRestApiOperationResponseAsync(RestApiOperationResponseFactoryContext context, CancellationToken cancellationToken)
{
// Create the response using the internal factory
RestApiOperationResponse response = await context.InternalFactory(context, cancellationToken);
// Obtain the 'repair-id' header value from the HTTP response and include it in the operation response only for the 'createRepair' operation
if (context.Operation.Id == "createRepair" && context.Response.Headers.TryGetValues("repair-id", out IEnumerable<string>? values))
{
response.Headers ??= new Dictionary<string, IEnumerable<string>>();
response.Headers["repair-id"] = values;
}
// Include the request options in the operation response
if (context.Request.Options is not null)
{
response.Data ??= new Dictionary<string, object?>();
response.Data["http.request.options"] = context.Request.Options;
}
// Return the modified response that will be returned to the caller
return response;
}
/// <summary>
/// A custom HTTP handler to intercept HTTP requests and return custom responses.
/// </summary>
/// <param name="request">The original HTTP request.</param>
/// <returns>The custom HTTP response.</returns>
private static async Task<HttpResponseMessage> InterceptRequestAndCustomizeResponseAsync(HttpRequestMessage request)
{
// Return a mock response that includes the 'repair-id' header for the 'createRepair' operation
if (request.RequestUri!.AbsolutePath == "/repairs" && request.Method == HttpMethod.Post)
{
return new HttpResponseMessage(HttpStatusCode.Created)
{
Content = new StringContent("Success", Encoding.UTF8, "application/json"),
Headers =
{
{ "repair-id", "repair-12345" }
}
};
}
return new HttpResponseMessage(HttpStatusCode.NoContent);
}
private sealed class StubHttpHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> requestHandler) : DelegatingHandler()
{
private readonly Func<HttpRequestMessage, Task<HttpResponseMessage>> _requestHandler = requestHandler;
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
return await this._requestHandler(request);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._httpClient.Dispose();
}
}
@@ -0,0 +1,144 @@
// Copyright (c) Microsoft. All rights reserved.
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Plugins.OpenApi;
namespace Plugins;
/// <summary>
/// Sample with demonstration of logging in OpenAPI plugins.
/// </summary>
public sealed class OpenApiPlugin_Telemetry(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// Default logging in OpenAPI plugins.
/// It's possible to use HTTP logging middleware in ASP.NET applications to log information about HTTP request, headers, body, response etc.
/// More information here: <see href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/http-logging"/>.
/// For custom logging logic, use <see cref="DelegatingHandler"/>.
/// More information here: <see href="https://learn.microsoft.com/en-us/aspnet/web-api/overview/advanced/http-message-handlers"/>.
/// </summary>
[Fact]
public async Task LoggingAsync()
{
// Arrange
using var stream = File.OpenRead("Resources/Plugins/RepairServicePlugin/repair-service.json");
using HttpClient httpClient = new();
var kernelBuilder = Kernel.CreateBuilder();
// If ILoggerFactory is registered in kernel's DI container, it will be used for logging purposes in OpenAPI functionality.
kernelBuilder.Services.AddSingleton<ILoggerFactory>(this.LoggerFactory);
var kernel = kernelBuilder.Build();
var plugin = await OpenApiKernelPluginFactory.CreateFromOpenApiAsync(
"RepairService",
stream,
new OpenApiFunctionExecutionParameters(httpClient)
{
IgnoreNonCompliantErrors = true,
EnableDynamicPayload = false,
// For non-DI scenarios, it's possible to set ILoggerFactory in execution parameters when creating a plugin.
// If ILoggerFactory is provided in both ways through the kernel's DI container and execution parameters,
// the one from execution parameters will be used.
LoggerFactory = kernel.LoggerFactory
});
kernel.Plugins.Add(plugin);
var arguments = new KernelArguments
{
["payload"] = """{ "title": "Engine oil change", "description": "Need to drain the old engine oil and replace it with fresh oil.", "assignedTo": "", "date": "", "image": "" }""",
["content-type"] = "application/json"
};
// Create Repair
var result = await plugin["createRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
// List All Repairs
result = await plugin["listRepairs"].InvokeAsync(kernel, arguments);
var repairs = JsonSerializer.Deserialize<Repair[]>(result.ToString());
Assert.True(repairs?.Length > 0);
var id = repairs[repairs.Length - 1].Id;
// Update Repair
arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id}, \"assignedTo\": \"Karin Blair\", \"date\": \"2024-04-16\", \"image\": \"https://www.howmuchisit.org/wp-content/uploads/2011/01/oil-change.jpg\" }}",
["content-type"] = "application/json"
};
result = await plugin["updateRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
// Delete Repair
arguments = new KernelArguments
{
["payload"] = $"{{ \"id\": {id} }}",
["content-type"] = "application/json"
};
result = await plugin["deleteRepair"].InvokeAsync(kernel, arguments);
Console.WriteLine(result.ToString());
// Output:
// Registering Rest function RepairService.listRepairs
// Created KernelFunction 'listRepairs' for '<CreateRestApiFunction>g__ExecuteAsync|0'
// Registering Rest function RepairService.createRepair
// Created KernelFunction 'createRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
// Registering Rest function RepairService.updateRepair
// Created KernelFunction 'updateRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
// Registering Rest function RepairService.deleteRepair
// Created KernelFunction 'deleteRepair' for '<CreateRestApiFunction>g__ExecuteAsync|0'
// Function RepairService - createRepair invoking.
// Function RepairService - createRepair arguments: { "payload":"{ \u0022title\u0022: \u0022Engine oil change...
// Function RepairService-createRepair succeeded.
// Function RepairService-createRepair result: { "Content":"New repair created",...
// Function RepairService-createRepair completed. Duration: 0.2793481s
// New repair created
// Function RepairService-listRepairs invoking.
// Function RepairService-listRepairs arguments: { "payload":"{ \u0022title\u0022: \u0022Engine oil change...
// Function RepairService-listRepairs succeeded.
// Function RepairService-listRepairs result: { "Content":"[{\u0022id\u0022:79,\u0022title...
// Function RepairService - updateRepair invoking.
// Function RepairService-updateRepair arguments: { "payload":"{ \u0022id\u0022: 96, ...
// Function RepairService-updateRepair succeeded.
// Function RepairService-updateRepair result: { "Content":"Repair updated",...
// Function RepairService-updateRepair completed. Duration: 0.0430169s
// Repair updated
// Function RepairService - deleteRepair invoking.
// Function RepairService-deleteRepair arguments: { "payload":"{ \u0022id\u0022: 96 ...
// Function RepairService-deleteRepair succeeded.
// Function RepairService-deleteRepair result: { "Content":"Repair deleted",...
// Function RepairService-deleteRepair completed. Duration: 0.049715s
// Repair deleted
}
private sealed class Repair
{
[JsonPropertyName("id")]
public int? Id { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("assignedTo")]
public string? AssignedTo { get; set; }
[JsonPropertyName("date")]
public string? Date { get; set; }
[JsonPropertyName("image")]
public string? Image { get; set; }
}
}
@@ -0,0 +1,202 @@
// Copyright (c) Microsoft. All rights reserved.
using System.ComponentModel;
using System.Text.Json.Serialization;
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.Connectors.OpenAI;
namespace Plugins;
/// <summary>
/// Sample showing how to transform a <see cref="KernelPlugin"/> so that not all parameters are advertised to the LLM
/// and instead the argument values are provided by the client code.
/// </summary>
public sealed class TransformPlugin(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// A plugin that returns favorite information for a user.
/// </summary>
public class UserFavorites
{
[KernelFunction]
[Description("Returns the favorite color for the user.")]
public string GetFavoriteColor([Description("Email address of the user.")] string email)
{
return email.Equals("bob@contoso.com", StringComparison.OrdinalIgnoreCase) ? "Green" : "Blue";
}
[KernelFunction]
[Description("Returns the favorite animal of the specified type for the user.")]
public string GetFavoriteAnimal([Description("Email address of the user.")] string email, [Description("Type of animal.")] AnimalType animalType)
{
if (email.Equals("bob@contoso.com", StringComparison.OrdinalIgnoreCase))
{
return GetBobsFavoriteAnimal(animalType);
}
return GetDefaultFavoriteAnimal(animalType);
}
private string GetBobsFavoriteAnimal(AnimalType animalType) => animalType switch
{
AnimalType.Mammals => "Dog",
AnimalType.Birds => "Sparrow",
AnimalType.Reptiles => "Lizard",
AnimalType.Amphibians => "Salamander",
AnimalType.Fish => "Tuna",
AnimalType.Invertebrates => "Spider",
_ => throw new ArgumentOutOfRangeException(nameof(animalType), $"Unexpected animal type: {animalType}"),
};
private string GetDefaultFavoriteAnimal(AnimalType animalType) => animalType switch
{
AnimalType.Mammals => "Horse",
AnimalType.Birds => "Eagle",
AnimalType.Reptiles => "Snake",
AnimalType.Amphibians => "Frog",
AnimalType.Fish => "Shark",
AnimalType.Invertebrates => "Ant",
_ => throw new ArgumentOutOfRangeException(nameof(animalType), $"Unexpected animal type: {animalType}"),
};
}
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum AnimalType
{
[Description("These warm-blooded animals have hair or fur, give birth to live young, and produce milk to feed their offspring. Examples include dogs, tigers, and elephants.")]
Mammals,
[Description("Feathered creatures that lay eggs and have beaks, wings, and hollow bones. They are adapted for flight and include species like eagles and sparrows.")]
Birds,
[Description("Cold-blooded animals with scales, lay eggs, and often live on land. Snakes, lizards, and turtles fall into this category.")]
Reptiles,
[Description("These animals can live both in water and on land. They typically start life as aquatic larvae (like tadpoles) and later transform into adults.Frogs and salamanders are examples.")]
Amphibians,
[Description("Aquatic vertebrates that breathe through gills and have scales.They come in various shapes and sizes, from tiny minnows to massive sharks.")]
Fish,
[Description("The most diverse group, lacking backbones. Insects (like ants and butterflies) and arachnids (such as spiders) are common examples.")]
Invertebrates
}
/// <summary>
/// Shows how LLM will respond if the prompt is missing information required to call a function.
/// </summary>
[Fact]
public async Task MissingRequiredInformationAsync()
{
// Create a kernel with OpenAI chat completion
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
kernelBuilder.Plugins.AddFromType<UserFavorites>();
Kernel kernel = kernelBuilder.Build();
// Invoke the kernel with a prompt and allow the AI to automatically invoke functions
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine(await kernel.InvokePromptAsync("What color should I paint the fence?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("I am going diving what animals would I like to see?", new(settings)));
// Example responses
// If you would like a suggestion based on your preferences, I can find out your favorite color if you provide your email address.
// To help you with that, I would need to know your favorite type of aquatic animals.If you provide your email, I can check your preferences, if available, for your favorite type of fish or other marine creatures.
}
/// <summary>
/// Shows how to transform a plugin so that certain parameters are removed and the arguments are provided separately.
/// </summary>
[Fact]
public async Task CreatePluginWithAlteredParametersAsync()
{
// Create a new Plugin which hides parameters that require PII
var plugin = KernelPluginFactory.CreateFromType<UserFavorites>();
var transformedPlugin = CreatePluginWithParameters(
plugin,
(KernelParameterMetadata parameter) => parameter.Name != "email",
(KernelFunctionMetadata function, KernelArguments arguments) => arguments.Add("email", "bob@contoso.com"));
// Create a kernel with OpenAI chat completion
IKernelBuilder kernelBuilder = Kernel.CreateBuilder();
kernelBuilder.AddOpenAIChatCompletion(
modelId: TestConfiguration.OpenAI.ChatModelId,
apiKey: TestConfiguration.OpenAI.ApiKey);
kernelBuilder.Plugins.Add(transformedPlugin);
Kernel kernel = kernelBuilder.Build();
// Invoke the kernel with a prompt and allow the AI to automatically invoke functions
OpenAIPromptExecutionSettings settings = new() { FunctionChoiceBehavior = FunctionChoiceBehavior.Auto() };
Console.WriteLine(await kernel.InvokePromptAsync("What color should my new car be?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("What color should I paint the fence?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("What is my favorite cold-blooded animal?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("What is my favorite marine animal?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("What is my favorite creepy crawly?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("What is my favorite four legged friend?", new(settings)));
Console.WriteLine(await kernel.InvokePromptAsync("I am going diving what animals would I like to see?", new(settings)));
// Example response
// Your favorite color is Green. 🌿
// Your favorite cold-blooded animal is a lizard.
// Your favorite marine animal is the Tuna. 🐟
// Your favorite creepy crawly is a spider! 🕷️
}
public delegate bool IncludeKernelParameter(KernelParameterMetadata parameter);
public delegate void UpdateKernelArguments(KernelFunctionMetadata function, KernelArguments arguments);
/// <summary>
/// Create a <see cref="KernelPlugin"/> instance from the provided instance where each function only includes
/// permitted parameters. The <see cref="IncludeKernelParameter"/> delegate is called to determine whether or not
/// parameter will be included. The <see cref="UpdateKernelArguments"/> delegate is called to update the arguments
/// and allow additional values to be included.
/// </summary>
public static KernelPlugin CreatePluginWithParameters(KernelPlugin plugin, IncludeKernelParameter includeKernelParameter, UpdateKernelArguments updateKernelArguments)
{
List<KernelFunction>? functions = [];
foreach (KernelFunction function in plugin)
{
functions.Add(CreateFunctionWithParameters(function, includeKernelParameter, updateKernelArguments));
}
return KernelPluginFactory.CreateFromFunctions(plugin.Name, plugin.Description, functions);
}
/// <summary>
/// Create a <see cref="KernelFunction"/> instance from the provided instance which only includes permitted parameters.
/// The function method will add additional argument values before calling the original function.
/// </summary>
private static KernelFunction CreateFunctionWithParameters(KernelFunction function, IncludeKernelParameter includeKernelParameter, UpdateKernelArguments updateKernelArguments)
{
var method = (Kernel kernel, KernelFunction currentFunction, KernelArguments arguments, CancellationToken cancellationToken) =>
{
updateKernelArguments(currentFunction.Metadata, arguments);
return function.InvokeAsync(kernel, arguments, cancellationToken);
};
var options = new KernelFunctionFromMethodOptions()
{
FunctionName = function.Name,
Description = function.Description,
Parameters = CreateParameterMetadataWithParameters(function.Metadata.Parameters, includeKernelParameter),
ReturnParameter = function.Metadata.ReturnParameter,
};
return KernelFunctionFactory.CreateFromMethod(method, options);
}
/// <summary>
/// Create a list of KernelParameterMetadata instances from the provided instances which only includes permitted parameters.
/// </summary>
private static List<KernelParameterMetadata> CreateParameterMetadataWithParameters(IReadOnlyList<KernelParameterMetadata> parameters, IncludeKernelParameter includeKernelParameter)
{
List<KernelParameterMetadata>? parametersToInclude = [];
foreach (var parameter in parameters)
{
if (includeKernelParameter(parameter))
{
parametersToInclude.Add(parameter);
}
}
return parametersToInclude;
}
}
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft. All rights reserved.
using Microsoft.SemanticKernel.Plugins.Web;
namespace Plugins;
/// <summary>
/// Sample showing how to use the Semantic Kernel web plugins correctly.
/// </summary>
public sealed class WebPlugins(ITestOutputHelper output) : BaseTest(output)
{
/// <summary>
/// Shows how to download to a temporary directory on the local machine.
/// </summary>
[Fact]
public async Task DownloadSKLogoAsync()
{
var uri = new Uri("https://raw.githubusercontent.com/microsoft/semantic-kernel/refs/heads/main/docs/images/sk_logo.png");
var folderPath = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
var filePath = Path.Combine(folderPath, "sk_logo.png");
try
{
Directory.CreateDirectory(folderPath);
var webFileDownload = new WebFileDownloadPlugin(this.LoggerFactory)
{
AllowedDomains = ["raw.githubusercontent.com"],
AllowedFolders = [folderPath]
};
await webFileDownload.DownloadToFileAsync(uri, filePath);
if (Path.Exists(filePath))
{
Output.WriteLine($"Successfully downloaded to {filePath}");
}
}
finally
{
if (Path.Exists(folderPath))
{
Directory.Delete(folderPath, true);
}
}
}
}