From 5197f0b1249d7b46ef51768777ea5e6c151aa02e Mon Sep 17 00:00:00 2001 From: Mark Wallace <127216156+markwallace-microsoft@users.noreply.github.com> Date: Tue, 19 Nov 2024 08:29:26 +0000 Subject: [PATCH] .Net: Samples showing use of allOf, anyof and oneOf (#9697) ### Motivation and Context Related to #8997 ### Description ### Contribution Checklist - [ ] The code builds clean without any errors or warnings - [ ] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [ ] All unit tests pass, and I have added new tests where possible - [ ] I didn't break anyone :smile: --- dotnet/samples/Concepts/Concepts.csproj | 18 +++ .../Plugins/OpenApiPlugin_PayloadHandling.cs | 96 ++++++++++++++- .../Resources/Plugins/PetsPlugin/allOfV3.json | 116 ++++++++++++++++++ .../Resources/Plugins/PetsPlugin/anyOfV3.json | 90 ++++++++++++++ .../Resources/Plugins/PetsPlugin/oneOfV3.json | 90 ++++++++++++++ 5 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/allOfV3.json create mode 100644 dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/anyOfV3.json create mode 100644 dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/oneOfV3.json diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 59d33794b9d8..6246e639ea4f 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -107,6 +107,21 @@ + + Always + + + Always + + + Always + + + Always + + + Always + PreserveNewest @@ -128,6 +143,9 @@ + + + diff --git a/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs index ee6db1a4916d..56a133a6ff53 100644 --- a/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs +++ b/dotnet/samples/Concepts/Plugins/OpenApiPlugin_PayloadHandling.cs @@ -24,11 +24,9 @@ public sealed class OpenApiPlugin_PayloadHandling : BaseTest public OpenApiPlugin_PayloadHandling(ITestOutputHelper output) : base(output) { IKernelBuilder builder = Kernel.CreateBuilder(); - - builder.AddAzureOpenAIChatCompletion( - TestConfiguration.AzureOpenAI.ChatDeploymentName, - TestConfiguration.AzureOpenAI.Endpoint, - TestConfiguration.AzureOpenAI.ApiKey); + builder.AddOpenAIChatCompletion( + modelId: TestConfiguration.OpenAI.ChatModelId, + apiKey: TestConfiguration.OpenAI.ApiKey); this._kernel = builder.Build(); @@ -36,7 +34,7 @@ public OpenApiPlugin_PayloadHandling(ITestOutputHelper output) : base(output) void RequestPayloadHandler(string requestPayload) { - this._output.WriteLine("Request payload"); + this._output.WriteLine("Actual request payload"); this._output.WriteLine(requestPayload); } @@ -288,6 +286,92 @@ public async Task InvokeOpenApiFunctionWithArgumentsForPayloadLeafPropertiesWith await this._kernel.InvokePromptAsync("Schedule one hour IT Meeting for October 1st, 2023, at 10:00 AM UTC.", new KernelArguments(settings)); } + /// + /// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using oneOf. + /// + [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 + AzureOpenAIPromptExecutionSettings 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))); + } + + /// + /// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using allOf. + /// + [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 + AzureOpenAIPromptExecutionSettings 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))); + } + + /// + /// This sample demonstrates how to invoke an OpenAPI function with arguments for payload using anyOf. + /// + [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 + AzureOpenAIPromptExecutionSettings 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); diff --git a/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/allOfV3.json b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/allOfV3.json new file mode 100644 index 000000000000..b3e0f172eecf --- /dev/null +++ b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/allOfV3.json @@ -0,0 +1,116 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Pets API", + "version": "1.0.0", + "description": "API for managing pets." + }, + "servers": [ + { + "url": "https://api.yourdomain.com" + } + ], + "paths": { + "/pets": { + "patch": { + "summary": "Update a pet. Call this with either details for a dog or a cat but not both.", + "description": "Update a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "updatePet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { "$ref": "#/components/schemas/Cat" }, + { "$ref": "#/components/schemas/Dog" } + ], + "discriminator": { + "propertyName": "pet_type" + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "post": { + "summary": "Create a pet. Call this with either details for a dog or a cat but not both.", + "description": "Create a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "createPet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { "$ref": "#/components/schemas/Cat" }, + { "$ref": "#/components/schemas/Dog" } + ], + "discriminator": { + "propertyName": "pet_type" + } + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + } + } + }, + "components": { + "schemas": { + "Pet": { + "type": "object", + "required": [ "pet_type" ], + "properties": { + "pet_type": { + "type": "string" + } + }, + "discriminator": { + "propertyName": "pet_type" + } + }, + "Dog": { + "allOf": [ + { "$ref": "#/components/schemas/Pet" }, + { + "type": "object", + "properties": { + "bark": { + "type": "boolean" + }, + "breed": { + "type": "string", + "enum": [ "Dingo", "Husky", "Retriever", "Shepherd" ] + } + } + } + ] + }, + "Cat": { + "allOf": [ + { "$ref": "#/components/schemas/Pet" }, + { + "type": "object", + "properties": { + "hunts": { + "type": "boolean" + }, + "age": { + "type": "integer" + } + } + } + ] + } + } + } +} \ No newline at end of file diff --git a/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/anyOfV3.json b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/anyOfV3.json new file mode 100644 index 000000000000..5c0d91edbc56 --- /dev/null +++ b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/anyOfV3.json @@ -0,0 +1,90 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Pets API", + "version": "1.0.0", + "description": "API for managing pets." + }, + "servers": [ + { + "url": "https://api.yourdomain.com" + } + ], + "paths": { + "/pets": { + "patch": { + "summary": "Update a pet. Call this with either details for a dog or a cat but not both.", + "description": "Update a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "updatePet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { "$ref": "#/components/schemas/PetByAge" }, + { "$ref": "#/components/schemas/PetByType" } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "put": { + "summary": "Create a pet. Call this with either details for a dog or a cat but not both.", + "description": "Create a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "createPet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "anyOf": [ + { "$ref": "#/components/schemas/PetByAge" }, + { "$ref": "#/components/schemas/PetByType" } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Create" + } + } + } + } + }, + "components": { + "schemas": { + "PetByAge": { + "type": "object", + "properties": { + "age": { + "type": "integer" + }, + "nickname": { + "type": "string" + } + }, + "required": [ "age" ] + }, + "PetByType": { + "type": "object", + "properties": { + "pet_type": { + "type": "string", + "enum": [ "Cat", "Dog" ] + }, + "hunts": { + "type": "boolean" + } + }, + "required": [ "pet_type" ] + } + } + } +} \ No newline at end of file diff --git a/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/oneOfV3.json b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/oneOfV3.json new file mode 100644 index 000000000000..a2cbb9a025b7 --- /dev/null +++ b/dotnet/samples/Concepts/Resources/Plugins/PetsPlugin/oneOfV3.json @@ -0,0 +1,90 @@ +{ + "openapi": "3.0.1", + "info": { + "title": "Pets API", + "version": "1.0.0", + "description": "API for managing pets." + }, + "servers": [ + { + "url": "https://api.yourdomain.com" + } + ], + "paths": { + "/pets": { + "patch": { + "summary": "Update a pet. Call this with either details for a dog or a cat but not both.", + "description": "Update a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "updatePet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { "$ref": "#/components/schemas/Cat" }, + { "$ref": "#/components/schemas/Dog" } + ] + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "post": { + "summary": "Create a pet. Call this with either details for a dog or a cat but not both.", + "description": "Create a pet. Call this with either details for a dog or a cat but not both.", + "operationId": "createPet", + "requestBody": { + "content": { + "application/json": { + "schema": { + "oneOf": [ + { "$ref": "#/components/schemas/Cat" }, + { "$ref": "#/components/schemas/Dog" } + ] + } + } + } + }, + "responses": { + "201": { + "description": "Created" + } + } + } + } + }, + "components": { + "schemas": { + "Dog": { + "type": "object", + "description": "A representation of a dog. Do not use for a cat.", + "properties": { + "bark": { + "type": "boolean" + }, + "breed": { + "type": "string", + "enum": [ "Dingo", "Husky", "Retriever", "Shepherd" ] + } + } + }, + "Cat": { + "type": "object", + "description": "A representation of a cat. Do not use for a dog.", + "properties": { + "hunts": { + "type": "boolean" + }, + "age": { + "type": "integer" + } + } + } + } + } +}