From 5f023b53ad7ca3e4f20ceac4daf65408447a2fe9 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 20 Jun 2024 19:06:55 +0100 Subject: [PATCH 001/226] .Net OpenAI SDK V2 - Phase 00 (Feature Branch) (#6894) Empty Projects created for the following changes getting in place in small PRs. This pull request primarily includes changes to the .NET project files and the solution file. These changes introduce new projects and update package dependencies. The most significant changes are: --- dotnet/Directory.Packages.props | 2 + dotnet/SK-dotnet.sln | 36 ++++++++++ dotnet/samples/ConceptsV2/ConceptsV2.csproj | 72 +++++++++++++++++++ .../Connectors.OpenAIV2.UnitTests.csproj | 39 ++++++++++ .../Connectors.OpenAIV2.csproj | 34 +++++++++ .../IntegrationTestsV2.csproj | 67 +++++++++++++++++ 6 files changed, 250 insertions(+) create mode 100644 dotnet/samples/ConceptsV2/ConceptsV2.csproj create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj create mode 100644 dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index d514e22cb5f4..146311afca6f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,6 +5,8 @@ true + + diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 2d11481810cb..9f09181e3846 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -314,6 +314,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimePlugin", "samples\Demos EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.AzureCosmosDBNoSQL", "src\Connectors\Connectors.Memory.AzureCosmosDBNoSQL\Connectors.Memory.AzureCosmosDBNoSQL.csproj", "{B0B3901E-AF56-432B-8FAA-858468E5D0DF}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2", "src\Connectors\Connectors.OpenAIV2\Connectors.OpenAIV2.csproj", "{8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2.UnitTests", "src\Connectors\Connectors.OpenAIV2.UnitTests\Connectors.OpenAIV2.UnitTests.csproj", "{A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConceptsV2", "samples\ConceptsV2\ConceptsV2.csproj", "{932B6B93-C297-47BE-A061-081ACC6105FB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTestsV2", "src\IntegrationTestsV2\IntegrationTestsV2.csproj", "{FDEB4884-89B9-4656-80A0-57C7464490F7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -771,6 +779,30 @@ Global {B0B3901E-AF56-432B-8FAA-858468E5D0DF}.Publish|Any CPU.Build.0 = Publish|Any CPU {B0B3901E-AF56-432B-8FAA-858468E5D0DF}.Release|Any CPU.ActiveCfg = Release|Any CPU {B0B3901E-AF56-432B-8FAA-858468E5D0DF}.Release|Any CPU.Build.0 = Release|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.Build.0 = Debug|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Release|Any CPU.Build.0 = Release|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Publish|Any CPU.Build.0 = Debug|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.Build.0 = Release|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Publish|Any CPU.Build.0 = Debug|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {932B6B93-C297-47BE-A061-081ACC6105FB}.Release|Any CPU.Build.0 = Release|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.Build.0 = Debug|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -877,6 +909,10 @@ Global {1D3EEB5B-0E06-4700-80D5-164956E43D0A} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {F312FCE1-12D7-4DEF-BC29-2FF6618509F3} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {B0B3901E-AF56-432B-8FAA-858468E5D0DF} = {24503383-A8C4-4255-9998-28D70FE8E99A} + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {932B6B93-C297-47BE-A061-081ACC6105FB} = {FA3720F1-C99A-49B2-9577-A940257098BF} + {FDEB4884-89B9-4656-80A0-57C7464490F7} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/ConceptsV2/ConceptsV2.csproj b/dotnet/samples/ConceptsV2/ConceptsV2.csproj new file mode 100644 index 000000000000..a9fe41232166 --- /dev/null +++ b/dotnet/samples/ConceptsV2/ConceptsV2.csproj @@ -0,0 +1,72 @@ + + + + Concepts + + net8.0 + enable + false + true + + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 + Library + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + + Always + + + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj new file mode 100644 index 000000000000..046b5999bee6 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj @@ -0,0 +1,39 @@ + + + + SemanticKernel.Connectors.OpenAI.UnitTests + $(AssemblyName) + net8.0 + true + enable + false + $(NoWarn);SKEXP0001;SKEXP0070;CS1591;IDE1006;RCS1261;CA1031;CA1308;CA1861;CA2007;CA2234;VSTHRD111 + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj new file mode 100644 index 000000000000..3e51e9674e21 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -0,0 +1,34 @@ + + + + + Microsoft.SemanticKernel.Connectors.OpenAI + $(AssemblyName) + net8.0;netstandard2.0 + true + $(NoWarn);NU5104;SKEXP0001,SKEXP0010 + true + + + + + + + + + Semantic Kernel - OpenAI and Azure OpenAI connectors + Semantic Kernel connectors for OpenAI and Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + + + + + diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj new file mode 100644 index 000000000000..cbfbfe9e4df3 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj @@ -0,0 +1,67 @@ + + + IntegrationTests + SemanticKernel.IntegrationTests + net8.0 + true + false + $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0110 + b7762d10-e29b-4bb1-8b74-b6d69a667dd4 + + + + + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + Always + + + Always + + + Always + + + + + + Always + + + \ No newline at end of file From 00f80bc21278650749fffa22a3f6498e9c267ed2 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 20 Jun 2024 22:23:34 +0100 Subject: [PATCH 002/226] feat: add empty AzureOpenAI and AzureOpenAI.UnitTests projects. --- dotnet/SK-dotnet.sln | 20 ++++++++- .../Connectors.AzureOpenAI.UnitTests.csproj | 41 +++++++++++++++++++ .../Connectors.AzureOpenAI.csproj | 34 +++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 9f09181e3846..e87e6db29e1b 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -320,7 +320,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2.UnitTes EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConceptsV2", "samples\ConceptsV2\ConceptsV2.csproj", "{932B6B93-C297-47BE-A061-081ACC6105FB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "IntegrationTestsV2", "src\IntegrationTestsV2\IntegrationTestsV2.csproj", "{FDEB4884-89B9-4656-80A0-57C7464490F7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsV2", "src\IntegrationTestsV2\IntegrationTestsV2.csproj", "{FDEB4884-89B9-4656-80A0-57C7464490F7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -803,6 +807,18 @@ Global {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.Build.0 = Debug|Any CPU {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.ActiveCfg = Release|Any CPU {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.Build.0 = Release|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.Build.0 = Debug|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Release|Any CPU.Build.0 = Release|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Publish|Any CPU.Build.0 = Debug|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DB219924-208B-4CDD-8796-EE424689901E}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -913,6 +929,8 @@ Global {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {932B6B93-C297-47BE-A061-081ACC6105FB} = {FA3720F1-C99A-49B2-9577-A940257098BF} {FDEB4884-89B9-4656-80A0-57C7464490F7} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} + {6744272E-8326-48CE-9A3F-6BE227A5E777} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {DB219924-208B-4CDD-8796-EE424689901E} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj new file mode 100644 index 000000000000..703061c403a2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -0,0 +1,41 @@ + + + + + SemanticKernel.Connectors.AzureOpenAI.UnitTests + $(AssemblyName) + net8.0 + true + enable + false + $(NoWarn);SKEXP0001;SKEXP0010;CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111 + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj new file mode 100644 index 000000000000..837dd5b3c1db --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -0,0 +1,34 @@ + + + + + Microsoft.SemanticKernel.Connectors.AzureOpenAI + $(AssemblyName) + net8.0;netstandard2.0 + true + $(NoWarn);NU5104;SKEXP0001,SKEXP0010 + false + + + + + + + + + Semantic Kernel - Azure OpenAI connectors + Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + + + + + \ No newline at end of file From 5cd0a2809c725788cbf8317ab1b8461bd6af7dfa Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 20 Jun 2024 23:25:14 +0100 Subject: [PATCH 003/226] fix: temporarily disable package validation --- .../Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index 3e51e9674e21..d5e129765dc9 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -7,7 +7,7 @@ net8.0;netstandard2.0 true $(NoWarn);NU5104;SKEXP0001,SKEXP0010 - true + false From 58523951d3c1b7b3e3cda36c23d2a3cc9b872ce0 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh Date: Thu, 20 Jun 2024 23:39:47 +0100 Subject: [PATCH 004/226] fix: publish configuration for the OpenAIV2 project --- dotnet/SK-dotnet.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index e87e6db29e1b..79f0e6bb5596 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -785,8 +785,8 @@ Global {B0B3901E-AF56-432B-8FAA-858468E5D0DF}.Release|Any CPU.Build.0 = Release|Any CPU {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.Build.0 = Debug|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Publish|Any CPU.Build.0 = Publish|Any CPU {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Release|Any CPU.ActiveCfg = Release|Any CPU {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}.Release|Any CPU.Build.0 = Release|Any CPU {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU From e7632f00e2c399ea239418254c0b47ab2737e462 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 20 Jun 2024 16:02:36 -0700 Subject: [PATCH 005/226] .Net: Empty projects for the new AzureOpenAI connector (#6900) Empty Connectors.AzureOpenAI and Connectors.AzureOpenAI.UnitTests projects as a first step to start building AzureOpenAI conector based on new AzureOpenAI SDK. Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/SK-dotnet.sln | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 79f0e6bb5596..01ffff52057a 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -809,8 +809,8 @@ Global {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.Build.0 = Release|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.Build.0 = Debug|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.ActiveCfg = Publish|Any CPU + {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.Build.0 = Publish|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Release|Any CPU.ActiveCfg = Release|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Release|Any CPU.Build.0 = Release|Any CPU {DB219924-208B-4CDD-8796-EE424689901E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU From af19aa707569348b2e3e3d6e57a5918271be576c Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 24 Jun 2024 09:37:31 +0100 Subject: [PATCH 006/226] .Net OpenAI SDK V2 - Phase 01 Embeddings + ClientCore (Feature Branch) (#6898) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## ClientCore + Foundation This PR is the first and starts the foundation structure and classes for the V2 OpenAI Connector. In this PR I also used the simpler `TextEmbeddings` service to wrap up a vertical slice of the Service + Client + UT + IT + Dependencies needed also to validate the proposed structure of folders and namespaces for internal and public components. ### ClientCore As part of this PR I'm also taking benefit of the `partial` keyword for `ClientCore` class dividing its implementation per Service. In the original V1 ClientCore the file was very big, and creating specific files PR service/modality will make it simpler and easier to maintain.    ## What Changed This change includes a new update from previous `Azure.Core` Pipeline abstractions to the new `System.ClientModel` which is used by `OpenAI` package. Those include the update and addition of the below files: - AddHeaderRequestPolicy - Adapted from previous `AddHeaderRequestPolicy` - ClientResultExceptionExtensions - Adapted from previous `RequestExceptionExtensions` - OpenAIClientCore - Merged with ClientCore (No more need for a specialized Azure and OpenAI clients) - ClientCore (Updated internals just with necessary for Text Embeddings), merged `OpenAIClientCore` also into this one and made it not as `abstract` class. - OpenAITextEmbbedingGenerationService (Updated to use `ClientCore` directly instead of `OpenAIClientCore`. ## Whats New - [PipelineSynchronousPolicy - Azure.Core/src/Pipeline/HttpPipelineSynchronousPolicy.cs](https://github.com/Azure/azure-sdk-for-net/blob/8bd22837639d54acccc820e988747f8d28bbde4a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineSynchronousPolicy.cs#L18) This file didn't exist and was necessary to add as it is a dependency for `AddHeaderRequestPolicy` - Mockups added for `System.ClientModel` pipeline testing - Unit Tests Covering - ClientCore - OpenAITextEmbeddingsGenerationService - AddHeadersRequestPolicy - PipelineSynchronousPolicy - ClientResultExceptionExtensions - Integration Tests - OpenAITextEmbeddingsGenerationService (Moved from V1) ## What was Removed - OpenAIClientCore - This class was merged in ClientCore - CustomHostPipelinePolicy - Removed as the new OpenAI SDK supports Non-Default OpenAI endpoints. ## Unit & Integration Test Differently from V1, this PR focus on individual UnitTest for the OpenAI connector only. With the target of above 80% code converage the Unit Tests targets Services + Clients + Extensions & Utilities The structure of folders and tested components on the UnitTests will follow the same structure defined in project under test. --- .../Connectors.OpenAIV2.UnitTests.csproj | 16 +- .../Core/ClientCoreTests.cs | 188 ++++++++++++++++++ .../Models/AddHeaderRequestPolicyTests.cs | 43 ++++ .../Models/PipelineSynchronousPolicyTests.cs | 56 ++++++ .../ClientResultExceptionExtensionsTests.cs | 73 +++++++ ...enAITextEmbeddingGenerationServiceTests.cs | 86 ++++++++ .../text-embeddings-multiple-response.txt | 20 ++ .../TestData/text-embeddings-response.txt | 15 ++ .../Utils/MockPipelineResponse.cs | 156 +++++++++++++++ .../Utils/MockResponseHeaders.cs | 37 ++++ .../Connectors.OpenAIV2.csproj | 1 + .../Core/ClientCore.Embeddings.cs | 64 ++++++ .../Connectors.OpenAIV2/Core/ClientCore.cs | 187 +++++++++++++++++ .../Core/Models/AddHeaderRequestPolicy.cs | 23 +++ .../Core/Models/PipelineSynchronousPolicy.cs | 89 +++++++++ .../ClientResultExceptionExtensions.cs | 44 ++++ .../OpenAITextEmbbedingGenerationService.cs | 85 ++++++++ dotnet/src/IntegrationTestsV2/.editorconfig | 6 + .../OpenAI/OpenAITextEmbeddingTests.cs | 63 ++++++ .../IntegrationTestsV2.csproj | 8 +- .../TestSettings/OpenAIConfiguration.cs | 15 ++ 21 files changed, 1268 insertions(+), 7 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs create mode 100644 dotnet/src/IntegrationTestsV2/.editorconfig create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj index 046b5999bee6..0d89e02beb21 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj @@ -1,4 +1,4 @@ - + SemanticKernel.Connectors.OpenAI.UnitTests @@ -7,7 +7,7 @@ true enable false - $(NoWarn);SKEXP0001;SKEXP0070;CS1591;IDE1006;RCS1261;CA1031;CA1308;CA1861;CA2007;CA2234;VSTHRD111 + $(NoWarn);SKEXP0001;SKEXP0070;SKEXP0010;CS1591;IDE1006;RCS1261;CA1031;CA1308;CA1861;CA2007;CA2234;VSTHRD111 @@ -29,11 +29,21 @@ - + + + + + + Always + + + Always + + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs new file mode 100644 index 000000000000..a3415663459a --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Linq; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Http; +using Moq; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; +public partial class ClientCoreTests +{ + [Fact] + public void ItCanBeInstantiatedAndPropertiesSetAsExpected() + { + // Act + var logger = new Mock>().Object; + var openAIClient = new OpenAIClient(new ApiKeyCredential("key")); + + var clientCoreModelConstructor = new ClientCore("model1", "apiKey"); + var clientCoreOpenAIClientConstructor = new ClientCore("model1", openAIClient, logger: logger); + + // Assert + Assert.NotNull(clientCoreModelConstructor); + Assert.NotNull(clientCoreOpenAIClientConstructor); + + Assert.Equal("model1", clientCoreModelConstructor.ModelId); + Assert.Equal("model1", clientCoreOpenAIClientConstructor.ModelId); + + Assert.NotNull(clientCoreModelConstructor.Client); + Assert.NotNull(clientCoreOpenAIClientConstructor.Client); + Assert.Equal(openAIClient, clientCoreOpenAIClientConstructor.Client); + Assert.Equal(NullLogger.Instance, clientCoreModelConstructor.Logger); + Assert.Equal(logger, clientCoreOpenAIClientConstructor.Logger); + } + + [Theory] + [InlineData(null, null)] + [InlineData("http://localhost", null)] + [InlineData(null, "http://localhost")] + [InlineData("http://localhost-1", "http://localhost-2")] + public void ItUsesEndpointAsExpected(string? clientBaseAddress, string? providedEndpoint) + { + // Arrange + Uri? endpoint = null; + HttpClient? client = null; + if (providedEndpoint is not null) + { + endpoint = new Uri(providedEndpoint); + } + + if (clientBaseAddress is not null) + { + client = new HttpClient { BaseAddress = new Uri(clientBaseAddress) }; + } + + // Act + var clientCore = new ClientCore("model", "apiKey", endpoint: endpoint, httpClient: client); + + // Assert + Assert.Equal(endpoint ?? client?.BaseAddress ?? new Uri("https://api.openai.com/v1"), clientCore.Endpoint); + + client?.Dispose(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ItAddOrganizationHeaderWhenProvidedAsync(bool organizationIdProvided) + { + using HttpMessageHandlerStub handler = new(); + using HttpClient client = new(handler); + handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + + // Act + var clientCore = new ClientCore( + modelId: "model", + apiKey: "test", + organizationId: (organizationIdProvided) ? "organization" : null, + httpClient: client); + + var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + pipelineMessage.Request.Method = "POST"; + pipelineMessage.Request.Uri = new Uri("http://localhost"); + pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); + + // Assert + await clientCore.Client.Pipeline.SendAsync(pipelineMessage); + + if (organizationIdProvided) + { + Assert.True(handler.RequestHeaders!.Contains("OpenAI-Organization")); + Assert.Equal("organization", handler.RequestHeaders.GetValues("OpenAI-Organization").FirstOrDefault()); + } + else + { + Assert.False(handler.RequestHeaders!.Contains("OpenAI-Organization")); + } + } + + [Fact] + public async Task ItAddSemanticKernelHeadersOnEachRequestAsync() + { + using HttpMessageHandlerStub handler = new(); + using HttpClient client = new(handler); + handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + + // Act + var clientCore = new ClientCore(modelId: "model", apiKey: "test", httpClient: client); + + var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + pipelineMessage.Request.Method = "POST"; + pipelineMessage.Request.Uri = new Uri("http://localhost"); + pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); + + // Assert + await clientCore.Client.Pipeline.SendAsync(pipelineMessage); + + Assert.True(handler.RequestHeaders!.Contains(HttpHeaderConstant.Names.SemanticKernelVersion)); + Assert.Equal(HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore)), handler.RequestHeaders.GetValues(HttpHeaderConstant.Names.SemanticKernelVersion).FirstOrDefault()); + + Assert.True(handler.RequestHeaders.Contains("User-Agent")); + Assert.Contains(HttpHeaderConstant.Values.UserAgent, handler.RequestHeaders.GetValues("User-Agent").FirstOrDefault()); + } + + [Fact] + public async Task ItDoNotAddSemanticKernelHeadersWhenOpenAIClientIsProvidedAsync() + { + using HttpMessageHandlerStub handler = new(); + using HttpClient client = new(handler); + handler.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK); + + // Act + var clientCore = new ClientCore( + modelId: "model", + openAIClient: new OpenAIClient( + new ApiKeyCredential("test"), + new OpenAIClientOptions() + { + Transport = new HttpClientPipelineTransport(client), + RetryPolicy = new ClientRetryPolicy(maxRetries: 0), + NetworkTimeout = Timeout.InfiniteTimeSpan + })); + + var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + pipelineMessage.Request.Method = "POST"; + pipelineMessage.Request.Uri = new Uri("http://localhost"); + pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); + + // Assert + await clientCore.Client.Pipeline.SendAsync(pipelineMessage); + + Assert.False(handler.RequestHeaders!.Contains(HttpHeaderConstant.Names.SemanticKernelVersion)); + Assert.DoesNotContain(HttpHeaderConstant.Values.UserAgent, handler.RequestHeaders.GetValues("User-Agent").FirstOrDefault()); + } + + [Theory] + [InlineData(null)] + [InlineData("")] + [InlineData("value")] + public void ItAddAttributesButDoesNothingIfNullOrEmpty(string? value) + { + // Arrange + var clientCore = new ClientCore("model", "apikey"); + // Act + + clientCore.AddAttribute("key", value); + + // Assert + if (string.IsNullOrEmpty(value)) + { + Assert.False(clientCore.Attributes.ContainsKey("key")); + } + else + { + Assert.True(clientCore.Attributes.ContainsKey("key")); + Assert.Equal(value, clientCore.Attributes["key"]); + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs new file mode 100644 index 000000000000..83ec6a20568d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models; + +public class AddHeaderRequestPolicyTests +{ + [Fact] + public void ItCanBeInstantiated() + { + // Arrange + var headerName = "headerName"; + var headerValue = "headerValue"; + + // Act + var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue); + + // Assert + Assert.NotNull(addHeaderRequestPolicy); + } + + [Fact] + public void ItOnSendingRequestAddsHeaderToRequest() + { + // Arrange + var headerName = "headerName"; + var headerValue = "headerValue"; + var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue); + var pipeline = ClientPipeline.Create(); + var message = pipeline.CreateMessage(); + + // Act + addHeaderRequestPolicy.OnSendingRequest(message); + + // Assert + message.Request.Headers.TryGetValue(headerName, out var value); + Assert.NotNull(value); + Assert.Equal(headerValue, value); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs new file mode 100644 index 000000000000..cae4b32b4283 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models; +public class PipelineSynchronousPolicyTests +{ + [Fact] + public async Task ItProcessAsyncWhenSpecializationHasReceivedResponseOverrideShouldCallIt() + { + // Arrange + var first = new MyHttpPipelinePolicyWithoutOverride(); + var last = new MyHttpPipelinePolicyWithOverride(); + + IReadOnlyList policies = [first, last]; + + // Act + await policies[0].ProcessAsync(ClientPipeline.Create().CreateMessage(), policies, 0); + + // Assert + Assert.True(first.CalledProcess); + Assert.True(last.CalledProcess); + Assert.True(last.CalledOnReceivedResponse); + } + + private class MyHttpPipelinePolicyWithoutOverride : PipelineSynchronousPolicy + { + public bool CalledProcess { get; private set; } + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this.CalledProcess = true; + base.Process(message, pipeline, currentIndex); + } + + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this.CalledProcess = true; + return base.ProcessAsync(message, pipeline, currentIndex); + } + } + + private sealed class MyHttpPipelinePolicyWithOverride : MyHttpPipelinePolicyWithoutOverride + { + public bool CalledOnReceivedResponse { get; private set; } + + public override void OnReceivedResponse(PipelineMessage message) + { + this.CalledOnReceivedResponse = true; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs new file mode 100644 index 000000000000..0b95f904d893 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using System.ClientModel.Primitives; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; + +public class ClientResultExceptionExtensionsTests +{ + [Fact] + public void ItCanRecoverFromResponseErrorAndConvertsToHttpOperationExceptionWithDefaultData() + { + // Arrange + var exception = new ClientResultException("message", ClientPipeline.Create().CreateMessage().Response); + + // Act + var httpOperationException = exception.ToHttpOperationException(); + + // Assert + Assert.NotNull(httpOperationException); + Assert.Equal(exception, httpOperationException.InnerException); + Assert.Equal(exception.Message, httpOperationException.Message); + Assert.Null(httpOperationException.ResponseContent); + Assert.Null(httpOperationException.StatusCode); + } + + [Fact] + public void ItCanProvideResponseContentAndStatusCode() + { + // Arrange + using var pipelineResponse = new MockPipelineResponse(); + + pipelineResponse.SetContent("content"); + pipelineResponse.SetStatus(200); + + var exception = new ClientResultException("message", pipelineResponse); + + // Act + var httpOperationException = exception.ToHttpOperationException(); + + // Assert + Assert.NotNull(httpOperationException); + Assert.NotNull(httpOperationException.StatusCode); + Assert.Equal(exception, httpOperationException.InnerException); + Assert.Equal(exception.Message, httpOperationException.Message); + Assert.Equal(pipelineResponse.Content.ToString(), httpOperationException.ResponseContent); + Assert.Equal(pipelineResponse.Status, (int)httpOperationException.StatusCode!); + } + + [Fact] + public void ItProvideStatusForResponsesWithoutContent() + { + // Arrange + using var pipelineResponse = new MockPipelineResponse(); + + pipelineResponse.SetStatus(200); + + var exception = new ClientResultException("message", pipelineResponse); + + // Act + var httpOperationException = exception.ToHttpOperationException(); + + // Assert + Assert.NotNull(httpOperationException); + Assert.NotNull(httpOperationException.StatusCode); + Assert.Empty(httpOperationException.ResponseContent!); + Assert.Equal(exception, httpOperationException.InnerException); + Assert.Equal(exception.Message, httpOperationException.Message); + Assert.Equal(pipelineResponse.Status, (int)httpOperationException.StatusCode!); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs new file mode 100644 index 000000000000..25cdc4ec61aa --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Services; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; +public class OpenAITextEmbeddingGenerationServiceTests +{ + [Fact] + public void ItCanBeInstantiatedAndPropertiesSetAsExpected() + { + // Arrange + var sut = new OpenAITextEmbeddingGenerationService("model", "apiKey", dimensions: 2); + var sutWithOpenAIClient = new OpenAITextEmbeddingGenerationService("model", new OpenAIClient(new ApiKeyCredential("apiKey")), dimensions: 2); + + // Assert + Assert.NotNull(sut); + Assert.NotNull(sutWithOpenAIClient); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal("model", sutWithOpenAIClient.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty() + { + // Arrange + var sut = new OpenAITextEmbeddingGenerationService("model", "apikey"); + + // Act + var result = await sut.GenerateEmbeddingsAsync([], null, CancellationToken.None); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() + { + using HttpMessageHandlerStub handler = new() + { + ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-response.txt")) + } + }; + using HttpClient client = new(handler); + + // Arrange + var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client); + + // Act + var result = await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None); + + // Assert + Assert.Single(result); + Assert.Equal(4, result[0].Length); + } + + [Fact] + public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync() + { + using HttpMessageHandlerStub handler = new() + { + ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-multiple-response.txt")) + } + }; + using HttpClient client = new(handler); + + // Arrange + var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client); + + // Act & Assert + await Assert.ThrowsAsync(async () => await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None)); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt new file mode 100644 index 000000000000..46a9581cf0cc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + }, + { + "object": "embedding", + "index": 1, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 7, + "total_tokens": 7 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt new file mode 100644 index 000000000000..c715b851b78c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt @@ -0,0 +1,15 @@ +{ + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 7, + "total_tokens": 7 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs new file mode 100644 index 000000000000..6fe18b9c1684 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs @@ -0,0 +1,156 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* Phase 01 +This class was imported and adapted from the System.ClientModel Unit Tests. +https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockPipelineResponse.cs +*/ + +using System; +using System.ClientModel.Primitives; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests; + +public class MockPipelineResponse : PipelineResponse +{ + private int _status; + private string _reasonPhrase; + private Stream? _contentStream; + private BinaryData? _bufferedContent; + + private readonly PipelineResponseHeaders _headers; + + private bool _disposed; + + public MockPipelineResponse(int status = 0, string reasonPhrase = "") + { + this._status = status; + this._reasonPhrase = reasonPhrase; + this._headers = new MockResponseHeaders(); + } + + public override int Status => this._status; + + public void SetStatus(int value) => this._status = value; + + public override string ReasonPhrase => this._reasonPhrase; + + public void SetReasonPhrase(string value) => this._reasonPhrase = value; + + public void SetContent(byte[] content) + { + this.ContentStream = new MemoryStream(content, 0, content.Length, false, true); + } + + public MockPipelineResponse SetContent(string content) + { + this.SetContent(Encoding.UTF8.GetBytes(content)); + return this; + } + + public override Stream? ContentStream + { + get => this._contentStream; + set => this._contentStream = value; + } + + public override BinaryData Content + { + get + { + if (this._contentStream is null) + { + return new BinaryData(Array.Empty()); + } + + if (this.ContentStream is not MemoryStream memoryContent) + { + throw new InvalidOperationException("The response is not buffered."); + } + + if (memoryContent.TryGetBuffer(out ArraySegment segment)) + { + return new BinaryData(segment.AsMemory()); + } + return new BinaryData(memoryContent.ToArray()); + } + } + + protected override PipelineResponseHeaders HeadersCore + => this._headers; + + public sealed override void Dispose() + { + this.Dispose(true); + + GC.SuppressFinalize(this); + } + + protected void Dispose(bool disposing) + { + if (disposing && !this._disposed) + { + Stream? content = this._contentStream; + if (content != null) + { + this._contentStream = null; + content.Dispose(); + } + + this._disposed = true; + } + } + + public override BinaryData BufferContent(CancellationToken cancellationToken = default) + { + if (this._bufferedContent is not null) + { + return this._bufferedContent; + } + + if (this._contentStream is null) + { + this._bufferedContent = new BinaryData(Array.Empty()); + return this._bufferedContent; + } + + MemoryStream bufferStream = new(); + this._contentStream.CopyTo(bufferStream); + this._contentStream.Dispose(); + this._contentStream = bufferStream; + + // Less efficient FromStream method called here because it is a mock. + // For intended production implementation, see HttpClientTransportResponse. + this._bufferedContent = BinaryData.FromStream(bufferStream); + return this._bufferedContent; + } + + public override async ValueTask BufferContentAsync(CancellationToken cancellationToken = default) + { + if (this._bufferedContent is not null) + { + return this._bufferedContent; + } + + if (this._contentStream is null) + { + this._bufferedContent = new BinaryData(Array.Empty()); + return this._bufferedContent; + } + + MemoryStream bufferStream = new(); + + await this._contentStream.CopyToAsync(bufferStream, cancellationToken).ConfigureAwait(false); + await this._contentStream.DisposeAsync().ConfigureAwait(false); + + this._contentStream = bufferStream; + + // Less efficient FromStream method called here because it is a mock. + // For intended production implementation, see HttpClientTransportResponse. + this._bufferedContent = BinaryData.FromStream(bufferStream); + return this._bufferedContent; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs new file mode 100644 index 000000000000..fceef64e4bae --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* Phase 01 +This class was imported and adapted from the System.ClientModel Unit Tests. +https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockResponseHeaders.cs +*/ + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests; + +public class MockResponseHeaders : PipelineResponseHeaders +{ + private readonly Dictionary _headers; + + public MockResponseHeaders() + { + this._headers = new Dictionary(); + } + + public override IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public override bool TryGetValue(string name, out string? value) + { + return this._headers.TryGetValue(name, out value); + } + + public override bool TryGetValues(string name, out IEnumerable? values) + { + throw new NotImplementedException(); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index d5e129765dc9..b17b14eb91ef 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -30,5 +30,6 @@ + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs new file mode 100644 index 000000000000..d11e2799addd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 01 + +This class was created to simplify any Text Embeddings Support from the v1 ClientCore +*/ + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Embeddings; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an embedding from the given . + /// + /// List of strings to generate embeddings for + /// The containing services, plugins, and other state for use throughout the operation. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The to monitor for cancellation requests. The default is . + /// List of embeddings + internal async Task>> GetEmbeddingsAsync( + IList data, + Kernel? kernel, + int? dimensions, + CancellationToken cancellationToken) + { + var result = new List>(data.Count); + + if (data.Count > 0) + { + var embeddingsOptions = new EmbeddingGenerationOptions() + { + Dimensions = dimensions + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.ModelId).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var embeddings = response.Value; + + if (embeddings.Count != data.Count) + { + throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); + } + + for (var i = 0; i < embeddings.Count; i++) + { + result.Add(embeddings[i].Vector); + } + } + + return result; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs new file mode 100644 index 000000000000..12ca2f3d92fe --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 01 : This class was created adapting and merging ClientCore and OpenAIClientCore classes. +System.ClientModel changes were added and adapted to the code as this package is now used as a dependency over OpenAI package. +All logic from original ClientCore and OpenAIClientCore were preserved. +*/ + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Http; +using OpenAI; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Default OpenAI API endpoint. + /// + private const string OpenAIV1Endpoint = "https://api.openai.com/v1"; + + /// + /// Identifier of the default model to use + /// + internal string ModelId { get; init; } = string.Empty; + + /// + /// Non-default endpoint for OpenAI API. + /// + internal Uri? Endpoint { get; init; } + + /// + /// Logger instance + /// + internal ILogger Logger { get; init; } + + /// + /// OpenAI / Azure OpenAI Client + /// + internal OpenAIClient Client { get; } + + /// + /// Storage for AI service attributes. + /// + internal Dictionary Attributes { get; } = []; + + /// + /// Initializes a new instance of the class. + /// + /// Model name. + /// OpenAI API Key. + /// OpenAI compatible API endpoint. + /// OpenAI Organization Id (usually optional). + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + internal ClientCore( + string modelId, + string? apiKey = null, + Uri? endpoint = null, + string? organizationId = null, + HttpClient? httpClient = null, + ILogger? logger = null) + { + Verify.NotNullOrWhiteSpace(modelId); + + this.Logger = logger ?? NullLogger.Instance; + this.ModelId = modelId; + + // Accepts the endpoint if provided, otherwise uses the default OpenAI endpoint. + this.Endpoint = endpoint ?? httpClient?.BaseAddress; + if (this.Endpoint is null) + { + Verify.NotNullOrWhiteSpace(apiKey); // For Public OpenAI Endpoint a key must be provided. + this.Endpoint = new Uri(OpenAIV1Endpoint); + } + + var options = GetOpenAIClientOptions(httpClient, this.Endpoint); + if (!string.IsNullOrWhiteSpace(organizationId)) + { + options.AddPolicy(new AddHeaderRequestPolicy("OpenAI-Organization", organizationId!), PipelinePosition.PerCall); + } + + this.Client = new OpenAIClient(apiKey ?? string.Empty, options); + } + + /// + /// Initializes a new instance of the class using the specified OpenAIClient. + /// Note: instances created this way might not have the default diagnostics settings, + /// it's up to the caller to configure the client. + /// + /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// The to use for logging. If null, no logging will be performed. + internal ClientCore( + string modelId, + OpenAIClient openAIClient, + ILogger? logger = null) + { + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNull(openAIClient); + + this.Logger = logger ?? NullLogger.Instance; + this.ModelId = modelId; + this.Client = openAIClient; + } + + /// + /// Logs OpenAI action details. + /// + /// Caller member name. Populated automatically by runtime. + internal void LogActionDetails([CallerMemberName] string? callerMemberName = default) + { + if (this.Logger.IsEnabled(LogLevel.Information)) + { + this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.ModelId); + } + } + + /// + /// Allows adding attributes to the client. + /// + /// Attribute key. + /// Attribute value. + internal void AddAttribute(string key, string? value) + { + if (!string.IsNullOrEmpty(value)) + { + this.Attributes.Add(key, value); + } + } + + /// Gets options to use for an OpenAIClient + /// Custom for HTTP requests. + /// Endpoint for the OpenAI API. + /// An instance of . + private static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, Uri? endpoint) + { + OpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint + }; + + options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout + } + + return options; + } + + /// + /// Invokes the specified request and handles exceptions. + /// + /// Type of the response. + /// Request to invoke. + /// Returns the response. + private static async Task RunRequestAsync(Func> request) + { + try + { + return await request.Invoke().ConfigureAwait(false); + } + catch (ClientResultException e) + { + throw e.ToHttpOperationException(); + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs new file mode 100644 index 000000000000..2279d639c54e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* Phase 1 +Added from OpenAI v1 with adapted logic to the System.ClientModel abstraction +*/ + +using System.ClientModel.Primitives; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Helper class to inject headers into System ClientModel Http pipeline +/// +internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : PipelineSynchronousPolicy +{ + private readonly string _headerName = headerName; + private readonly string _headerValue = headerValue; + + public override void OnSendingRequest(PipelineMessage message) + { + message.Request.Headers.Add(this._headerName, this._headerValue); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs new file mode 100644 index 000000000000..b7690ead8b7f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 1 +As SystemClient model does not have any specialization or extension ATM, introduced this class with the adapted to use System.ClientModel abstractions. +https://github.com/Azure/azure-sdk-for-net/blob/8bd22837639d54acccc820e988747f8d28bbde4a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineSynchronousPolicy.cs +*/ + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Represents a that doesn't do any asynchronous or synchronously blocking operations. +/// +internal class PipelineSynchronousPolicy : PipelinePolicy +{ + private static readonly Type[] s_onReceivedResponseParameters = new[] { typeof(PipelineMessage) }; + + private readonly bool _hasOnReceivedResponse = true; + + /// + /// Initializes a new instance of + /// + protected PipelineSynchronousPolicy() + { + var onReceivedResponseMethod = this.GetType().GetMethod(nameof(OnReceivedResponse), BindingFlags.Instance | BindingFlags.Public, null, s_onReceivedResponseParameters, null); + if (onReceivedResponseMethod != null) + { + this._hasOnReceivedResponse = onReceivedResponseMethod.GetBaseDefinition().DeclaringType != onReceivedResponseMethod.DeclaringType; + } + } + + /// + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this.OnSendingRequest(message); + if (pipeline.Count > currentIndex + 1) + { + // If there are more policies in the pipeline, continue processing + ProcessNext(message, pipeline, currentIndex); + } + this.OnReceivedResponse(message); + } + + /// + public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + if (!this._hasOnReceivedResponse) + { + // If OnReceivedResponse was not overridden we can avoid creating a state machine and return the task directly + this.OnSendingRequest(message); + if (pipeline.Count > currentIndex + 1) + { + // If there are more policies in the pipeline, continue processing + return ProcessNextAsync(message, pipeline, currentIndex); + } + } + + return this.InnerProcessAsync(message, pipeline, currentIndex); + } + + private async ValueTask InnerProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this.OnSendingRequest(message); + if (pipeline.Count > currentIndex + 1) + { + // If there are more policies in the pipeline, continue processing + await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); + } + this.OnReceivedResponse(message); + } + + /// + /// Method is invoked before the request is sent. + /// + /// The containing the request. + public virtual void OnSendingRequest(PipelineMessage message) { } + + /// + /// Method is invoked after the response is received. + /// + /// The containing the response. + public virtual void OnReceivedResponse(PipelineMessage message) { } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs new file mode 100644 index 000000000000..7da92e5826ba --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 01: +This class is introduced in exchange for the original RequestExceptionExtensions class of Azure.Core to the new ClientException from System.ClientModel, +Preserved the logic as is. +*/ + +using System.ClientModel; +using System.Net; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Provides extension methods for the class. +/// +internal static class ClientResultExceptionExtensions +{ + /// + /// Converts a to an . + /// + /// The original . + /// An instance. + public static HttpOperationException ToHttpOperationException(this ClientResultException exception) + { + const int NoResponseReceived = 0; + + string? responseContent = null; + + try + { + responseContent = exception.GetRawResponse()?.Content.ToString(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead. +#pragma warning restore CA1031 + + return new HttpOperationException( + exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status, + responseContent, + exception.Message, + exception); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs new file mode 100644 index 000000000000..49915031b7fc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Services; +using OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI implementation of +/// +[Experimental("SKEXP0010")] +public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService +{ + private readonly ClientCore _core; + private readonly int? _dimensions; + + /// + /// Create an instance of + /// + /// Model name + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + public OpenAITextEmbeddingGenerationService( + string modelId, + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null, + int? dimensions = null) + { + this._core = new( + modelId: modelId, + apiKey: apiKey, + organizationId: organization, + httpClient: httpClient, + logger: loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._dimensions = dimensions; + } + + /// + /// Create an instance of the OpenAI text embedding connector + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + public OpenAITextEmbeddingGenerationService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null, + int? dimensions = null) + { + this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._dimensions = dimensions; + } + + /// + public IReadOnlyDictionary Attributes => this._core.Attributes; + + /// + public Task>> GenerateEmbeddingsAsync( + IList data, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + this._core.LogActionDetails(); + return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); + } +} diff --git a/dotnet/src/IntegrationTestsV2/.editorconfig b/dotnet/src/IntegrationTestsV2/.editorconfig new file mode 100644 index 000000000000..394eef685f21 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/.editorconfig @@ -0,0 +1,6 @@ +# Suppressing errors for Test projects under dotnet folder +[*.cs] +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member +dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs new file mode 100644 index 000000000000..6eca1909a546 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Embeddings; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAITextEmbeddingTests +{ + private const int AdaVectorLength = 1536; + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData("test sentence")] + public async Task OpenAITestAsync(string testInputString) + { + // Arrange + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get(); + Assert.NotNull(openAIConfiguration); + + var embeddingGenerator = new OpenAITextEmbeddingGenerationService(openAIConfiguration.ModelId, openAIConfiguration.ApiKey); + + // Act + var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString); + var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]); + + // Assert + Assert.Equal(AdaVectorLength, singleResult.Length); + Assert.Equal(3, batchResult.Count); + } + + [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData(null, 3072)] + [InlineData(1024, 1024)] + public async Task OpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength) + { + // Arrange + const string TestInputString = "test sentence"; + + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get(); + Assert.NotNull(openAIConfiguration); + + var embeddingGenerator = new OpenAITextEmbeddingGenerationService( + "text-embedding-3-large", + openAIConfiguration.ApiKey, + dimensions: dimensions); + + // Act + var result = await embeddingGenerator.GenerateEmbeddingAsync(TestInputString); + + // Assert + Assert.Equal(expectedVectorLength, result.Length); + } +} diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj index cbfbfe9e4df3..f3c704a27307 100644 --- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj +++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj @@ -1,7 +1,7 @@ - + IntegrationTests - SemanticKernel.IntegrationTests + SemanticKernel.IntegrationTestsV2 net8.0 true false @@ -16,7 +16,7 @@ - + @@ -44,7 +44,6 @@ - @@ -64,4 +63,5 @@ Always + \ No newline at end of file diff --git a/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs b/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs new file mode 100644 index 000000000000..cb3884e3bdfc --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace SemanticKernel.IntegrationTests.TestSettings; + +[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Configuration classes are instantiated through IConfiguration.")] +internal sealed class OpenAIConfiguration(string serviceId, string modelId, string apiKey, string? chatModelId = null) +{ + public string ServiceId { get; set; } = serviceId; + public string ModelId { get; set; } = modelId; + public string? ChatModelId { get; set; } = chatModelId; + public string ApiKey { get; set; } = apiKey; +} From 6729af13a4909ac40ce9a0272b1cc2b67b8329e8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 24 Jun 2024 02:28:03 -0700 Subject: [PATCH 007/226] .Net: Copy code related to AzureChatCompletionSeervice from Connectors.OpenAI to Connectors.AzureOpenAI (#6906) ### Motivation and Context As a first step in migrating AzureOpenAIConnector to Azure AI SDK v2, all code related to AzureOpenAIChatCompletionService, including unit tests, is copied from the Connectors.OpenAI project to the Connectors.AzureOpenAI project as-is, with only the structural modifications described below and no logical modifications. This is a preparatory step before refactoring the AzureOpenAIChatCompletionService to use Azure SDK v2. ### Description This PR does the following: 1. Copies the AzureOpenAIChatCompletionService class and all its dependencies to the Connectors.AzureOpenAI project as they are, with no code changes. 2. Copies all existing unit tests related to the AzureOpenAIChatCompletionService service and its dependencies to the Connectors.AzureOpenAI.UnitTests project. 3. Renames some files in the Connectors.AzureOpenAI project so that their names begin with AzureOpenAI instead of OpenAI. 4. Changes namespaces in the copied files from Microsoft.SemanticKernel.Connectors.OpenAI to Microsoft.SemanticKernel.Connectors.AzureOpenAI. Related to the "Move reusable code from existing Microsoft.SemanticKernel.Connectors.OpenAI project to the new project" task of the https://github.com/microsoft/semantic-kernel/issues/6864 issue. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../.editorconfig | 6 + ...AzureOpenAIPromptExecutionSettingsTests.cs | 274 +++ .../AzureOpenAITestHelper.cs | 20 + .../AzureToolCallBehaviorTests.cs | 248 +++ .../AzureOpenAIChatCompletionServiceTests.cs | 958 ++++++++++ .../ChatHistoryExtensionsTests.cs | 45 + .../Connectors.AzureOpenAI.UnitTests.csproj | 6 + .../AzureOpenAIChatMessageContentTests.cs | 124 ++ .../Core/AzureOpenAIFunctionToolCallTests.cs | 81 + ...reOpenAIPluginCollectionExtensionsTests.cs | 75 + .../AzureOpenAIStreamingTextContentTests.cs | 41 + .../RequestFailedExceptionExtensionsTests.cs | 77 + .../AutoFunctionInvocationFilterTests.cs | 629 +++++++ .../AzureOpenAIFunctionTests.cs | 188 ++ .../KernelFunctionMetadataExtensionsTests.cs | 256 +++ .../MultipleHttpMessageHandlerStub.cs | 53 + ...multiple_function_calls_test_response.json | 64 + ...on_single_function_call_test_response.json | 32 + ..._multiple_function_calls_test_response.txt | 9 + ...ing_single_function_call_test_response.txt | 3 + ...hat_completion_streaming_test_response.txt | 5 + .../chat_completion_test_response.json | 22 + ...tion_with_data_streaming_test_response.txt | 1 + ...at_completion_with_data_test_response.json | 28 + ...multiple_function_calls_test_response.json | 40 + ..._multiple_function_calls_test_response.txt | 5 + ...ext_completion_streaming_test_response.txt | 3 + .../text_completion_test_response.json | 19 + .../AddHeaderRequestPolicy.cs | 20 + .../AzureOpenAIPromptExecutionSettings.cs | 432 +++++ .../AzureToolCallBehavior.cs | 269 +++ .../AzureOpenAIChatCompletionService.cs | 102 ++ .../ChatHistoryExtensions.cs | 70 + .../Connectors.AzureOpenAI.csproj | 2 +- .../Core/AzureOpenAIChatMessageContent.cs | 117 ++ .../Core/AzureOpenAIClientCore.cs | 102 ++ .../Core/AzureOpenAIFunction.cs | 178 ++ .../Core/AzureOpenAIFunctionToolCall.cs | 170 ++ ...eOpenAIKernelFunctionMetadataExtensions.cs | 54 + .../AzureOpenAIPluginCollectionExtensions.cs | 62 + .../AzureOpenAIStreamingChatMessageContent.cs | 87 + .../Core/AzureOpenAIStreamingTextContent.cs | 51 + .../Connectors.AzureOpenAI/Core/ClientCore.cs | 1574 +++++++++++++++++ .../RequestFailedExceptionExtensions.cs | 38 + 44 files changed, 6639 insertions(+), 1 deletion(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/.editorconfig create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/.editorconfig b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/.editorconfig new file mode 100644 index 000000000000..394eef685f21 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/.editorconfig @@ -0,0 +1,6 @@ +# Suppressing errors for Test projects under dotnet folder +[*.cs] +dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task +dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave +dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member +dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs new file mode 100644 index 000000000000..0cf1c4e2a9e3 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs @@ -0,0 +1,274 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; + +/// +/// Unit tests of AzureOpenAIPromptExecutionSettingsTests +/// +public class AzureOpenAIPromptExecutionSettingsTests +{ + [Fact] + public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() + { + // Arrange + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(1, executionSettings.Temperature); + Assert.Equal(1, executionSettings.TopP); + Assert.Equal(0, executionSettings.FrequencyPenalty); + Assert.Equal(0, executionSettings.PresencePenalty); + Assert.Equal(1, executionSettings.ResultsPerPrompt); + Assert.Null(executionSettings.StopSequences); + Assert.Null(executionSettings.TokenSelectionBiases); + Assert.Null(executionSettings.TopLogprobs); + Assert.Null(executionSettings.Logprobs); + Assert.Null(executionSettings.AzureChatExtensionsOptions); + Assert.Equal(128, executionSettings.MaxTokens); + } + + [Fact] + public void ItUsesExistingOpenAIExecutionSettings() + { + // Arrange + AzureOpenAIPromptExecutionSettings actualSettings = new() + { + Temperature = 0.7, + TopP = 0.7, + FrequencyPenalty = 0.7, + PresencePenalty = 0.7, + ResultsPerPrompt = 2, + StopSequences = new string[] { "foo", "bar" }, + ChatSystemPrompt = "chat system prompt", + MaxTokens = 128, + Logprobs = true, + TopLogprobs = 5, + TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(actualSettings, executionSettings); + } + + [Fact] + public void ItCanUseOpenAIExecutionSettings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() { + { "max_tokens", 1000 }, + { "temperature", 0 } + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(1000, executionSettings.MaxTokens); + Assert.Equal(0, executionSettings.Temperature); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", 0.7 }, + { "top_p", 0.7 }, + { "frequency_penalty", 0.7 }, + { "presence_penalty", 0.7 }, + { "results_per_prompt", 2 }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", 128 }, + { "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 }, + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", "0.7" }, + { "top_p", "0.7" }, + { "frequency_penalty", "0.7" }, + { "presence_penalty", "0.7" }, + { "results_per_prompt", "2" }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", "128" }, + { "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 } + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase() + { + // Arrange + var json = """ + { + "temperature": 0.7, + "top_p": 0.7, + "frequency_penalty": 0.7, + "presence_penalty": 0.7, + "results_per_prompt": 2, + "stop_sequences": [ "foo", "bar" ], + "chat_system_prompt": "chat system prompt", + "token_selection_biases": { "1": 2, "3": 4 }, + "max_tokens": 128, + "seed": 123456, + "logprobs": true, + "top_logprobs": 5 + } + """; + var actualSettings = JsonSerializer.Deserialize(json); + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Theory] + [InlineData("", "")] + [InlineData("System prompt", "System prompt")] + public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) + { + // Arrange & Act + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; + + // Assert + Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); + } + + [Fact] + public void PromptExecutionSettingsCloneWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + var clone = executionSettings!.Clone(); + + // Assert + Assert.NotNull(clone); + Assert.Equal(executionSettings.ModelId, clone.ModelId); + Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); + } + + [Fact] + public void PromptExecutionSettingsFreezeWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": [ "DONE" ], + "token_selection_biases": { "1": 2, "3": 4 } + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + executionSettings!.Freeze(); + + // Assert + Assert.True(executionSettings.IsFrozen); + Assert.Throws(() => executionSettings.ModelId = "gpt-4"); + Assert.Throws(() => executionSettings.ResultsPerPrompt = 2); + Assert.Throws(() => executionSettings.Temperature = 1); + Assert.Throws(() => executionSettings.TopP = 1); + Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); + Assert.Throws(() => executionSettings.TokenSelectionBiases?.Add(5, 6)); + + executionSettings!.Freeze(); // idempotent + Assert.True(executionSettings.IsFrozen); + } + + [Fact] + public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() + { + // Arrange + var executionSettings = new AzureOpenAIPromptExecutionSettings { StopSequences = [] }; + + // Act +#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions + var executionSettingsWithData = AzureOpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings); +#pragma warning restore CS0618 + // Assert + Assert.Null(executionSettingsWithData.StopSequences); + } + + private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings executionSettings) + { + Assert.NotNull(executionSettings); + Assert.Equal(0.7, executionSettings.Temperature); + Assert.Equal(0.7, executionSettings.TopP); + Assert.Equal(0.7, executionSettings.FrequencyPenalty); + Assert.Equal(0.7, executionSettings.PresencePenalty); + Assert.Equal(2, executionSettings.ResultsPerPrompt); + Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); + Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); + Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); + Assert.Equal(128, executionSettings.MaxTokens); + Assert.Equal(123456, executionSettings.Seed); + Assert.Equal(true, executionSettings.Logprobs); + Assert.Equal(5, executionSettings.TopLogprobs); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs new file mode 100644 index 000000000000..9df4aae40c2d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.IO; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; + +/// +/// Helper for AzureOpenAI test purposes. +/// +internal static class AzureOpenAITestHelper +{ + /// + /// Reads test response from file for mocking purposes. + /// + /// Name of the file with test response. + internal static string GetTestResponse(string fileName) + { + return File.ReadAllText($"./TestData/{fileName}"); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs new file mode 100644 index 000000000000..525dabcd26d2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs @@ -0,0 +1,248 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureToolCallBehavior; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; + +/// +/// Unit tests for +/// +public sealed class AzureToolCallBehaviorTests +{ + [Fact] + public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + var behavior = AzureToolCallBehavior.EnableKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + const int DefaultMaximumAutoInvokeAttempts = 128; + var behavior = AzureToolCallBehavior.AutoInvokeKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(DefaultMaximumAutoInvokeAttempts, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void EnableFunctionsReturnsEnabledFunctionsInstance() + { + // Arrange & Act + List functions = [new("Plugin", "Function", "description", [], null)]; + var behavior = AzureToolCallBehavior.EnableFunctions(functions); + + // Assert + Assert.IsType(behavior); + } + + [Fact] + public void RequireFunctionReturnsRequiredFunctionInstance() + { + // Arrange & Act + var behavior = AzureToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); + + // Assert + Assert.IsType(behavior); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act + kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); + + // Assert + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + // Act + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + var plugin = this.GetTestPlugin(); + + kernel.Plugins.Add(plugin); + + // Act + kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var enabledFunctions = new EnabledFunctions([], autoInvoke: false); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act + enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); + + // Assert + Assert.Null(chatCompletionsOptions.ToolChoice); + Assert.Empty(chatCompletionsOptions.Tools); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); + Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); + Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool autoInvoke) + { + // Arrange + var plugin = this.GetTestPlugin(); + var functions = plugin.GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + kernel.Plugins.Add(plugin); + + // Act + enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + [Fact] + public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null, chatCompletionsOptions)); + Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions)); + Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); + } + + [Fact] + public void RequiredFunctionConfigureOptionsAddsTools() + { + // Arrange + var plugin = this.GetTestPlugin(); + var function = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); + var chatCompletionsOptions = new ChatCompletionsOptions(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var kernel = new Kernel(); + kernel.Plugins.Add(plugin); + + // Act + requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions); + + // Assert + Assert.NotNull(chatCompletionsOptions.ToolChoice); + + this.AssertTools(chatCompletionsOptions); + } + + private KernelPlugin GetTestPlugin() + { + var function = KernelFunctionFactory.CreateFromMethod( + (string parameter1, string parameter2) => "Result1", + "MyFunction", + "Test Function", + [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], + new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + } + + private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) + { + Assert.Single(chatCompletionsOptions.Tools); + + var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; + + Assert.NotNull(tool); + + Assert.Equal("MyPlugin-MyFunction", tool.Name); + Assert.Equal("Test Function", tool.Description); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.Parameters.ToString()); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs new file mode 100644 index 000000000000..69c314bdcb46 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -0,0 +1,958 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Moq; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.ChatCompletion; + +/// +/// Unit tests for +/// +public sealed class AzureOpenAIChatCompletionServiceTests : IDisposable +{ + private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAIChatCompletionServiceTests() + { + this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + + var mockLogger = new Mock(); + + mockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(l => l.CreateLogger(It.IsAny())).Returns(mockLogger.Object); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new AzureOpenAIChatCompletionService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIChatCompletionService("deployment", client, "model-id"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task GetTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetTextContentsAsync("Prompt"); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Text); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + } + + [Fact] + public async Task GetChatMessageContentsWithEmptyChoicesThrowsExceptionAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent("{\"id\":\"response-id\",\"object\":\"chat.completion\",\"created\":1704208954,\"model\":\"gpt-4\",\"choices\":[],\"usage\":{\"prompt_tokens\":55,\"completion_tokens\":100,\"total_tokens\":155},\"system_fingerprint\":null}") + }); + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([])); + + Assert.Equal("Chat completions not found", exception.Message); + } + + [Theory] + [InlineData(0)] + [InlineData(129)] + public async Task GetChatMessageContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; + + // Act & Assert + var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([], settings)); + + Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings() + { + MaxTokens = 123, + Temperature = 0.6, + TopP = 0.5, + FrequencyPenalty = 1.6, + PresencePenalty = 1.2, + ResultsPerPrompt = 5, + Seed = 567, + TokenSelectionBiases = new Dictionary { { 2, 3 } }, + StopSequences = ["stop_sequence"], + Logprobs = true, + TopLogprobs = 5, + AzureChatExtensionsOptions = new AzureChatExtensionsOptions + { + Extensions = + { + new AzureSearchChatExtensionConfiguration + { + SearchEndpoint = new Uri("http://test-search-endpoint"), + IndexName = "test-index-name" + } + } + } + }; + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("User Message"); + chatHistory.AddUserMessage([new ImageContent(new Uri("https://image")), new TextContent("User Message")]); + chatHistory.AddSystemMessage("System Message"); + chatHistory.AddAssistantMessage("Assistant Message"); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync(chatHistory, settings); + + // Assert + var requestContent = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContent); + + var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); + + var messages = content.GetProperty("messages"); + + var userMessage = messages[0]; + var userMessageCollection = messages[1]; + var systemMessage = messages[2]; + var assistantMessage = messages[3]; + + Assert.Equal("user", userMessage.GetProperty("role").GetString()); + Assert.Equal("User Message", userMessage.GetProperty("content").GetString()); + + Assert.Equal("user", userMessageCollection.GetProperty("role").GetString()); + var contentItems = userMessageCollection.GetProperty("content"); + Assert.Equal(2, contentItems.GetArrayLength()); + Assert.Equal("https://image/", contentItems[0].GetProperty("image_url").GetProperty("url").GetString()); + Assert.Equal("image_url", contentItems[0].GetProperty("type").GetString()); + Assert.Equal("User Message", contentItems[1].GetProperty("text").GetString()); + Assert.Equal("text", contentItems[1].GetProperty("type").GetString()); + + Assert.Equal("system", systemMessage.GetProperty("role").GetString()); + Assert.Equal("System Message", systemMessage.GetProperty("content").GetString()); + + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("Assistant Message", assistantMessage.GetProperty("content").GetString()); + + Assert.Equal(123, content.GetProperty("max_tokens").GetInt32()); + Assert.Equal(0.6, content.GetProperty("temperature").GetDouble()); + Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); + Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); + Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); + Assert.Equal(5, content.GetProperty("n").GetInt32()); + Assert.Equal(567, content.GetProperty("seed").GetInt32()); + Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); + Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); + Assert.True(content.GetProperty("logprobs").GetBoolean()); + Assert.Equal(5, content.GetProperty("top_logprobs").GetInt32()); + + var dataSources = content.GetProperty("data_sources"); + Assert.Equal(1, dataSources.GetArrayLength()); + Assert.Equal("azure_search", dataSources[0].GetProperty("type").GetString()); + + var dataSourceParameters = dataSources[0].GetProperty("parameters"); + Assert.Equal("http://test-search-endpoint/", dataSourceParameters.GetProperty("endpoint").GetString()); + Assert.Equal("test-index-name", dataSourceParameters.GetProperty("index_name").GetString()); + } + + [Theory] + [MemberData(nameof(ResponseFormats))] + public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(object responseFormat, string? expectedResponseType) + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings + { + ResponseFormat = responseFormat + }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync([], settings); + + // Assert + var requestContent = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContent); + + var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); + + Assert.Equal(expectedResponseType, content.GetProperty("response_format").GetProperty("type").GetString()); + } + + [Theory] + [MemberData(nameof(ToolCallBehaviors))] + public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureToolCallBehavior behavior) + { + // Arrange + var kernel = Kernel.CreateBuilder().Build(); + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = behavior }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Content); + + var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + + Assert.NotNull(usage); + Assert.Equal(55, usage.PromptTokens); + Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(155, usage.TotalTokens); + + Assert.Equal("stop", result[0].Metadata?["FinishReason"]); + } + + [Fact] + public async Task GetChatMessageContentsWithFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function1 = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => + { + functionCallCount++; + throw new ArgumentException("Some exception"); + }, "FunctionWithException"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Content); + + Assert.Equal(2, functionCallCount); + } + + [Fact] + public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() + { + // Arrange + const int DefaultMaximumAutoInvokeAttempts = 128; + const int ModelResponsesCount = 129; + + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + var responses = new List(); + + for (var i = 0; i < ModelResponsesCount; i++) + { + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }); + } + + this._messageHandlerStub.ResponsesToReturn = responses; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); + } + + [Fact] + public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToAzureOpenAIFunction(); + + kernel.Plugins.Add(plugin); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await service.GetChatMessageContentsAsync([], settings, kernel); + + // Assert + Assert.Equal(1, functionCallCount); + + var requestContents = this._messageHandlerStub.RequestContents; + + Assert.Equal(2, requestContents.Count); + + requestContents.ForEach(Assert.NotNull); + + var firstContent = Encoding.UTF8.GetString(requestContents[0]!); + var secondContent = Encoding.UTF8.GetString(requestContents[1]!); + + var firstContentJson = JsonSerializer.Deserialize(firstContent); + var secondContentJson = JsonSerializer.Deserialize(secondContent); + + Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("MyPlugin-GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }); + + // Act & Assert + var enumerator = service.GetStreamingTextContentsAsync("Prompt").GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Text); + + await enumerator.MoveNextAsync(); + Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + using var stream = new MemoryStream(Encoding.UTF8.GetBytes(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }); + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([]).GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + + await enumerator.MoveNextAsync(); + Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function1 = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => + { + functionCallCount++; + throw new ArgumentException("Some exception"); + }, "FunctionWithException"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_multiple_function_calls_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + + await enumerator.MoveNextAsync(); + Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + + // Keep looping until the end of stream + while (await enumerator.MoveNextAsync()) + { + } + + Assert.Equal(2, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() + { + // Arrange + const int DefaultMaximumAutoInvokeAttempts = 128; + const int ModelResponsesCount = 129; + + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + var responses = new List(); + + for (var i = 0; i < ModelResponsesCount; i++) + { + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }); + } + + this._messageHandlerStub.ResponsesToReturn = responses; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + + Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToAzureOpenAIFunction(); + + kernel.Plugins.Add(plugin); + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); + + // Function Tool Call Streaming (One Chunk) + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + + // Chat Completion Streaming (1st Chunk) + await enumerator.MoveNextAsync(); + Assert.Null(enumerator.Current.Metadata?["FinishReason"]); + + // Chat Completion Streaming (2nd Chunk) + await enumerator.MoveNextAsync(); + Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + + Assert.Equal(1, functionCallCount); + + var requestContents = this._messageHandlerStub.RequestContents; + + Assert.Equal(2, requestContents.Count); + + requestContents.ForEach(Assert.NotNull); + + var firstContent = Encoding.UTF8.GetString(requestContents[0]!); + var secondContent = Encoding.UTF8.GetString(requestContents[1]!); + + var firstContentJson = JsonSerializer.Deserialize(firstContent); + var secondContentJson = JsonSerializer.Deserialize(secondContent); + + Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("MyPlugin-GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); + } + + [Fact] + public async Task GetChatMessageContentsUsesPromptAndSettingsCorrectlyAsync() + { + // Arrange + const string Prompt = "This is test prompt"; + const string SystemMessage = "This is test system message"; + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + IKernelBuilder builder = Kernel.CreateBuilder(); + builder.Services.AddTransient((sp) => service); + Kernel kernel = builder.Build(); + + // Act + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + // Assert + Assert.Equal("Test chat response", result.ToString()); + + var requestContentByteArray = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContentByteArray); + + var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray)); + + var messages = requestContent.GetProperty("messages"); + + Assert.Equal(2, messages.GetArrayLength()); + + Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); + Assert.Equal("system", messages[0].GetProperty("role").GetString()); + + Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); + Assert.Equal("user", messages[1].GetProperty("role").GetString()); + } + + [Fact] + public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync() + { + // Arrange + const string Prompt = "This is test prompt"; + const string SystemMessage = "This is test system message"; + const string AssistantMessage = "This is assistant message"; + const string CollectionItemPrompt = "This is collection item prompt"; + + var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + var settings = new AzureOpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; + + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage(Prompt); + chatHistory.AddAssistantMessage(AssistantMessage); + chatHistory.AddUserMessage( + [ + new TextContent(CollectionItemPrompt), + new ImageContent(new Uri("https://image")) + ]); + + // Act + var result = await service.GetChatMessageContentsAsync(chatHistory, settings); + + // Assert + Assert.True(result.Count > 0); + Assert.Equal("Test chat response", result[0].Content); + + var requestContentByteArray = this._messageHandlerStub.RequestContents[0]; + + Assert.NotNull(requestContentByteArray); + + var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray)); + + var messages = requestContent.GetProperty("messages"); + + Assert.Equal(4, messages.GetArrayLength()); + + Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); + Assert.Equal("system", messages[0].GetProperty("role").GetString()); + + Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); + Assert.Equal("user", messages[1].GetProperty("role").GetString()); + + Assert.Equal(AssistantMessage, messages[2].GetProperty("content").GetString()); + Assert.Equal("assistant", messages[2].GetProperty("role").GetString()); + + var contentItems = messages[3].GetProperty("content"); + Assert.Equal(2, contentItems.GetArrayLength()); + Assert.Equal(CollectionItemPrompt, contentItems[0].GetProperty("text").GetString()); + Assert.Equal("text", contentItems[0].GetProperty("type").GetString()); + Assert.Equal("https://image/", contentItems[1].GetProperty("image_url").GetProperty("url").GetString()); + Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); + } + + [Fact] + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Fake prompt"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Items.Count); + + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + Assert.NotNull(getCurrentWeatherFunctionCall); + Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); + Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); + Assert.Equal("1", getCurrentWeatherFunctionCall.Id); + Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); + + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + Assert.NotNull(functionWithExceptionFunctionCall); + Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); + Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); + Assert.Equal("2", functionWithExceptionFunctionCall.Id); + Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); + + var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + Assert.NotNull(nonExistentFunctionCall); + Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); + Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); + Assert.Equal("3", nonExistentFunctionCall.Id); + Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); + + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + Assert.NotNull(invalidArgumentsFunctionCall); + Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); + Assert.Equal("4", invalidArgumentsFunctionCall.Id); + Assert.Null(invalidArgumentsFunctionCall.Arguments); + Assert.NotNull(invalidArgumentsFunctionCall.Exception); + Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); + Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); + + var intArgumentsFunctionCall = result.Items[4] as FunctionCallContent; + Assert.NotNull(intArgumentsFunctionCall); + Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); + Assert.Equal("5", intArgumentsFunctionCall.Id); + Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); + } + + [Fact] + public async Task FunctionCallsShouldBeReturnedToLLMAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var items = new ChatMessageContentItemCollection + { + new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + }; + + ChatHistory chatHistory = + [ + new ChatMessageContent(AuthorRole.Assistant, items) + ]; + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(1, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + + Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); + + var tool1 = assistantMessage.GetProperty("tool_calls")[0]; + Assert.Equal("1", tool1.GetProperty("id").GetString()); + Assert.Equal("function", tool1.GetProperty("type").GetString()); + + var function1 = tool1.GetProperty("function"); + Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); + + var tool2 = assistantMessage.GetProperty("tool_calls")[1]; + Assert.Equal("2", tool2.GetProperty("id").GetString()); + Assert.Equal("function", tool2.GetProperty("type").GetString()); + + var function2 = tool2.GetProperty("function"); + Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + ]), + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + ]) + }; + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[1]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) + }); + + var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + ]) + }; + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[1]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + public static TheoryData ToolCallBehaviors => new() + { + AzureToolCallBehavior.EnableKernelFunctions, + AzureToolCallBehavior.AutoInvokeKernelFunctions + }; + + public static TheoryData ResponseFormats => new() + { + { new FakeChatCompletionsResponseFormat(), null }, + { "json_object", "json_object" }, + { "text", "text" } + }; + + private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat; +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs new file mode 100644 index 000000000000..a0579f6d6c72 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; +public class ChatHistoryExtensionsTests +{ + [Fact] + public async Task ItCanAddMessageFromStreamingChatContentsAsync() + { + var metadata = new Dictionary() + { + { "message", "something" }, + }; + + var chatHistoryStreamingContents = new List + { + new(AuthorRole.User, "Hello ", metadata: metadata), + new(null, ", ", metadata: metadata), + new(null, "I ", metadata: metadata), + new(null, "am ", metadata : metadata), + new(null, "a ", metadata : metadata), + new(null, "test ", metadata : metadata), + }.ToAsyncEnumerable(); + + var chatHistory = new ChatHistory(); + var finalContent = "Hello , I am a test "; + string processedContent = string.Empty; + await foreach (var chatMessageChunk in chatHistory.AddStreamingMessageAsync(chatHistoryStreamingContents)) + { + processedContent += chatMessageChunk.Content; + } + + Assert.Single(chatHistory); + Assert.Equal(finalContent, processedContent); + Assert.Equal(finalContent, chatHistory[0].Content); + Assert.Equal(AuthorRole.User, chatHistory[0].Role); + Assert.Equal(metadata["message"], chatHistory[0].Metadata!["message"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj index 703061c403a2..5952d571a09f 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -38,4 +38,10 @@ + + + Always + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs new file mode 100644 index 000000000000..304e62bc9aeb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIChatMessageContentTests +{ + [Fact] + public void ConstructorsWorkCorrectly() + { + // Arrange + List toolCalls = [new FakeChatCompletionsToolCall("id")]; + + // Act + var content1 = new AzureOpenAIChatMessageContent(new ChatRole("user"), "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; + var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); + + // Assert + this.AssertChatMessageContent(AuthorRole.User, "content1", "model-id1", toolCalls, content1, "Fred"); + this.AssertChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, content2); + } + + [Fact] + public void GetOpenAIFunctionToolCallsReturnsCorrectList() + { + // Arrange + List toolCalls = [ + new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), + new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), + new FakeChatCompletionsToolCall("id3"), + new FakeChatCompletionsToolCall("id4")]; + + var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); + var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); + + // Act + var actualToolCalls1 = content1.GetOpenAIFunctionToolCalls(); + var actualToolCalls2 = content2.GetOpenAIFunctionToolCalls(); + + // Assert + Assert.Equal(2, actualToolCalls1.Count); + Assert.Equal("id1", actualToolCalls1[0].Id); + Assert.Equal("id2", actualToolCalls1[1].Id); + + Assert.Empty(actualToolCalls2); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) + { + // Arrange + IReadOnlyDictionary metadata = readOnlyMetadata ? + new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : + new Dictionary { { "key", "value" } }; + + List toolCalls = [ + new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), + new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), + new FakeChatCompletionsToolCall("id3"), + new FakeChatCompletionsToolCall("id4")]; + + // Act + var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); + var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, metadata); + + // Assert + Assert.NotNull(content1.Metadata); + Assert.Single(content1.Metadata); + + Assert.NotNull(content2.Metadata); + Assert.Equal(2, content2.Metadata.Count); + Assert.Equal("value", content2.Metadata["key"]); + + Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); + + var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; + Assert.NotNull(actualToolCalls); + + Assert.Equal(2, actualToolCalls.Count); + Assert.Equal("id1", actualToolCalls[0].Id); + Assert.Equal("id2", actualToolCalls[1].Id); + } + + private void AssertChatMessageContent( + AuthorRole expectedRole, + string expectedContent, + string expectedModelId, + IReadOnlyList expectedToolCalls, + AzureOpenAIChatMessageContent actualContent, + string? expectedName = null) + { + Assert.Equal(expectedRole, actualContent.Role); + Assert.Equal(expectedContent, actualContent.Content); + Assert.Equal(expectedName, actualContent.AuthorName); + Assert.Equal(expectedModelId, actualContent.ModelId); + Assert.Same(expectedToolCalls, actualContent.ToolCalls); + } + + private sealed class FakeChatCompletionsToolCall(string id) : ChatCompletionsToolCall(id) + { } + + private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> + { + public TValue this[TKey key] => dictionary[key]; + public IEnumerable Keys => dictionary.Keys; + public IEnumerable Values => dictionary.Values; + public int Count => dictionary.Count; + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => dictionary.TryGetValue(key, out value); + IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs new file mode 100644 index 000000000000..8f16c6ea7db2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIFunctionToolCallTests +{ + [Theory] + [InlineData("MyFunction", "MyFunction")] + [InlineData("MyPlugin_MyFunction", "MyPlugin_MyFunction")] + public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) + { + // Arrange + var toolCall = new ChatCompletionsFunctionToolCall("id", toolCallName, string.Empty); + var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal(expectedName, openAIFunctionToolCall.FullyQualifiedName); + Assert.Same(openAIFunctionToolCall.FullyQualifiedName, openAIFunctionToolCall.FullyQualifiedName); + } + + [Fact] + public void ToStringReturnsCorrectValue() + { + // Arrange + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); + var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", openAIFunctionToolCall.ToString()); + } + + [Fact] + public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary(); + var functionNamesByIndex = new Dictionary(); + var functionArgumentBuildersByIndex = new Dictionary(); + + // Act + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Empty(toolCalls); + } + + [Fact] + public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary { { 3, "test-id" } }; + var functionNamesByIndex = new Dictionary { { 3, "test-function" } }; + var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; + + // Act + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Single(toolCalls); + + var toolCall = toolCalls[0]; + + Assert.Equal("test-id", toolCall.Id); + Assert.Equal("test-function", toolCall.Name); + Assert.Equal("test-argument", toolCall.Arguments); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs new file mode 100644 index 000000000000..bbfb636196d3 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIPluginCollectionExtensionsTests +{ + [Fact] + public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() + { + // Arrange + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); + var plugins = new KernelPluginCollection([plugin]); + + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.False(result); + Assert.Null(actualFunction); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Equal(function.Name, actualFunction?.Name); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); + + // Act + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Equal(function.Name, actualFunction?.Name); + + Assert.NotNull(actualArguments); + + Assert.Equal("San Diego", actualArguments["location"]); + Assert.Equal("300", actualArguments["max_price"]); + + Assert.Null(actualArguments["null_argument"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs new file mode 100644 index 000000000000..a58df5676aca --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIStreamingTextContentTests +{ + [Fact] + public void ToByteArrayWorksCorrectly() + { + // Arrange + var expectedBytes = Encoding.UTF8.GetBytes("content"); + var content = new AzureOpenAIStreamingTextContent("content", 0, "model-id"); + + // Act + var actualBytes = content.ToByteArray(); + + // Assert + Assert.Equal(expectedBytes, actualBytes); + } + + [Theory] + [InlineData(null, "")] + [InlineData("content", "content")] + public void ToStringWorksCorrectly(string? content, string expectedString) + { + // Arrange + var textContent = new AzureOpenAIStreamingTextContent(content!, 0, "model-id"); + + // Act + var actualString = textContent.ToString(); + + // Assert + Assert.Equal(expectedString, actualString); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs new file mode 100644 index 000000000000..9fb65039116d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Net; +using Azure; +using Azure.Core; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class RequestFailedExceptionExtensionsTests +{ + [Theory] + [InlineData(0, null)] + [InlineData(500, HttpStatusCode.InternalServerError)] + public void ToHttpOperationExceptionWithStatusReturnsValidException(int responseStatus, HttpStatusCode? httpStatusCode) + { + // Arrange + var exception = new RequestFailedException(responseStatus, "Error Message"); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(httpStatusCode, actualException.StatusCode); + Assert.Equal("Error Message", actualException.Message); + Assert.Same(exception, actualException.InnerException); + } + + [Fact] + public void ToHttpOperationExceptionWithContentReturnsValidException() + { + // Arrange + using var response = new FakeResponse("Response Content", 500); + var exception = new RequestFailedException(response); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); + Assert.Equal("Response Content", actualException.ResponseContent); + Assert.Same(exception, actualException.InnerException); + } + + #region private + + private sealed class FakeResponse(string responseContent, int status) : Response + { + private readonly string _responseContent = responseContent; + private readonly IEnumerable _headers = []; + + public override BinaryData Content => BinaryData.FromString(this._responseContent); + public override int Status { get; } = status; + public override string ReasonPhrase => "Reason Phrase"; + public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } + public override string ClientRequestId { get => "Client Request Id"; set => throw new NotImplementedException(); } + + public override void Dispose() { } + protected override bool ContainsHeader(string name) => throw new NotImplementedException(); + protected override IEnumerable EnumerateHeaders() => this._headers; +#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + protected override bool TryGetHeader(string name, out string? value) => throw new NotImplementedException(); + protected override bool TryGetHeaderValues(string name, out IEnumerable? values) => throw new NotImplementedException(); +#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs new file mode 100644 index 000000000000..270b055d730c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs @@ -0,0 +1,629 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; + +public sealed class AutoFunctionInvocationFilterTests : IDisposable +{ + private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + + public AutoFunctionInvocationFilterTests() + { + this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); + + this._httpClient = new HttpClient(this._messageHandlerStub, false); + } + + [Fact] + public async Task FiltersAreExecutedCorrectlyAsync() + { + // Arrange + int filterInvocations = 0; + int functionInvocations = 0; + int[] expectedRequestSequenceNumbers = [0, 0, 1, 1]; + int[] expectedFunctionSequenceNumbers = [0, 1, 0, 1]; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + Kernel? contextKernel = null; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + contextKernel = context.Kernel; + + if (context.ChatHistory.Last() is AzureOpenAIChatMessageContent content) + { + Assert.Equal(2, content.ToolCalls.Count); + } + + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + filterInvocations++; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(4, filterInvocations); + Assert.Equal(4, functionInvocations); + Assert.Equal(expectedRequestSequenceNumbers, requestSequenceNumbers); + Assert.Equal(expectedFunctionSequenceNumbers, functionSequenceNumbers); + Assert.Same(kernel, contextKernel); + Assert.Equal("Test chat response", result.ToString()); + } + + [Fact] + public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() + { + // Arrange + int filterInvocations = 0; + int functionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + if (context.ChatHistory.Last() is AzureOpenAIChatMessageContent content) + { + Assert.Equal(2, content.ToolCalls.Count); + } + + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + filterInvocations++; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { } + + // Assert + Assert.Equal(4, filterInvocations); + Assert.Equal(4, functionInvocations); + Assert.Equal([0, 0, 1, 1], requestSequenceNumbers); + Assert.Equal([0, 1, 0, 1], functionSequenceNumbers); + } + + [Fact] + public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); + var executionOrder = new List(); + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var filter1 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter1-Invoking"); + await next(context); + }); + + var filter2 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter2-Invoking"); + await next(context); + }); + + var builder = Kernel.CreateBuilder(); + + builder.Plugins.Add(plugin); + + builder.Services.AddSingleton((serviceProvider) => + { + return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + + // Case #1 - Add filter to services + builder.Services.AddSingleton(filter1); + + var kernel = builder.Build(); + + // Case #2 - Add filter to kernel + kernel.AutoFunctionInvocationFilters.Add(filter2); + + var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal("Filter1-Invoking", executionOrder[0]); + Assert.Equal("Filter2-Invoking", executionOrder[1]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); + var executionOrder = new List(); + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var filter1 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter1-Invoking"); + await next(context); + executionOrder.Add("Filter1-Invoked"); + }); + + var filter2 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter2-Invoking"); + await next(context); + executionOrder.Add("Filter2-Invoked"); + }); + + var filter3 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter3-Invoking"); + await next(context); + executionOrder.Add("Filter3-Invoked"); + }); + + var builder = Kernel.CreateBuilder(); + + builder.Plugins.Add(plugin); + + builder.Services.AddSingleton((serviceProvider) => + { + return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); + }); + + builder.Services.AddSingleton(filter1); + builder.Services.AddSingleton(filter2); + builder.Services.AddSingleton(filter3); + + var kernel = builder.Build(); + + var arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + }); + + // Act + if (isStreaming) + { + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", arguments)) + { } + } + else + { + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + await kernel.InvokePromptAsync("Test prompt", arguments); + } + + // Assert + Assert.Equal("Filter1-Invoking", executionOrder[0]); + Assert.Equal("Filter2-Invoking", executionOrder[1]); + Assert.Equal("Filter3-Invoking", executionOrder[2]); + Assert.Equal("Filter3-Invoked", executionOrder[3]); + Assert.Equal("Filter2-Invoked", executionOrder[4]); + Assert.Equal("Filter1-Invoked", executionOrder[5]); + } + + [Fact] + public async Task FilterCanOverrideArgumentsAsync() + { + // Arrange + const string NewValue = "NewValue"; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + context.Arguments!["parameter"] = NewValue; + await next(context); + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal("NewValue", result.ToString()); + } + + [Fact] + public async Task FilterCanHandleExceptionAsync() + { + // Arrange + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + try + { + await next(context); + } + catch (KernelException exception) + { + Assert.Equal("Exception from Function1", exception.Message); + context.Result = new FunctionResult(context.Result, "Result from filter"); + } + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); + + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + var chatHistory = new ChatHistory(); + + // Act + var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); + + var firstFunctionResult = chatHistory[^2].Content; + var secondFunctionResult = chatHistory[^1].Content; + + // Assert + Assert.Equal("Result from filter", firstFunctionResult); + Assert.Equal("Result from Function2", secondFunctionResult); + } + + [Fact] + public async Task FilterCanHandleExceptionOnStreamingAsync() + { + // Arrange + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + try + { + await next(context); + } + catch (KernelException) + { + context.Result = new FunctionResult(context.Result, "Result from filter"); + } + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); + + var chatHistory = new ChatHistory(); + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) + { } + + var firstFunctionResult = chatHistory[^2].Content; + var secondFunctionResult = chatHistory[^1].Content; + + // Assert + Assert.Equal("Result from filter", firstFunctionResult); + Assert.Equal("Result from Function2", secondFunctionResult); + } + + [Fact] + public async Task FiltersCanSkipFunctionExecutionAsync() + { + // Arrange + int filterInvocations = 0; + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Filter delegate is invoked only for second function, the first one should be skipped. + if (context.Function.Name == "Function2") + { + await next(context); + } + + filterInvocations++; + }); + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(2, filterInvocations); + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(1, secondFunctionInvocations); + } + + [Fact] + public async Task PreFilterCanTerminateOperationAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Terminating before first function, so all functions won't be invoked. + context.Terminate = true; + + await next(context); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + } + + [Fact] + public async Task PreFilterCanTerminateOperationOnStreamingAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Terminating before first function, so all functions won't be invoked. + context.Terminate = true; + + await next(context); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { } + + // Assert + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + } + + [Fact] + public async Task PostFilterCanTerminateOperationAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + // Terminating after first function, so second function won't be invoked. + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings + { + ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(1, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + Assert.Equal([0], requestSequenceNumbers); + Assert.Equal([0], functionSequenceNumbers); + + // Results of function invoked before termination should be returned + var lastMessageContent = result.GetValue(); + Assert.NotNull(lastMessageContent); + + Assert.Equal("function1-value", lastMessageContent.Content); + Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); + } + + [Fact] + public async Task PostFilterCanTerminateOperationOnStreamingAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + // Terminating after first function, so second function won't be invoked. + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + + List streamingContent = []; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { + streamingContent.Add(item); + } + + // Assert + Assert.Equal(1, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + Assert.Equal([0], requestSequenceNumbers); + Assert.Equal([0], functionSequenceNumbers); + + // Results of function invoked before termination should be returned + Assert.Equal(3, streamingContent.Count); + + var lastMessageContent = streamingContent[^1] as StreamingChatMessageContent; + Assert.NotNull(lastMessageContent); + + Assert.Equal("function1-value", lastMessageContent.Content); + Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + #region private + +#pragma warning disable CA2000 // Dispose objects before losing scope + private static List GetFunctionCallingResponses() + { + return [ + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) } + ]; + } + + private static List GetFunctionCallingStreamingResponses() + { + return [ + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) } + ]; + } +#pragma warning restore CA2000 + + private Kernel GetKernelWithFilter( + KernelPlugin plugin, + Func, Task>? onAutoFunctionInvocation) + { + var builder = Kernel.CreateBuilder(); + var filter = new AutoFunctionInvocationFilter(onAutoFunctionInvocation); + + builder.Plugins.Add(plugin); + builder.Services.AddSingleton(filter); + + builder.Services.AddSingleton((serviceProvider) => + { + return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); + }); + + return builder.Build(); + } + + private sealed class AutoFunctionInvocationFilter( + Func, Task>? onAutoFunctionInvocation) : IAutoFunctionInvocationFilter + { + private readonly Func, Task>? _onAutoFunctionInvocation = onAutoFunctionInvocation; + + public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) => + this._onAutoFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs new file mode 100644 index 000000000000..bd268ef67991 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs @@ -0,0 +1,188 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; + +public sealed class AzureOpenAIFunctionTests +{ + [Theory] + [InlineData(null, null, "", "")] + [InlineData("name", "description", "name", "description")] + public void ItInitializesOpenAIFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new AzureOpenAIFunctionParameter(name, description, true, typeof(string), schema); + + // Assert + Assert.Equal(expectedName, functionParameter.Name); + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.True(functionParameter.IsRequired); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + + [Theory] + [InlineData(null, "")] + [InlineData("description", "description")] + public void ItInitializesOpenAIFunctionReturnParameterCorrectly(string? description, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new AzureOpenAIFunctionReturnParameter(description, typeof(string), schema); + + // Assert + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithNoPluginName() + { + // Arrange + AzureOpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToAzureOpenAIFunction(); + + // Act + FunctionDefinition result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal(sut.FunctionName, result.Name); + Assert.Equal(sut.Description, result.Description); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithNullParameters() + { + // Arrange + AzureOpenAIFunction sut = new("plugin", "function", "description", null, null); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.Parameters.ToString()); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithPluginName() + { + // Arrange + AzureOpenAIFunction sut = KernelPluginFactory.CreateFromFunctions("myplugin", new[] + { + KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.") + }).GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); + + // Act + FunctionDefinition result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal("myplugin-myfunc", result.Name); + Assert.Equal(sut.Description, result.Description); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() + { + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + + KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] + { + KernelFunctionFactory.CreateFromMethod( + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", + "TestFunction", + "My test function") + }); + + AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); + + FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + + var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); + var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters)); + + Assert.NotNull(functionDefinition); + Assert.Equal("Tests-TestFunction", functionDefinition.Name); + Assert.Equal("My test function", functionDefinition.Description); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() + { + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + + KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] + { + KernelFunctionFactory.CreateFromMethod( + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, + "TestFunction", + "My test function") + }); + + AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); + + FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + + Assert.NotNull(functionDefinition); + Assert.Equal("Tests-TestFunction", functionDefinition.Name); + Assert.Equal("My test function", functionDefinition.Description); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() + { + // Arrange + AzureOpenAIFunction f = KernelFunctionFactory.CreateFromMethod( + () => { }, + parameters: [new KernelParameterMetadata("param1")]).Metadata.ToAzureOpenAIFunction(); + + // Act + FunctionDefinition result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + + // Assert + Assert.NotNull(pd.properties); + Assert.Single(pd.properties); + Assert.Equal( + JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string" }""")), + JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescriptions() + { + // Arrange + AzureOpenAIFunction f = KernelFunctionFactory.CreateFromMethod( + () => { }, + parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToAzureOpenAIFunction(); + + // Act + FunctionDefinition result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + + // Assert + Assert.NotNull(pd.properties); + Assert.Single(pd.properties); + Assert.Equal( + JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")), + JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); + } + +#pragma warning disable CA1812 // uninstantiated internal class + private sealed class ParametersData + { + public string? type { get; set; } + public string[]? required { get; set; } + public Dictionary? properties { get; set; } + } +#pragma warning restore CA1812 +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs new file mode 100644 index 000000000000..ebf7b67a2f9b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.Linq; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +#pragma warning disable CA1812 // Uninstantiated internal types + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; + +public sealed class KernelFunctionMetadataExtensionsTests +{ + [Fact] + public void ItCanConvertToAzureOpenAIFunctionNoParameters() + { + // Arrange + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToAzureOpenAIFunction(); + + // Assert + Assert.Equal(sut.Name, result.FunctionName); + Assert.Equal(sut.PluginName, result.PluginName); + Assert.Equal(sut.Description, result.Description); + Assert.Equal($"{sut.PluginName}-{sut.Name}", result.FullyQualifiedName); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionNoPluginName() + { + // Arrange + var sut = new KernelFunctionMetadata("foo") + { + PluginName = string.Empty, + Description = "baz", + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToAzureOpenAIFunction(); + + // Assert + Assert.Equal(sut.Name, result.FunctionName); + Assert.Equal(sut.PluginName, result.PluginName); + Assert.Equal(sut.Description, result.Description); + Assert.Equal(sut.Name, result.FullyQualifiedName); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ItCanConvertToAzureOpenAIFunctionWithParameter(bool withSchema) + { + // Arrange + var param1 = new KernelParameterMetadata("param1") + { + Description = "This is param1", + DefaultValue = "1", + ParameterType = typeof(int), + IsRequired = false, + Schema = withSchema ? KernelJsonSchema.Parse("""{"type":"integer"}""") : null, + }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToAzureOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal("This is param1 (default value: 1)", outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + Assert.NotNull(outputParam.Schema); + Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionWithParameterNoType() + { + // Arrange + var param1 = new KernelParameterMetadata("param1") { Description = "This is param1" }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToAzureOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal(param1.Description, outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionWithNoReturnParameterType() + { + // Arrange + var param1 = new KernelParameterMetadata("param1") + { + Description = "This is param1", + ParameterType = typeof(int), + }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + }; + + // Act + var result = sut.ToAzureOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal(param1.Description, outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + Assert.NotNull(outputParam.Schema); + Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); + } + + [Fact] + public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromType("MyPlugin"); + + var functionMetadata = kernel.Plugins["MyPlugin"].First().Metadata; + + var sut = functionMetadata.ToAzureOpenAIFunction(); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Equal( + """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", + result.Parameters.ToString() + ); + } + + [Fact] + public void ItCanCreateValidAzureOpenAIFunctionManualForPrompt() + { + // Arrange + var promptTemplateConfig = new PromptTemplateConfig("Hello AI") + { + Description = "My sample function." + }; + promptTemplateConfig.InputVariables.Add(new InputVariable + { + Name = "parameter1", + Description = "String parameter", + JsonSchema = """{"type":"string","description":"String parameter"}""" + }); + promptTemplateConfig.InputVariables.Add(new InputVariable + { + Name = "parameter2", + Description = "Enum parameter", + JsonSchema = """{"enum":["Value1","Value2"],"description":"Enum parameter"}""" + }); + var function = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig); + var functionMetadata = function.Metadata; + var sut = functionMetadata.ToAzureOpenAIFunction(); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Equal( + """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", + result.Parameters.ToString() + ); + } + + private enum MyEnum + { + Value1, + Value2 + } + + private sealed class MyPlugin + { + [KernelFunction, Description("My sample function.")] + public string MyFunction( + [Description("String parameter")] string parameter1, + [Description("Enum parameter")] MyEnum parameter2, + [Description("DateTime parameter")] DateTime parameter3 + ) + { + return "return"; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs new file mode 100644 index 000000000000..0af66de6a519 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Threading; +using System.Threading.Tasks; + +namespace SemanticKernel.Connectors.AzureOpenAI; + +internal sealed class MultipleHttpMessageHandlerStub : DelegatingHandler +{ + private int _callIteration = 0; + + public List RequestHeaders { get; private set; } + + public List ContentHeaders { get; private set; } + + public List RequestContents { get; private set; } + + public List RequestUris { get; private set; } + + public List Methods { get; private set; } + + public List ResponsesToReturn { get; set; } + + public MultipleHttpMessageHandlerStub() + { + this.RequestHeaders = []; + this.ContentHeaders = []; + this.RequestContents = []; + this.RequestUris = []; + this.Methods = []; + this.ResponsesToReturn = []; + } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this._callIteration++; + + this.Methods.Add(request.Method); + this.RequestUris.Add(request.RequestUri); + this.RequestHeaders.Add(request.Headers); + this.ContentHeaders.Add(request.Content?.Headers); + + var content = request.Content is null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken); + + this.RequestContents.Add(content); + + return await Task.FromResult(this.ResponsesToReturn[this._callIteration - 1]); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json new file mode 100644 index 000000000000..737b972309ba --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json @@ -0,0 +1,64 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + }, + { + "id": "2", + "type": "function", + "function": { + "name": "MyPlugin-FunctionWithException", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "3", + "type": "function", + "function": { + "name": "MyPlugin-NonExistentFunction", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "4", + "type": "function", + "function": { + "name": "MyPlugin-InvalidArguments", + "arguments": "invalid_arguments_format" + } + }, + { + "id": "5", + "type": "function", + "function": { + "name": "MyPlugin-IntArguments", + "arguments": "{\n\"age\": 36\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json new file mode 100644 index 000000000000..6c93e434f259 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json @@ -0,0 +1,32 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt new file mode 100644 index 000000000000..ceb8f3e8b44b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt @@ -0,0 +1,9 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin-FunctionWithException","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":2,"id":"3","type":"function","function":{"name":"MyPlugin-NonExistentFunction","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":3,"id":"4","type":"function","function":{"name":"MyPlugin-InvalidArguments","arguments":"invalid_arguments_format"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt new file mode 100644 index 000000000000..6835039941ce --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt new file mode 100644 index 000000000000..e5e8d1b19afd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt @@ -0,0 +1,5 @@ +data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{"content":"Test chat streaming response"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_test_response.json new file mode 100644 index 000000000000..b601bac8b55b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_test_response.json @@ -0,0 +1,22 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1704208954, + "model": "gpt-4", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Test chat response" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + }, + "system_fingerprint": null +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt new file mode 100644 index 000000000000..5e17403da9fc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt @@ -0,0 +1 @@ +data: {"id":"response-id","model":"","created":1684304924,"object":"chat.completion","choices":[{"index":0,"messages":[{"delta":{"role":"assistant","content":"Test chat with data streaming response"},"end_turn":false}]}]} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json new file mode 100644 index 000000000000..40d769dac8a7 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json @@ -0,0 +1,28 @@ +{ + "id": "response-id", + "model": "", + "created": 1684304924, + "object": "chat.completion", + "choices": [ + { + "index": 0, + "messages": [ + { + "role": "tool", + "content": "{\"citations\": [{\"content\": \"\\nAzure AI services are cloud-based artificial intelligence (AI) services...\", \"id\": null, \"title\": \"What is Azure AI services\", \"filepath\": null, \"url\": null, \"metadata\": {\"chunking\": \"original document size=250. Scores=0.4314117431640625 and 1.72564697265625.Org Highlight count=4.\"}, \"chunk_id\": \"0\"}], \"intent\": \"[\\\"Learn about Azure AI services.\\\"]\"}", + "end_turn": false + }, + { + "role": "assistant", + "content": "Test chat with data response", + "end_turn": true + } + ] + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json new file mode 100644 index 000000000000..3ffa6b00cc3f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json @@ -0,0 +1,40 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-Function1", + "arguments": "{\n\"parameter\": \"function1-value\"\n}" + } + }, + { + "id": "2", + "type": "function", + "function": { + "name": "MyPlugin-Function2", + "arguments": "{\n\"parameter\": \"function2-value\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt new file mode 100644 index 000000000000..c8aeb98e8b82 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt @@ -0,0 +1,5 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-Function1","arguments":"{\n\"parameter\": \"function1-value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin-Function2","arguments":"{\n\"parameter\": \"function2-value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_streaming_test_response.txt new file mode 100644 index 000000000000..a511ea446236 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_streaming_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"text_completion","created":1646932609,"model":"ada","choices":[{"text":"Test chat streaming response","index":0,"logprobs":null,"finish_reason":"length"}],"usage":{"prompt_tokens":55,"completion_tokens":100,"total_tokens":155}} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_test_response.json b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_test_response.json new file mode 100644 index 000000000000..540229437440 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text_completion_test_response.json @@ -0,0 +1,19 @@ +{ + "id": "response-id", + "object": "text_completion", + "created": 1646932609, + "model": "ada", + "choices": [ + { + "text": "Test chat response", + "index": 0, + "logprobs": null, + "finish_reason": "length" + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs new file mode 100644 index 000000000000..8303b2ceaeaf --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Azure.Core; +using Azure.Core.Pipeline; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Helper class to inject headers into Azure SDK HTTP pipeline +/// +internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : HttpPipelineSynchronousPolicy +{ + private readonly string _headerName = headerName; + private readonly string _headerValue = headerValue; + + public override void OnSendingRequest(HttpMessage message) + { + message.Request.Headers.Add(this._headerName, this._headerValue); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs new file mode 100644 index 000000000000..69c305f58f34 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs @@ -0,0 +1,432 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Execution settings for an AzureOpenAI completion request. +/// +[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] +public sealed class AzureOpenAIPromptExecutionSettings : PromptExecutionSettings +{ + /// + /// Temperature controls the randomness of the completion. + /// The higher the temperature, the more random the completion. + /// Default is 1.0. + /// + [JsonPropertyName("temperature")] + public double Temperature + { + get => this._temperature; + + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// TopP controls the diversity of the completion. + /// The higher the TopP, the more diverse the completion. + /// Default is 1.0. + /// + [JsonPropertyName("top_p")] + public double TopP + { + get => this._topP; + + set + { + this.ThrowIfFrozen(); + this._topP = value; + } + } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens + /// based on whether they appear in the text so far, increasing the + /// model's likelihood to talk about new topics. + /// + [JsonPropertyName("presence_penalty")] + public double PresencePenalty + { + get => this._presencePenalty; + + set + { + this.ThrowIfFrozen(); + this._presencePenalty = value; + } + } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens + /// based on their existing frequency in the text so far, decreasing + /// the model's likelihood to repeat the same line verbatim. + /// + [JsonPropertyName("frequency_penalty")] + public double FrequencyPenalty + { + get => this._frequencyPenalty; + + set + { + this.ThrowIfFrozen(); + this._frequencyPenalty = value; + } + } + + /// + /// The maximum number of tokens to generate in the completion. + /// + [JsonPropertyName("max_tokens")] + public int? MaxTokens + { + get => this._maxTokens; + + set + { + this.ThrowIfFrozen(); + this._maxTokens = value; + } + } + + /// + /// Sequences where the completion will stop generating further tokens. + /// + [JsonPropertyName("stop_sequences")] + public IList? StopSequences + { + get => this._stopSequences; + + set + { + this.ThrowIfFrozen(); + this._stopSequences = value; + } + } + + /// + /// How many completions to generate for each prompt. Default is 1. + /// Note: Because this parameter generates many completions, it can quickly consume your token quota. + /// Use carefully and ensure that you have reasonable settings for max_tokens and stop. + /// + [JsonPropertyName("results_per_prompt")] + public int ResultsPerPrompt + { + get => this._resultsPerPrompt; + + set + { + this.ThrowIfFrozen(); + this._resultsPerPrompt = value; + } + } + + /// + /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the + /// same seed and parameters should return the same result. Determinism is not guaranteed. + /// + [JsonPropertyName("seed")] + public long? Seed + { + get => this._seed; + + set + { + this.ThrowIfFrozen(); + this._seed = value; + } + } + + /// + /// Gets or sets the response format to use for the completion. + /// + /// + /// Possible values are: "json_object", "text", object. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("response_format")] + public object? ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The system prompt to use when generating text using a chat model. + /// Defaults to "Assistant is a large language model." + /// + [JsonPropertyName("chat_system_prompt")] + public string? ChatSystemPrompt + { + get => this._chatSystemPrompt; + + set + { + this.ThrowIfFrozen(); + this._chatSystemPrompt = value; + } + } + + /// + /// Modify the likelihood of specified tokens appearing in the completion. + /// + [JsonPropertyName("token_selection_biases")] + public IDictionary? TokenSelectionBiases + { + get => this._tokenSelectionBiases; + + set + { + this.ThrowIfFrozen(); + this._tokenSelectionBiases = value; + } + } + + /// + /// Gets or sets the behavior for how tool calls are handled. + /// + /// + /// + /// To disable all tool calling, set the property to null (the default). + /// + /// To request that the model use a specific function, set the property to an instance returned + /// from . + /// + /// + /// To allow the model to request one of any number of functions, set the property to an + /// instance returned from , called with + /// a list of the functions available. + /// + /// + /// To allow the model to request one of any of the functions in the supplied , + /// set the property to if the client should simply + /// send the information about the functions and not handle the response in any special manner, or + /// if the client should attempt to automatically + /// invoke the function and send the result back to the service. + /// + /// + /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service + /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to + /// resolve that function from the functions available in the , and if found, rather + /// than returning the response back to the caller, it will handle the request automatically, invoking + /// the function, and sending back the result. The intermediate messages will be retained in the + /// if an instance was provided. + /// + public AzureToolCallBehavior? ToolCallBehavior + { + get => this._toolCallBehavior; + + set + { + this.ThrowIfFrozen(); + this._toolCallBehavior = value; + } + } + + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse + /// + public string? User + { + get => this._user; + + set + { + this.ThrowIfFrozen(); + this._user = value; + } + } + + /// + /// Whether to return log probabilities of the output tokens or not. + /// If true, returns the log probabilities of each output token returned in the `content` of `message`. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("logprobs")] + public bool? Logprobs + { + get => this._logprobs; + + set + { + this.ThrowIfFrozen(); + this._logprobs = value; + } + } + + /// + /// An integer specifying the number of most likely tokens to return at each token position, each with an associated log probability. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("top_logprobs")] + public int? TopLogprobs + { + get => this._topLogprobs; + + set + { + this.ThrowIfFrozen(); + this._topLogprobs = value; + } + } + + /// + /// An abstraction of additional settings for chat completion, see https://learn.microsoft.com/en-us/dotnet/api/azure.ai.openai.azurechatextensionsoptions. + /// This property is compatible only with Azure OpenAI. + /// + [Experimental("SKEXP0010")] + [JsonIgnore] + public AzureChatExtensionsOptions? AzureChatExtensionsOptions + { + get => this._azureChatExtensionsOptions; + + set + { + this.ThrowIfFrozen(); + this._azureChatExtensionsOptions = value; + } + } + + /// + public override void Freeze() + { + if (this.IsFrozen) + { + return; + } + + base.Freeze(); + + if (this._stopSequences is not null) + { + this._stopSequences = new ReadOnlyCollection(this._stopSequences); + } + + if (this._tokenSelectionBiases is not null) + { + this._tokenSelectionBiases = new ReadOnlyDictionary(this._tokenSelectionBiases); + } + } + + /// + public override PromptExecutionSettings Clone() + { + return new AzureOpenAIPromptExecutionSettings() + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + TopP = this.TopP, + PresencePenalty = this.PresencePenalty, + FrequencyPenalty = this.FrequencyPenalty, + MaxTokens = this.MaxTokens, + StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, + ResultsPerPrompt = this.ResultsPerPrompt, + Seed = this.Seed, + ResponseFormat = this.ResponseFormat, + TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, + ToolCallBehavior = this.ToolCallBehavior, + User = this.User, + ChatSystemPrompt = this.ChatSystemPrompt, + Logprobs = this.Logprobs, + TopLogprobs = this.TopLogprobs, + AzureChatExtensionsOptions = this.AzureChatExtensionsOptions, + }; + } + + /// + /// Default max tokens for a text generation + /// + internal static int DefaultTextMaxTokens { get; } = 256; + + /// + /// Create a new settings object with the values from another settings object. + /// + /// Template configuration + /// Default max tokens + /// An instance of OpenAIPromptExecutionSettings + public static AzureOpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) + { + if (executionSettings is null) + { + return new AzureOpenAIPromptExecutionSettings() + { + MaxTokens = defaultMaxTokens + }; + } + + if (executionSettings is AzureOpenAIPromptExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + if (openAIExecutionSettings is not null) + { + return openAIExecutionSettings; + } + + throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIPromptExecutionSettings)}", nameof(executionSettings)); + } + + /// + /// Create a new settings object with the values from another settings object. + /// + /// Template configuration + /// Default max tokens + /// An instance of OpenAIPromptExecutionSettings + [Obsolete("This method is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] + public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) + { + var settings = FromExecutionSettings(executionSettings, defaultMaxTokens); + + if (settings.StopSequences?.Count == 0) + { + // Azure OpenAI WithData API does not allow to send empty array of stop sequences + // Gives back "Validation error at #/stop/str: Input should be a valid string\nValidation error at #/stop/list[str]: List should have at least 1 item after validation, not 0" + settings.StopSequences = null; + } + + return settings; + } + + #region private ================================================================================ + + private double _temperature = 1; + private double _topP = 1; + private double _presencePenalty; + private double _frequencyPenalty; + private int? _maxTokens; + private IList? _stopSequences; + private int _resultsPerPrompt = 1; + private long? _seed; + private object? _responseFormat; + private IDictionary? _tokenSelectionBiases; + private AzureToolCallBehavior? _toolCallBehavior; + private string? _user; + private string? _chatSystemPrompt; + private bool? _logprobs; + private int? _topLogprobs; + private AzureChatExtensionsOptions? _azureChatExtensionsOptions; + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs new file mode 100644 index 000000000000..4c3baef49268 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; +using System.Text.Json; +using Azure.AI.OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// Represents a behavior for Azure OpenAI tool calls. +public abstract class AzureToolCallBehavior +{ + // NOTE: Right now, the only tools that are available are for function calling. In the future, + // this class can be extended to support additional kinds of tools, including composite ones: + // the OpenAIPromptExecutionSettings has a single ToolCallBehavior property, but we could + // expose a `public static ToolCallBehavior Composite(params ToolCallBehavior[] behaviors)` + // or the like to allow multiple distinct tools to be provided, should that be appropriate. + // We can also consider additional forms of tools, such as ones that dynamically examine + // the Kernel, KernelArguments, etc., and dynamically contribute tools to the ChatCompletionsOptions. + + /// + /// The default maximum number of tool-call auto-invokes that can be made in a single request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled (e.g. will behave like )). + /// This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. It is currently hardcoded, but in the future it could + /// be made configurable by the developer. Other configuration is also possible in the future, + /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure + /// to find the requested function, failure to invoke the function, etc.), with behaviors for + /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call + /// support, where the model can request multiple tools in a single response, it is significantly + /// less likely that this limit is reached, as most of the time only a single request is needed. + /// + private const int DefaultMaximumAutoInvokeAttempts = 128; + + /// + /// Gets an instance that will provide all of the 's plugins' function information. + /// Function call requests from the model will be propagated back to the caller. + /// + /// + /// If no is available, no function information will be provided to the model. + /// + public static AzureToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); + + /// + /// Gets an instance that will both provide all of the 's plugins' function information + /// to the model and attempt to automatically handle any function call requests. + /// + /// + /// When successful, tool call requests from the model become an implementation detail, with the service + /// handling invoking any requested functions and supplying the results back to the model. + /// If no is available, no function information will be provided to the model. + /// + public static AzureToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); + + /// Gets an instance that will provide the specified list of functions to the model. + /// The functions that should be made available to the model. + /// true to attempt to automatically handle function call requests; otherwise, false. + /// + /// The that may be set into + /// to indicate that the specified functions should be made available to the model. + /// + public static AzureToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) + { + Verify.NotNull(functions); + return new EnabledFunctions(functions, autoInvoke); + } + + /// Gets an instance that will request the model to use the specified function. + /// The function the model should request to use. + /// true to attempt to automatically handle function call requests; otherwise, false. + /// + /// The that may be set into + /// to indicate that the specified function should be requested by the model. + /// + public static AzureToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) + { + Verify.NotNull(function); + return new RequiredFunction(function, autoInvoke); + } + + /// Initializes the instance; prevents external instantiation. + private AzureToolCallBehavior(bool autoInvoke) + { + this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; + } + + /// + /// Options to control tool call result serialization behavior. + /// + [Obsolete("This property is deprecated in favor of Kernel.SerializerOptions that will be introduced in one of the following releases.")] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } + + /// Gets how many requests are part of a single interaction should include this tool in the request. + /// + /// This should be greater than or equal to . It defaults to . + /// Once this limit is reached, the tools will no longer be included in subsequent retries as part of the operation, e.g. + /// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result + /// will not include the tools for further use. + /// + internal virtual int MaximumUseAttempts => int.MaxValue; + + /// Gets how many tool call request/response roundtrips are supported with auto-invocation. + /// + /// To disable auto invocation, this can be set to 0. + /// + internal int MaximumAutoInvokeAttempts { get; } + + /// + /// Gets whether validation against a specified list is required before allowing the model to request a function from the kernel. + /// + /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. + internal virtual bool AllowAnyRequestedKernelFunction => false; + + /// Configures the with any tools this provides. + /// The used for the operation. This can be queried to determine what tools to provide into the . + /// The destination to configure. + internal abstract void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options); + + /// + /// Represents a that will provide to the model all available functions from a + /// provided by the client. Setting this will have no effect if no is provided. + /// + internal sealed class KernelFunctions : AzureToolCallBehavior + { + internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } + + public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; + + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + { + // If no kernel is provided, we don't have any tools to provide. + if (kernel is not null) + { + // Provide all functions from the kernel. + IList functions = kernel.Plugins.GetFunctionsMetadata(); + if (functions.Count > 0) + { + options.ToolChoice = ChatCompletionsToolChoice.Auto; + for (int i = 0; i < functions.Count; i++) + { + options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition())); + } + } + } + } + + internal override bool AllowAnyRequestedKernelFunction => true; + } + + /// + /// Represents a that provides a specified list of functions to the model. + /// + internal sealed class EnabledFunctions : AzureToolCallBehavior + { + private readonly AzureOpenAIFunction[] _openAIFunctions; + private readonly ChatCompletionsFunctionToolDefinition[] _functions; + + public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) + { + this._openAIFunctions = functions.ToArray(); + + var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; + for (int i = 0; i < defs.Length; i++) + { + defs[i] = new ChatCompletionsFunctionToolDefinition(this._openAIFunctions[i].ToFunctionDefinition()); + } + this._functions = defs; + } + + public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.Name))}"; + + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + { + AzureOpenAIFunction[] openAIFunctions = this._openAIFunctions; + ChatCompletionsFunctionToolDefinition[] functions = this._functions; + Debug.Assert(openAIFunctions.Length == functions.Length); + + if (openAIFunctions.Length > 0) + { + bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && kernel is null) + { + throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); + } + + options.ToolChoice = ChatCompletionsToolChoice.Auto; + for (int i = 0; i < openAIFunctions.Length; i++) + { + // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. + if (autoInvoke) + { + Debug.Assert(kernel is not null); + AzureOpenAIFunction f = openAIFunctions[i]; + if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) + { + throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); + } + } + + // Add the function. + options.Tools.Add(functions[i]); + } + } + } + } + + /// Represents a that requests the model use a specific function. + internal sealed class RequiredFunction : AzureToolCallBehavior + { + private readonly AzureOpenAIFunction _function; + private readonly ChatCompletionsFunctionToolDefinition _tool; + private readonly ChatCompletionsToolChoice _choice; + + public RequiredFunction(AzureOpenAIFunction function, bool autoInvoke) : base(autoInvoke) + { + this._function = function; + this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); + this._choice = new ChatCompletionsToolChoice(this._tool); + } + + public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.Name}"; + + internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + { + bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && kernel is null) + { + throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); + } + + // Make sure that if auto-invocation is specified, the required function can be found in the kernel. + if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) + { + throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); + } + + options.ToolChoice = this._choice; + options.Tools.Add(this._tool); + } + + /// Gets how many requests are part of a single interaction should include this tool in the request. + /// + /// Unlike and , this must use 1 as the maximum + /// use attempts. Otherwise, every call back to the model _requires_ it to invoke the function (as opposed + /// to allows it), which means we end up doing the same work over and over and over until the maximum is reached. + /// Thus for "requires", we must send the tool information only once. + /// + internal override int MaximumUseAttempts => 1; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs new file mode 100644 index 000000000000..e478a301d947 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextGeneration; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI chat completion service. +/// +public sealed class AzureOpenAIChatCompletionService : IChatCompletionService, ITextGenerationService +{ + /// Core implementation shared by Azure OpenAI clients. + private readonly AzureOpenAIClientCore _core; + + /// + /// Create an instance of the connector with API key auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIChatCompletionService( + string deploymentName, + string endpoint, + string apiKey, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + /// Create an instance of the connector with AAD auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIChatCompletionService( + string deploymentName, + string endpoint, + TokenCredential credentials, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + /// Creates a new client instance using the specified . + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIChatCompletionService( + string deploymentName, + OpenAIClient openAIClient, + string? modelId = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + public IReadOnlyDictionary Attributes => this._core.Attributes; + + /// + public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + => this._core.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + + /// + public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + => this._core.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + + /// + public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + => this._core.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); + + /// + public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + => this._core.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs new file mode 100644 index 000000000000..23412f666e23 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace Microsoft.SemanticKernel; + +/// +/// Chat history extensions. +/// +public static class ChatHistoryExtensions +{ + /// + /// Add a message to the chat history at the end of the streamed message + /// + /// Target chat history + /// list of streaming message contents + /// Returns the original streaming results with some message processing + [Experimental("SKEXP0010")] + public static async IAsyncEnumerable AddStreamingMessageAsync(this ChatHistory chatHistory, IAsyncEnumerable streamingMessageContents) + { + List messageContents = []; + + // Stream the response. + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + Dictionary? metadata = null; + AuthorRole? streamedRole = null; + string? streamedName = null; + + await foreach (var chatMessage in streamingMessageContents.ConfigureAwait(false)) + { + metadata ??= (Dictionary?)chatMessage.Metadata; + + if (chatMessage.Content is { Length: > 0 } contentUpdate) + { + (contentBuilder ??= new()).Append(contentUpdate); + } + + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + // Is always expected to have at least one chunk with the role provided from a streaming message + streamedRole ??= chatMessage.Role; + streamedName ??= chatMessage.AuthorName; + + messageContents.Add(chatMessage); + yield return chatMessage; + } + + if (messageContents.Count != 0) + { + var role = streamedRole ?? AuthorRole.Assistant; + + chatHistory.Add( + new AzureOpenAIChatMessageContent( + role, + contentBuilder?.ToString() ?? string.Empty, + messageContents[0].ModelId!, + AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), + metadata) + { AuthorName = streamedName }); + } + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 837dd5b3c1db..8e8f53594708 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,7 +21,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs new file mode 100644 index 000000000000..8cbecc909951 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// AzureOpenAI specialized chat message content +/// +public sealed class AzureOpenAIChatMessageContent : ChatMessageContent +{ + /// + /// Gets the metadata key for the name property. + /// + public static string ToolIdProperty => $"{nameof(ChatCompletionsToolCall)}.{nameof(ChatCompletionsToolCall.Id)}"; + + /// + /// Gets the metadata key for the list of . + /// + internal static string FunctionToolCallsProperty => $"{nameof(ChatResponseMessage)}.FunctionToolCalls"; + + /// + /// Initializes a new instance of the class. + /// + internal AzureOpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelId, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, CreateMetadataDictionary(chatMessage.ToolCalls, metadata)) + { + this.ToolCalls = chatMessage.ToolCalls; + } + + /// + /// Initializes a new instance of the class. + /// + internal AzureOpenAIChatMessageContent(ChatRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) + { + this.ToolCalls = toolCalls; + } + + /// + /// Initializes a new instance of the class. + /// + internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) + { + this.ToolCalls = toolCalls; + } + + /// + /// A list of the tools called by the model. + /// + public IReadOnlyList ToolCalls { get; } + + /// + /// Retrieve the resulting function from the chat result. + /// + /// The , or null if no function was returned by the model. + public IReadOnlyList GetOpenAIFunctionToolCalls() + { + List? functionToolCallList = null; + + foreach (var toolCall in this.ToolCalls) + { + if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + { + (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(functionToolCall)); + } + } + + if (functionToolCallList is not null) + { + return functionToolCallList; + } + + return []; + } + + private static IReadOnlyDictionary? CreateMetadataDictionary( + IReadOnlyList toolCalls, + IReadOnlyDictionary? original) + { + // We only need to augment the metadata if there are any tool calls. + if (toolCalls.Count > 0) + { + Dictionary newDictionary; + if (original is null) + { + // There's no existing metadata to clone; just allocate a new dictionary. + newDictionary = new Dictionary(1); + } + else if (original is IDictionary origIDictionary) + { + // Efficiently clone the old dictionary to a new one. + newDictionary = new Dictionary(origIDictionary); + } + else + { + // There's metadata to clone but we have to do so one item at a time. + newDictionary = new Dictionary(original.Count + 1); + foreach (var kvp in original) + { + newDictionary[kvp.Key] = kvp.Value; + } + } + + // Add the additional entry. + newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); + + return newDictionary; + } + + return original; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs new file mode 100644 index 000000000000..e34b191a83b8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using Azure; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Core implementation for Azure OpenAI clients, providing common functionality and properties. +/// +internal sealed class AzureOpenAIClientCore : ClientCore +{ + /// + /// Gets the key used to store the deployment name in the dictionary. + /// + public static string DeploymentNameKey => "DeploymentName"; + + /// + /// OpenAI / Azure OpenAI Client + /// + internal override OpenAIClient Client { get; } + + /// + /// Initializes a new instance of the class using API Key authentication. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + internal AzureOpenAIClientCore( + string deploymentName, + string endpoint, + string apiKey, + HttpClient? httpClient = null, + ILogger? logger = null) : base(logger) + { + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); + Verify.NotNullOrWhiteSpace(apiKey); + + var options = GetOpenAIClientOptions(httpClient); + + this.DeploymentOrModelName = deploymentName; + this.Endpoint = new Uri(endpoint); + this.Client = new OpenAIClient(this.Endpoint, new AzureKeyCredential(apiKey), options); + } + + /// + /// Initializes a new instance of the class supporting AAD authentication. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + internal AzureOpenAIClientCore( + string deploymentName, + string endpoint, + TokenCredential credential, + HttpClient? httpClient = null, + ILogger? logger = null) : base(logger) + { + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); + + var options = GetOpenAIClientOptions(httpClient); + + this.DeploymentOrModelName = deploymentName; + this.Endpoint = new Uri(endpoint); + this.Client = new OpenAIClient(this.Endpoint, credential, options); + } + + /// + /// Initializes a new instance of the class using the specified OpenAIClient. + /// Note: instances created this way might not have the default diagnostics settings, + /// it's up to the caller to configure the client. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// The to use for logging. If null, no logging will be performed. + internal AzureOpenAIClientCore( + string deploymentName, + OpenAIClient openAIClient, + ILogger? logger = null) : base(logger) + { + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNull(openAIClient); + + this.DeploymentOrModelName = deploymentName; + this.Client = openAIClient; + + this.AddAttribute(DeploymentNameKey, deploymentName); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs new file mode 100644 index 000000000000..4a3cff49103d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Represents a function parameter that can be passed to an AzureOpenAI function tool call. +/// +public sealed class AzureOpenAIFunctionParameter +{ + internal AzureOpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) + { + this.Name = name ?? string.Empty; + this.Description = description ?? string.Empty; + this.IsRequired = isRequired; + this.ParameterType = parameterType; + this.Schema = schema; + } + + /// Gets the name of the parameter. + public string Name { get; } + + /// Gets a description of the parameter. + public string Description { get; } + + /// Gets whether the parameter is required vs optional. + public bool IsRequired { get; } + + /// Gets the of the parameter, if known. + public Type? ParameterType { get; } + + /// Gets a JSON schema for the parameter, if known. + public KernelJsonSchema? Schema { get; } +} + +/// +/// Represents a function return parameter that can be returned by a tool call to AzureOpenAI. +/// +public sealed class AzureOpenAIFunctionReturnParameter +{ + internal AzureOpenAIFunctionReturnParameter(string? description, Type? parameterType, KernelJsonSchema? schema) + { + this.Description = description ?? string.Empty; + this.Schema = schema; + this.ParameterType = parameterType; + } + + /// Gets a description of the return parameter. + public string Description { get; } + + /// Gets the of the return parameter, if known. + public Type? ParameterType { get; } + + /// Gets a JSON schema for the return parameter, if known. + public KernelJsonSchema? Schema { get; } +} + +/// +/// Represents a function that can be passed to the AzureOpenAI API +/// +public sealed class AzureOpenAIFunction +{ + /// + /// Cached storing the JSON for a function with no parameters. + /// + /// + /// This is an optimization to avoid serializing the same JSON Schema over and over again + /// for this relatively common case. + /// + private static readonly BinaryData s_zeroFunctionParametersSchema = new("""{"type":"object","required":[],"properties":{}}"""); + /// + /// Cached schema for a descriptionless string. + /// + private static readonly KernelJsonSchema s_stringNoDescriptionSchema = KernelJsonSchema.Parse("""{"type":"string"}"""); + + /// Initializes the OpenAIFunction. + internal AzureOpenAIFunction( + string? pluginName, + string functionName, + string? description, + IReadOnlyList? parameters, + AzureOpenAIFunctionReturnParameter? returnParameter) + { + Verify.NotNullOrWhiteSpace(functionName); + + this.PluginName = pluginName; + this.FunctionName = functionName; + this.Description = description; + this.Parameters = parameters; + this.ReturnParameter = returnParameter; + } + + /// Gets the separator used between the plugin name and the function name, if a plugin name is present. + /// This separator was previously _, but has been changed to - to better align to the behavior elsewhere in SK and in response + /// to developers who want to use underscores in their function or plugin names. We plan to make this setting configurable in the future. + public static string NameSeparator { get; set; } = "-"; + + /// Gets the name of the plugin with which the function is associated, if any. + public string? PluginName { get; } + + /// Gets the name of the function. + public string FunctionName { get; } + + /// Gets the fully-qualified name of the function. + /// + /// This is the concatenation of the and the , + /// separated by . If there is no , this is + /// the same as . + /// + public string FullyQualifiedName => + string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{NameSeparator}{this.FunctionName}"; + + /// Gets a description of the function. + public string? Description { get; } + + /// Gets a list of parameters to the function, if any. + public IReadOnlyList? Parameters { get; } + + /// Gets the return parameter of the function, if any. + public AzureOpenAIFunctionReturnParameter? ReturnParameter { get; } + + /// + /// Converts the representation to the Azure SDK's + /// representation. + /// + /// A containing all the function information. + public FunctionDefinition ToFunctionDefinition() + { + BinaryData resultParameters = s_zeroFunctionParametersSchema; + + IReadOnlyList? parameters = this.Parameters; + if (parameters is { Count: > 0 }) + { + var properties = new Dictionary(); + var required = new List(); + + for (int i = 0; i < parameters.Count; i++) + { + var parameter = parameters[i]; + properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); + if (parameter.IsRequired) + { + required.Add(parameter.Name); + } + } + + resultParameters = BinaryData.FromObjectAsJson(new + { + type = "object", + required, + properties, + }); + } + + return new FunctionDefinition + { + Name = this.FullyQualifiedName, + Description = this.Description, + Parameters = resultParameters, + }; + } + + /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) + private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) + { + // If there's a description, incorporate it. + if (!string.IsNullOrWhiteSpace(description)) + { + return KernelJsonSchemaBuilder.Build(null, typeof(string), description); + } + + // Otherwise, we can use a cached schema for a string with no description. + return s_stringNoDescriptionSchema; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs new file mode 100644 index 000000000000..bea73a474d37 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using Azure.AI.OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Represents an AzureOpenAI function tool call with deserialized function name and arguments. +/// +public sealed class AzureOpenAIFunctionToolCall +{ + private string? _fullyQualifiedFunctionName; + + /// Initialize the from a . + internal AzureOpenAIFunctionToolCall(ChatCompletionsFunctionToolCall functionToolCall) + { + Verify.NotNull(functionToolCall); + Verify.NotNull(functionToolCall.Name); + + string fullyQualifiedFunctionName = functionToolCall.Name; + string functionName = fullyQualifiedFunctionName; + string? arguments = functionToolCall.Arguments; + string? pluginName = null; + + int separatorPos = fullyQualifiedFunctionName.IndexOf(AzureOpenAIFunction.NameSeparator, StringComparison.Ordinal); + if (separatorPos >= 0) + { + pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString(); + functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + AzureOpenAIFunction.NameSeparator.Length).Trim().ToString(); + } + + this.Id = functionToolCall.Id; + this._fullyQualifiedFunctionName = fullyQualifiedFunctionName; + this.PluginName = pluginName; + this.FunctionName = functionName; + if (!string.IsNullOrWhiteSpace(arguments)) + { + this.Arguments = JsonSerializer.Deserialize>(arguments!); + } + } + + /// Gets the ID of the tool call. + public string? Id { get; } + + /// Gets the name of the plugin with which this function is associated, if any. + public string? PluginName { get; } + + /// Gets the name of the function. + public string FunctionName { get; } + + /// Gets a name/value collection of the arguments to the function, if any. + public Dictionary? Arguments { get; } + + /// Gets the fully-qualified name of the function. + /// + /// This is the concatenation of the and the , + /// separated by . If there is no , + /// this is the same as . + /// + public string FullyQualifiedName => + this._fullyQualifiedFunctionName ??= + string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{AzureOpenAIFunction.NameSeparator}{this.FunctionName}"; + + /// + public override string ToString() + { + var sb = new StringBuilder(this.FullyQualifiedName); + + sb.Append('('); + if (this.Arguments is not null) + { + string separator = ""; + foreach (var arg in this.Arguments) + { + sb.Append(separator).Append(arg.Key).Append(':').Append(arg.Value); + separator = ", "; + } + } + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Tracks tooling updates from streaming responses. + /// + /// The tool call update to incorporate. + /// Lazily-initialized dictionary mapping indices to IDs. + /// Lazily-initialized dictionary mapping indices to names. + /// Lazily-initialized dictionary mapping indices to arguments. + internal static void TrackStreamingToolingUpdate( + StreamingToolCallUpdate? update, + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + if (update is null) + { + // Nothing to track. + return; + } + + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (update.Id is string id) + { + (toolCallIdsByIndex ??= [])[update.ToolCallIndex] = id; + } + + if (update is StreamingFunctionToolCallUpdate ftc) + { + // Ensure we're tracking the function's name. + if (ftc.Name is string name) + { + (functionNamesByIndex ??= [])[ftc.ToolCallIndex] = name; + } + + // Ensure we're tracking the function's arguments. + if (ftc.ArgumentsUpdate is string argumentsUpdate) + { + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(ftc.ToolCallIndex, out StringBuilder? arguments)) + { + functionArgumentBuildersByIndex[ftc.ToolCallIndex] = arguments = new(); + } + + arguments.Append(argumentsUpdate); + } + } + } + + /// + /// Converts the data built up by into an array of s. + /// + /// Dictionary mapping indices to IDs. + /// Dictionary mapping indices to names. + /// Dictionary mapping indices to arguments. + internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + ChatCompletionsFunctionToolCall[] toolCalls = []; + if (toolCallIdsByIndex is { Count: > 0 }) + { + toolCalls = new ChatCompletionsFunctionToolCall[toolCallIdsByIndex.Count]; + + int i = 0; + foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) + { + string? functionName = null; + StringBuilder? functionArguments = null; + + functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); + functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); + + toolCalls[i] = new ChatCompletionsFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); + i++; + } + + Debug.Assert(i == toolCalls.Length); + } + + return toolCalls; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs new file mode 100644 index 000000000000..30f796f82ae0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Extensions for specific to the AzureOpenAI connector. +/// +public static class AzureOpenAIKernelFunctionMetadataExtensions +{ + /// + /// Convert a to an . + /// + /// The object to convert. + /// An object. + public static AzureOpenAIFunction ToAzureOpenAIFunction(this KernelFunctionMetadata metadata) + { + IReadOnlyList metadataParams = metadata.Parameters; + + var openAIParams = new AzureOpenAIFunctionParameter[metadataParams.Count]; + for (int i = 0; i < openAIParams.Length; i++) + { + var param = metadataParams[i]; + + openAIParams[i] = new AzureOpenAIFunctionParameter( + param.Name, + GetDescription(param), + param.IsRequired, + param.ParameterType, + param.Schema); + } + + return new AzureOpenAIFunction( + metadata.PluginName, + metadata.Name, + metadata.Description, + openAIParams, + new AzureOpenAIFunctionReturnParameter( + metadata.ReturnParameter.Description, + metadata.ReturnParameter.ParameterType, + metadata.ReturnParameter.Schema)); + + static string GetDescription(KernelParameterMetadata param) + { + if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + return $"{param.Description} (default value: {stringValue})"; + } + + return param.Description; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs new file mode 100644 index 000000000000..c667183f773c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using Azure.AI.OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Extension methods for . +/// +public static class AzureOpenAIPluginCollectionExtensions +{ + /// + /// Given an object, tries to retrieve the corresponding and populate with its parameters. + /// + /// The plugins. + /// The object. + /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, + /// When this method returns, the arguments for the function; otherwise, + /// if the function was found; otherwise, . + public static bool TryGetFunctionAndArguments( + this IReadOnlyKernelPluginCollection plugins, + ChatCompletionsFunctionToolCall functionToolCall, + [NotNullWhen(true)] out KernelFunction? function, + out KernelArguments? arguments) => + plugins.TryGetFunctionAndArguments(new AzureOpenAIFunctionToolCall(functionToolCall), out function, out arguments); + + /// + /// Given an object, tries to retrieve the corresponding and populate with its parameters. + /// + /// The plugins. + /// The object. + /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, + /// When this method returns, the arguments for the function; otherwise, + /// if the function was found; otherwise, . + public static bool TryGetFunctionAndArguments( + this IReadOnlyKernelPluginCollection plugins, + AzureOpenAIFunctionToolCall functionToolCall, + [NotNullWhen(true)] out KernelFunction? function, + out KernelArguments? arguments) + { + if (plugins.TryGetFunction(functionToolCall.PluginName, functionToolCall.FunctionName, out function)) + { + // Add parameters to arguments + arguments = null; + if (functionToolCall.Arguments is not null) + { + arguments = []; + foreach (var parameter in functionToolCall.Arguments) + { + arguments[parameter.Key] = parameter.Value?.ToString(); + } + } + + return true; + } + + // Function not found in collection + arguments = null; + return false; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs new file mode 100644 index 000000000000..c1843b185f89 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI specialized streaming chat message content. +/// +/// +/// Represents a chat message content chunk that was streamed from the remote model. +/// +public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessageContent +{ + /// + /// The reason why the completion finished. + /// + public CompletionsFinishReason? FinishReason { get; set; } + + /// + /// Create a new instance of the class. + /// + /// Internal Azure SDK Message update representation + /// Index of the choice + /// The model ID used to generate the content + /// Additional metadata + internal AzureOpenAIStreamingChatMessageContent( + StreamingChatCompletionsUpdate chatUpdate, + int choiceIndex, + string modelId, + IReadOnlyDictionary? metadata = null) + : base( + chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, + chatUpdate.ContentUpdate, + chatUpdate, + choiceIndex, + modelId, + Encoding.UTF8, + metadata) + { + this.ToolCallUpdate = chatUpdate.ToolCallUpdate; + this.FinishReason = chatUpdate?.FinishReason; + } + + /// + /// Create a new instance of the class. + /// + /// Author role of the message + /// Content of the message + /// Tool call update + /// Completion finish reason + /// Index of the choice + /// The model ID used to generate the content + /// Additional metadata + internal AzureOpenAIStreamingChatMessageContent( + AuthorRole? authorRole, + string? content, + StreamingToolCallUpdate? tootToolCallUpdate = null, + CompletionsFinishReason? completionsFinishReason = null, + int choiceIndex = 0, + string? modelId = null, + IReadOnlyDictionary? metadata = null) + : base( + authorRole, + content, + null, + choiceIndex, + modelId, + Encoding.UTF8, + metadata) + { + this.ToolCallUpdate = tootToolCallUpdate; + this.FinishReason = completionsFinishReason; + } + + /// Gets any update information in the message about a tool call. + public StreamingToolCallUpdate? ToolCallUpdate { get; } + + /// + public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); + + /// + public override string ToString() => this.Content ?? string.Empty; +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs new file mode 100644 index 000000000000..9d9497fd68d5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI specialized streaming text content. +/// +/// +/// Represents a text content chunk that was streamed from the remote model. +/// +public sealed class AzureOpenAIStreamingTextContent : StreamingTextContent +{ + /// + /// Create a new instance of the class. + /// + /// Text update + /// Index of the choice + /// The model ID used to generate the content + /// Inner chunk object + /// Metadata information + internal AzureOpenAIStreamingTextContent( + string text, + int choiceIndex, + string modelId, + object? innerContentObject = null, + IReadOnlyDictionary? metadata = null) + : base( + text, + choiceIndex, + modelId, + innerContentObject, + Encoding.UTF8, + metadata) + { + } + + /// + public override byte[] ToByteArray() + { + return this.Encoding.GetBytes(this.ToString()); + } + + /// + public override string ToString() + { + return this.Text ?? string.Empty; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs new file mode 100644 index 000000000000..dda7578da8ea --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -0,0 +1,1574 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Net.Http; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure; +using Azure.AI.OpenAI; +using Azure.Core; +using Azure.Core.Pipeline; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.Http; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal abstract class ClientCore +{ + private const string ModelProvider = "openai"; + private const int MaxResultsPerPrompt = 128; + + /// + /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current + /// asynchronous chain of execution. + /// + /// + /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that + /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, + /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close + /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. + /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in + /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that + /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, + /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent + /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made + /// configurable should need arise. + /// + private const int MaxInflightAutoInvokes = 128; + + /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. + private static readonly ChatCompletionsFunctionToolDefinition s_nonInvocableFunctionTool = new() { Name = "NonInvocableTool" }; + + /// Tracking for . + private static readonly AsyncLocal s_inflightAutoInvokes = new(); + + internal ClientCore(ILogger? logger = null) + { + this.Logger = logger ?? NullLogger.Instance; + } + + /// + /// Model Id or Deployment Name + /// + internal string DeploymentOrModelName { get; set; } = string.Empty; + + /// + /// OpenAI / Azure OpenAI Client + /// + internal abstract OpenAIClient Client { get; } + + internal Uri? Endpoint { get; set; } = null; + + /// + /// Logger instance + /// + internal ILogger Logger { get; set; } + + /// + /// Storage for AI service attributes. + /// + internal Dictionary Attributes { get; } = []; + + /// + /// Instance of for metrics. + /// + private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); + + /// + /// Instance of to keep track of the number of prompt tokens used. + /// + private static readonly Counter s_promptTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.prompt", + unit: "{token}", + description: "Number of prompt tokens used"); + + /// + /// Instance of to keep track of the number of completion tokens used. + /// + private static readonly Counter s_completionTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.completion", + unit: "{token}", + description: "Number of completion tokens used"); + + /// + /// Instance of to keep track of the total number of tokens used. + /// + private static readonly Counter s_totalTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.total", + unit: "{token}", + description: "Number of tokens used"); + + /// + /// Creates completions for the prompt and settings. + /// + /// The prompt to complete. + /// Execution settings for the completion API. + /// The containing services, plugins, and other state for use throughout the operation. + /// The to monitor for cancellation requests. The default is . + /// Completions generated by the remote model + internal async Task> GetTextResultsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); + + ValidateMaxTokens(textExecutionSettings.MaxTokens); + + var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); + + Completions? responseData = null; + List responseContent; + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) + { + try + { + responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; + if (responseData.Choices.Count == 0) + { + throw new KernelException("Text completions not found"); + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + if (responseData != null) + { + // Capture available metadata even if the operation failed. + activity + .SetResponseId(responseData.Id) + .SetPromptTokenUsage(responseData.Usage.PromptTokens) + .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); + } + throw; + } + + responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); + activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); + } + + this.LogUsage(responseData.Usage); + + return responseContent; + } + + internal async IAsyncEnumerable GetStreamingTextContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); + + ValidateMaxTokens(textExecutionSettings.MaxTokens); + + var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); + + using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); + + StreamingResponse response; + try + { + response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); + List? streamedContents = activity is not null ? [] : null; + try + { + while (true) + { + try + { + if (!await responseEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + Completions completions = responseEnumerator.Current; + foreach (Choice choice in completions.Choices) + { + var openAIStreamingTextContent = new AzureOpenAIStreamingTextContent( + choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); + streamedContents?.Add(openAIStreamingTextContent); + yield return openAIStreamingTextContent; + } + } + } + finally + { + activity?.EndStreaming(streamedContents); + await responseEnumerator.DisposeAsync(); + } + } + + private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) + { + return new Dictionary(8) + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.Created), completions.Created }, + { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, + { nameof(completions.Usage), completions.Usage }, + { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, + + { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, + { nameof(choice.Index), choice.Index }, + }; + } + + private static Dictionary GetChatChoiceMetadata(ChatCompletions completions, ChatChoice chatChoice) + { + return new Dictionary(12) + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.Created), completions.Created }, + { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, + { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completions.Usage), completions.Usage }, + { nameof(chatChoice.ContentFilterResults), chatChoice.ContentFilterResults }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(chatChoice.FinishReason), chatChoice.FinishReason?.ToString() }, + + { nameof(chatChoice.FinishDetails), chatChoice.FinishDetails }, + { nameof(chatChoice.LogProbabilityInfo), chatChoice.LogProbabilityInfo }, + { nameof(chatChoice.Index), chatChoice.Index }, + { nameof(chatChoice.Enhancements), chatChoice.Enhancements }, + }; + } + + private static Dictionary GetResponseMetadata(StreamingChatCompletionsUpdate completions) + { + return new Dictionary(4) + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.Created), completions.Created }, + { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completions.FinishReason), completions.FinishReason?.ToString() }, + }; + } + + private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) + { + return new Dictionary(3) + { + { nameof(audioTranscription.Language), audioTranscription.Language }, + { nameof(audioTranscription.Duration), audioTranscription.Duration }, + { nameof(audioTranscription.Segments), audioTranscription.Segments } + }; + } + + /// + /// Generates an embedding from the given . + /// + /// List of strings to generate embeddings for + /// The containing services, plugins, and other state for use throughout the operation. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The to monitor for cancellation requests. The default is . + /// List of embeddings + internal async Task>> GetEmbeddingsAsync( + IList data, + Kernel? kernel, + int? dimensions, + CancellationToken cancellationToken) + { + var result = new List>(data.Count); + + if (data.Count > 0) + { + var embeddingsOptions = new EmbeddingsOptions(this.DeploymentOrModelName, data) + { + Dimensions = dimensions + }; + + var response = await RunRequestAsync(() => this.Client.GetEmbeddingsAsync(embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var embeddings = response.Value.Data; + + if (embeddings.Count != data.Count) + { + throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); + } + + for (var i = 0; i < embeddings.Count; i++) + { + result.Add(embeddings[i].Embedding); + } + } + + return result; + } + + //internal async Task> GetTextContentFromAudioAsync( + // AudioContent content, + // PromptExecutionSettings? executionSettings, + // CancellationToken cancellationToken) + //{ + // Verify.NotNull(content.Data); + // var audioData = content.Data.Value; + // if (audioData.IsEmpty) + // { + // throw new ArgumentException("Audio data cannot be empty", nameof(content)); + // } + + // OpenAIAudioToTextExecutionSettings? audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); + + // Verify.ValidFilename(audioExecutionSettings?.Filename); + + // var audioOptions = new AudioTranscriptionOptions + // { + // AudioData = BinaryData.FromBytes(audioData), + // DeploymentName = this.DeploymentOrModelName, + // Filename = audioExecutionSettings.Filename, + // Language = audioExecutionSettings.Language, + // Prompt = audioExecutionSettings.Prompt, + // ResponseFormat = audioExecutionSettings.ResponseFormat, + // Temperature = audioExecutionSettings.Temperature + // }; + + // AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioTranscriptionAsync(audioOptions, cancellationToken)).ConfigureAwait(false)).Value; + + // return [new(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData))]; + //} + + /// + /// Generate a new chat message + /// + /// Chat history + /// Execution settings for the completion API. + /// The containing services, plugins, and other state for use throughout the operation. + /// Async cancellation token + /// Generated chat message in string format + internal async Task> GetChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + // Convert the incoming execution settings to OpenAI settings. + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); + + // Create the Azure SDK ChatCompletionOptions instance from all available information. + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + + for (int requestIndex = 1; ; requestIndex++) + { + // Make the request. + ChatCompletions? responseData = null; + List responseContent; + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + { + try + { + responseData = (await RunRequestAsync(() => this.Client.GetChatCompletionsAsync(chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + this.LogUsage(responseData.Usage); + if (responseData.Choices.Count == 0) + { + throw new KernelException("Chat completions not found"); + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + if (responseData != null) + { + // Capture available metadata even if the operation failed. + activity + .SetResponseId(responseData.Id) + .SetPromptTokenUsage(responseData.Usage.PromptTokens) + .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); + } + throw; + } + + responseContent = responseData.Choices.Select(chatChoice => this.GetChatMessage(chatChoice, responseData)).ToList(); + activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); + } + + // If we don't want to attempt to invoke any functions, just return the result. + // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. + if (!autoInvoke || responseData.Choices.Count != 1) + { + return responseContent; + } + + Debug.Assert(kernel is not null); + + // Get our single result and extract the function call information. If this isn't a function call, or if it is + // but we're unable to find the function or extract the relevant information, just return the single result. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + ChatChoice resultChoice = responseData.Choices[0]; + AzureOpenAIChatMessageContent result = this.GetChatMessage(resultChoice, responseData); + if (result.ToolCalls.Count == 0) + { + return [result]; + } + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Tool requests: {Requests}", result.ToolCalls.Count); + } + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", result.ToolCalls.OfType().Select(ftc => $"{ftc.Name}({ftc.Arguments})"))); + } + + // Add the original assistant message to the chatOptions; this is required for the service + // to understand the tool call responses. Also add the result message to the caller's chat + // history: if they don't want it, they can remove it, but this makes the data available, + // including metadata like usage. + chatOptions.Messages.Add(GetRequestMessage(resultChoice.Message)); + chat.Add(result); + + // We must send back a response for every tool call, regardless of whether we successfully executed it or not. + // If we successfully execute it, we'll add the result. If we don't, we'll add an error. + for (int toolCallIndex = 0; toolCallIndex < result.ToolCalls.Count; toolCallIndex++) + { + ChatCompletionsToolCall toolCall = result.ToolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (toolCall is not ChatCompletionsFunctionToolCall functionToolCall) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + try + { + openAIFunctionToolCall = new(functionToolCall); + } + catch (JsonException) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex - 1, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = result.ToolCalls.Count + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatOptions, chat, null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + + // If filter requested termination, returning latest function result. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + return [chat.Last()]; + } + } + + // Update tool use information for the next go-around based on having completed another iteration. + Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); + + // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + chatOptions.Tools.Clear(); + + if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + } + else + { + // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented + // what functions are available in the kernel. + chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); + } + + // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") + // if we don't send any tools in subsequent requests, even if we say not to use any. + if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) + { + Debug.Assert(chatOptions.Tools.Count == 0); + chatOptions.Tools.Add(s_nonInvocableFunctionTool); + } + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + } + } + + internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); + + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + for (int requestIndex = 1; ; requestIndex++) + { + // Reset state + contentBuilder?.Clear(); + toolCallIdsByIndex?.Clear(); + functionNamesByIndex?.Clear(); + functionArgumentBuildersByIndex?.Clear(); + + // Stream the response. + IReadOnlyDictionary? metadata = null; + string? streamedName = null; + ChatRole? streamedRole = default; + CompletionsFinishReason finishReason = default; + ChatCompletionsFunctionToolCall[]? toolCalls = null; + FunctionCallContent[]? functionCallContents = null; + + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + { + // Make the request. + StreamingResponse response; + try + { + response = await RunRequestAsync(() => this.Client.GetChatCompletionsStreamingAsync(chatOptions, cancellationToken)).ConfigureAwait(false); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); + List? streamedContents = activity is not null ? [] : null; + try + { + while (true) + { + try + { + if (!await responseEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + StreamingChatCompletionsUpdate update = responseEnumerator.Current; + metadata = GetResponseMetadata(update); + streamedRole ??= update.Role; + streamedName ??= update.AuthorName; + finishReason = update.FinishReason ?? default; + + // If we're intending to invoke function calls, we need to consume that function call information. + if (autoInvoke) + { + if (update.ContentUpdate is { Length: > 0 } contentUpdate) + { + (contentBuilder ??= new()).Append(contentUpdate); + } + + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + } + + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, update.ChoiceIndex ?? 0, this.DeploymentOrModelName, metadata) { AuthorName = streamedName }; + + if (update.ToolCallUpdate is StreamingFunctionToolCallUpdate functionCallUpdate) + { + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( + callId: functionCallUpdate.Id, + name: functionCallUpdate.Name, + arguments: functionCallUpdate.ArgumentsUpdate, + functionCallIndex: functionCallUpdate.ToolCallIndex)); + } + + streamedContents?.Add(openAIStreamingChatMessageContent); + yield return openAIStreamingChatMessageContent; + } + + // Translate all entries into ChatCompletionsFunctionToolCall instances. + toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + // Translate all entries into FunctionCallContent instances for diagnostics purposes. + functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); + } + finally + { + activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); + await responseEnumerator.DisposeAsync(); + } + } + + // If we don't have a function to invoke, we're done. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + if (!autoInvoke || + toolCallIdsByIndex is not { Count: > 0 }) + { + yield break; + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + // Log the requests + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.Name}({fcr.Arguments})"))); + } + else if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); + } + + // Add the original assistant message to the chatOptions; this is required for the service + // to understand the tool call responses. + chatOptions.Messages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chat.Add(this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + + // Respond to each tooling request. + for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) + { + ChatCompletionsFunctionToolCall toolCall = toolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (string.IsNullOrEmpty(toolCall.Name)) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + try + { + openAIFunctionToolCall = new(toolCall); + } + catch (JsonException) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex - 1, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = toolCalls.Length + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatOptions, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, toolCall, this.Logger); + + // If filter requested termination, returning latest function result and breaking request iteration loop. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + var lastChatMessage = chat.Last(); + + yield return new AzureOpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); + yield break; + } + } + + // Update tool use information for the next go-around based on having completed another iteration. + Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); + + // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. + chatOptions.ToolChoice = ChatCompletionsToolChoice.None; + chatOptions.Tools.Clear(); + + if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + } + else + { + // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented + // what functions are available in the kernel. + chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); + } + + // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") + // if we don't send any tools in subsequent requests, even if we say not to use any. + if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) + { + Debug.Assert(chatOptions.Tools.Count == 0); + chatOptions.Tools.Add(s_nonInvocableFunctionTool); + } + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + } + } + + /// Checks if a tool call is for a function that was defined. + private static bool IsRequestableTool(ChatCompletionsOptions options, AzureOpenAIFunctionToolCall ftc) + { + IList tools = options.Tools; + for (int i = 0; i < tools.Count; i++) + { + if (tools[i] is ChatCompletionsFunctionToolDefinition def && + string.Equals(def.Name, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + ChatHistory chat = CreateNewChat(prompt, chatSettings); + + await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) + { + yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); + } + } + + internal async Task> GetChatAsTextContentsAsync( + string text, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ChatHistory chat = CreateNewChat(text, chatSettings); + return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) + .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) + .ToList(); + } + + internal void AddAttribute(string key, string? value) + { + if (!string.IsNullOrEmpty(value)) + { + this.Attributes.Add(key, value); + } + } + + /// Gets options to use for an OpenAIClient + /// Custom for HTTP requests. + /// Optional API version. + /// An instance of . + internal static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, OpenAIClientOptions.ServiceVersion? serviceVersion = null) + { + OpenAIClientOptions options = serviceVersion is not null ? + new(serviceVersion.Value) : + new(); + + options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; + options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), HttpPipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientTransport(httpClient); + options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + } + + return options; + } + + /// + /// Create a new empty chat instance + /// + /// Optional chat instructions for the AI service + /// Execution settings + /// Chat object + private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptExecutionSettings? executionSettings = null) + { + var chat = new ChatHistory(); + + // If settings is not provided, create a new chat with the text as the system prompt + AuthorRole textRole = AuthorRole.System; + + if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) + { + chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); + textRole = AuthorRole.User; + } + + if (!string.IsNullOrWhiteSpace(text)) + { + chat.AddMessage(textRole, text!); + } + + return chat; + } + + private static CompletionsOptions CreateCompletionsOptions(string text, AzureOpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) + { + if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) + { + throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); + } + + var options = new CompletionsOptions + { + Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings + MaxTokens = executionSettings.MaxTokens, + Temperature = (float?)executionSettings.Temperature, + NucleusSamplingFactor = (float?)executionSettings.TopP, + FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + PresencePenalty = (float?)executionSettings.PresencePenalty, + Echo = false, + ChoicesPerPrompt = executionSettings.ResultsPerPrompt, + GenerationSampleCount = executionSettings.ResultsPerPrompt, + LogProbabilityCount = executionSettings.TopLogprobs, + User = executionSettings.User, + DeploymentName = deploymentOrModelName + }; + + if (executionSettings.TokenSelectionBiases is not null) + { + foreach (var keyValue in executionSettings.TokenSelectionBiases) + { + options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); + } + } + + if (executionSettings.StopSequences is { Count: > 0 }) + { + foreach (var s in executionSettings.StopSequences) + { + options.StopSequences.Add(s); + } + } + + return options; + } + + private ChatCompletionsOptions CreateChatCompletionsOptions( + AzureOpenAIPromptExecutionSettings executionSettings, + ChatHistory chatHistory, + Kernel? kernel, + string deploymentOrModelName) + { + if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) + { + throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); + } + + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chatHistory), + JsonSerializer.Serialize(executionSettings)); + } + + var options = new ChatCompletionsOptions + { + MaxTokens = executionSettings.MaxTokens, + Temperature = (float?)executionSettings.Temperature, + NucleusSamplingFactor = (float?)executionSettings.TopP, + FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + PresencePenalty = (float?)executionSettings.PresencePenalty, + ChoiceCount = executionSettings.ResultsPerPrompt, + DeploymentName = deploymentOrModelName, + Seed = executionSettings.Seed, + User = executionSettings.User, + LogProbabilitiesPerToken = executionSettings.TopLogprobs, + EnableLogProbabilities = executionSettings.Logprobs, + AzureExtensionsOptions = executionSettings.AzureChatExtensionsOptions + }; + + switch (executionSettings.ResponseFormat) + { + case ChatCompletionsResponseFormat formatObject: + // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. + options.ResponseFormat = formatObject; + break; + + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; + break; + + case "text": + options.ResponseFormat = ChatCompletionsResponseFormat.Text; + break; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; + break; + + case "text": + options.ResponseFormat = ChatCompletionsResponseFormat.Text; + break; + } + } + break; + } + + executionSettings.ToolCallBehavior?.ConfigureOptions(kernel, options); + if (executionSettings.TokenSelectionBiases is not null) + { + foreach (var keyValue in executionSettings.TokenSelectionBiases) + { + options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); + } + } + + if (executionSettings.StopSequences is { Count: > 0 }) + { + foreach (var s in executionSettings.StopSequences) + { + options.StopSequences.Add(s); + } + } + + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) + { + options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); + } + + foreach (var message in chatHistory) + { + options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + } + + return options; + } + + private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string contents, string? name, ChatCompletionsFunctionToolCall[]? tools) + { + if (chatRole == ChatRole.User) + { + return new ChatRequestUserMessage(contents) { Name = name }; + } + + if (chatRole == ChatRole.System) + { + return new ChatRequestSystemMessage(contents) { Name = name }; + } + + if (chatRole == ChatRole.Assistant) + { + var msg = new ChatRequestAssistantMessage(contents) { Name = name }; + if (tools is not null) + { + foreach (ChatCompletionsFunctionToolCall tool in tools) + { + msg.ToolCalls.Add(tool); + } + } + return msg; + } + + throw new NotImplementedException($"Role {chatRole} is not implemented"); + } + + private static List GetRequestMessages(ChatMessageContent message, AzureToolCallBehavior? toolCallBehavior) + { + if (message.Role == AuthorRole.System) + { + return [new ChatRequestSystemMessage(message.Content) { Name = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Tool) + { + // Handling function results represented by the TextContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) + if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && + toolId?.ToString() is string toolIdString) + { + return [new ChatRequestToolMessage(message.Content, toolIdString)]; + } + + // Handling function results represented by the FunctionResultContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) + List? toolMessages = null; + foreach (var item in message.Items) + { + if (item is not FunctionResultContent resultContent) + { + continue; + } + + toolMessages ??= []; + + if (resultContent.Result is Exception ex) + { + toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.CallId)); + continue; + } + + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + + toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.CallId)); + } + + if (toolMessages is not null) + { + return toolMessages; + } + + throw new NotSupportedException("No function result provided in the tool message."); + } + + if (message.Role == AuthorRole.User) + { + if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) + { + return [new ChatRequestUserMessage(textContent.Text) { Name = message.AuthorName }]; + } + + return [new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch + { + TextContent textContent => new ChatMessageTextContentItem(textContent.Text), + ImageContent imageContent => GetImageContentItem(imageContent), + _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") + }))) + { Name = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Assistant) + { + var asstMessage = new ChatRequestAssistantMessage(message.Content) { Name = message.AuthorName }; + + // Handling function calls supplied via either: + // ChatCompletionsToolCall.ToolCalls collection items or + // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. + IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; + if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) + { + tools = toolCallsObject as IEnumerable; + if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) + { + int length = array.GetArrayLength(); + var ftcs = new List(length); + for (int i = 0; i < length; i++) + { + JsonElement e = array[i]; + if (e.TryGetProperty("Id", out JsonElement id) && + e.TryGetProperty("Name", out JsonElement name) && + e.TryGetProperty("Arguments", out JsonElement arguments) && + id.ValueKind == JsonValueKind.String && + name.ValueKind == JsonValueKind.String && + arguments.ValueKind == JsonValueKind.String) + { + ftcs.Add(new ChatCompletionsFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); + } + } + tools = ftcs; + } + } + + if (tools is not null) + { + asstMessage.ToolCalls.AddRange(tools); + } + + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. + HashSet? functionCallIds = null; + foreach (var item in message.Items) + { + if (item is not FunctionCallContent callRequest) + { + continue; + } + + functionCallIds ??= new HashSet(asstMessage.ToolCalls.Select(t => t.Id)); + + if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) + { + continue; + } + + var argument = JsonSerializer.Serialize(callRequest.Arguments); + + asstMessage.ToolCalls.Add(new ChatCompletionsFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); + } + + return [asstMessage]; + } + + throw new NotSupportedException($"Role {message.Role} is not supported."); + } + + private static ChatMessageImageContentItem GetImageContentItem(ImageContent imageContent) + { + if (imageContent.Data is { IsEmpty: false } data) + { + return new ChatMessageImageContentItem(BinaryData.FromBytes(data), imageContent.MimeType); + } + + if (imageContent.Uri is not null) + { + return new ChatMessageImageContentItem(imageContent.Uri); + } + + throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); + } + + private static ChatRequestMessage GetRequestMessage(ChatResponseMessage message) + { + if (message.Role == ChatRole.System) + { + return new ChatRequestSystemMessage(message.Content); + } + + if (message.Role == ChatRole.Assistant) + { + var msg = new ChatRequestAssistantMessage(message.Content); + if (message.ToolCalls is { Count: > 0 } tools) + { + foreach (ChatCompletionsToolCall tool in tools) + { + msg.ToolCalls.Add(tool); + } + } + + return msg; + } + + if (message.Role == ChatRole.User) + { + return new ChatRequestUserMessage(message.Content); + } + + throw new NotSupportedException($"Role {message.Role} is not supported."); + } + + private AzureOpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompletions responseData) + { + var message = new AzureOpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice)); + + message.Items.AddRange(this.GetFunctionCallContents(chatChoice.Message.ToolCalls)); + + return message; + } + + private AzureOpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string content, ChatCompletionsFunctionToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + { + var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) + { + AuthorName = authorName, + }; + + if (functionCalls is not null) + { + message.Items.AddRange(functionCalls); + } + + return message; + } + + private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) + { + List? result = null; + + foreach (var toolCall in toolCalls) + { + // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. + // This allows consumers to work with functions in an LLM-agnostic way. + if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + { + Exception? exception = null; + KernelArguments? arguments = null; + try + { + arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); + if (arguments is not null) + { + // Iterate over copy of the names to avoid mutating the dictionary while enumerating it + var names = arguments.Names.ToArray(); + foreach (var name in names) + { + arguments[name] = arguments[name]?.ToString(); + } + } + } + catch (JsonException ex) + { + exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", functionToolCall.Name, functionToolCall.Id); + } + } + + var functionName = FunctionName.Parse(functionToolCall.Name, AzureOpenAIFunction.NameSeparator); + + var functionCallContent = new FunctionCallContent( + functionName: functionName.Name, + pluginName: functionName.PluginName, + id: functionToolCall.Id, + arguments: arguments) + { + InnerContent = functionToolCall, + Exception = exception + }; + + result ??= []; + result.Add(functionCallContent); + } + } + + return result ?? Enumerable.Empty(); + } + + private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, ChatCompletionsToolCall toolCall, ILogger logger) + { + // Log any error + if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) + { + Debug.Assert(result is null); + logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); + } + + // Add the tool response message to the chat options + result ??= errorMessage ?? string.Empty; + chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); + + // Add the tool response message to the chat history. + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + + if (toolCall is ChatCompletionsFunctionToolCall functionCall) + { + // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. + // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. + var functionName = FunctionName.Parse(functionCall.Name, AzureOpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); + } + + chat.Add(message); + } + + private static void ValidateMaxTokens(int? maxTokens) + { + if (maxTokens.HasValue && maxTokens < 1) + { + throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); + } + } + + private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) + { + if (autoInvoke && resultsPerPrompt != 1) + { + // We can remove this restriction in the future if valuable. However, multiple results per prompt is rare, + // and limiting this significantly curtails the complexity of the implementation. + throw new ArgumentException($"Auto-invocation of tool calls may only be used with a {nameof(AzureOpenAIPromptExecutionSettings.ResultsPerPrompt)} of 1."); + } + } + + private static async Task RunRequestAsync(Func> request) + { + try + { + return await request.Invoke().ConfigureAwait(false); + } + catch (RequestFailedException e) + { + throw e.ToHttpOperationException(); + } + } + + /// + /// Captures usage details, including token information. + /// + /// Instance of with usage details. + private void LogUsage(CompletionsUsage usage) + { + if (usage is null) + { + this.Logger.LogDebug("Token usage information unavailable."); + return; + } + + if (this.Logger.IsEnabled(LogLevel.Information)) + { + this.Logger.LogInformation( + "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", + usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens); + } + + s_promptTokensCounter.Add(usage.PromptTokens); + s_completionTokensCounter.Add(usage.CompletionTokens); + s_totalTokensCounter.Add(usage.TotalTokens); + } + + /// + /// Processes the function result. + /// + /// The result of the function call. + /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. + /// A string representation of the function result. + private static string? ProcessFunctionResult(object functionResult, AzureToolCallBehavior? toolCallBehavior) + { + if (functionResult is string stringResult) + { + return stringResult; + } + + // This is an optimization to use ChatMessageContent content directly + // without unnecessary serialization of the whole message content class. + if (functionResult is ChatMessageContent chatMessageContent) + { + return chatMessageContent.ToString(); + } + + // For polymorphic serialization of unknown in advance child classes of the KernelContent class, + // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. + // For more details about the polymorphic serialization, see the article at: + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 +#pragma warning disable CS0618 // Type or member is obsolete + return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Executes auto function invocation filters and/or function itself. + /// This method can be moved to when auto function invocation logic will be extracted to common place. + /// + private static async Task OnAutoFunctionInvocationAsync( + Kernel kernel, + AutoFunctionInvocationContext context, + Func functionCallCallback) + { + await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); + + return context; + } + + /// + /// This method will execute auto function invocation filters and function recursively. + /// If there are no registered filters, just function will be executed. + /// If there are registered filters, filter on position will be executed. + /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. + /// Function will be always executed as last step after all filters. + /// + private static async Task InvokeFilterOrFunctionAsync( + IList? autoFunctionInvocationFilters, + Func functionCallCallback, + AutoFunctionInvocationContext context, + int index = 0) + { + if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) + { + await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, + (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); + } + else + { + await functionCallCallback(context).ConfigureAwait(false); + } + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs new file mode 100644 index 000000000000..3857d0191fbe --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Net; +using Azure; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Provides extension methods for the class. +/// +internal static class RequestFailedExceptionExtensions +{ + /// + /// Converts a to an . + /// + /// The original . + /// An instance. + public static HttpOperationException ToHttpOperationException(this RequestFailedException exception) + { + const int NoResponseReceived = 0; + + string? responseContent = null; + + try + { + responseContent = exception.GetRawResponse()?.Content?.ToString(); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead. +#pragma warning restore CA1031 + + return new HttpOperationException( + exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status, + responseContent, + exception.Message, + exception); + } +} From c967a2463026ed3a0ad8170b28c8d54b45466732 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 26 Jun 2024 14:20:30 +0100 Subject: [PATCH 008/226] .Net OpenAI V2 - Text to Image Service - Phase 02 (#6951) - Updated ImageToText service implementation using OpenAI SDK - Updated ImageToText service API's parameters order (modelId first) and added modelId as required (OpenAI supports both dall-e-2 and dall-e-3) - Added support for OpenAIClient breaking glass for Image to Text Service - Added support for custom/Non-default endpoint for Image to Text Service - Added missing Extensions (Service Collection + Kernel Builder) for Embeddings and Image to Text modalities - Added missing UnitTest for Embeddings - Added UT convering Image to Text. - Added integration tests for ImageTotext - Resolve Partially #6916 --- dotnet/SK-dotnet.sln | 3 +- .../Connectors.OpenAIV2.UnitTests.csproj | 10 +- .../Core/ClientCoreTests.cs | 68 +++++++- .../KernelBuilderExtensionsTests.cs | 73 +++++++++ .../ServiceCollectionExtensionsTests.cs | 74 +++++++++ ...enAITextEmbeddingGenerationServiceTests.cs | 41 ++++- .../Services/OpenAITextToImageServiceTests.cs | 108 +++++++++++++ .../TestData/text-to-image-response.txt | 8 + .../Core/ClientCore.Embeddings.cs | 2 - .../Core/ClientCore.TextToImage.cs | 53 ++++++ .../Connectors.OpenAIV2/Core/ClientCore.cs | 35 +++- .../OpenAIKernelBuilderExtensions.cs | 152 ++++++++++++++++++ .../OpenAIServiceCollectionExtensions.cs | 146 +++++++++++++++++ .../OpenAITextEmbbedingGenerationService.cs | 12 +- .../Services/OpenAITextToImageService.cs | 76 +++++++++ .../OpenAI/OpenAITextEmbeddingTests.cs | 4 +- .../OpenAI/OpenAITextToImageTests.cs | 42 +++++ .../InternalUtilities/test/MoqExtensions.cs | 22 +++ .../AI/TextToImage/ITextToImageService.cs | 6 +- 19 files changed, 914 insertions(+), 21 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs create mode 100644 dotnet/src/InternalUtilities/test/MoqExtensions.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 01ffff52057a..326a35a79ff7 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -92,6 +92,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5C246969-D ProjectSection(SolutionItems) = preProject src\InternalUtilities\test\AssertExtensions.cs = src\InternalUtilities\test\AssertExtensions.cs src\InternalUtilities\test\HttpMessageHandlerStub.cs = src\InternalUtilities\test\HttpMessageHandlerStub.cs + src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs = src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs = src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs src\InternalUtilities\test\TestInternalUtilities.props = src\InternalUtilities\test\TestInternalUtilities.props EndProjectSection @@ -324,7 +325,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsV2", "src\I EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj index 0d89e02beb21..0a100b3c13a6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj @@ -28,14 +28,17 @@ - - + + + + + @@ -44,6 +47,9 @@ Always + + Always + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs index a3415663459a..f162e1d7334c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs @@ -11,6 +11,7 @@ using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Services; using Moq; using OpenAI; using Xunit; @@ -23,7 +24,7 @@ public void ItCanBeInstantiatedAndPropertiesSetAsExpected() { // Act var logger = new Mock>().Object; - var openAIClient = new OpenAIClient(new ApiKeyCredential("key")); + var openAIClient = new OpenAIClient("key"); var clientCoreModelConstructor = new ClientCore("model1", "apiKey"); var clientCoreOpenAIClientConstructor = new ClientCore("model1", openAIClient, logger: logger); @@ -67,6 +68,8 @@ public void ItUsesEndpointAsExpected(string? clientBaseAddress, string? provided // Assert Assert.Equal(endpoint ?? client?.BaseAddress ?? new Uri("https://api.openai.com/v1"), clientCore.Endpoint); + Assert.True(clientCore.Attributes.ContainsKey(AIServiceExtensions.EndpointKey)); + Assert.Equal(endpoint?.ToString() ?? client?.BaseAddress?.ToString() ?? "https://api.openai.com/v1", clientCore.Attributes[AIServiceExtensions.EndpointKey]); client?.Dispose(); } @@ -142,7 +145,7 @@ public async Task ItDoNotAddSemanticKernelHeadersWhenOpenAIClientIsProvidedAsync var clientCore = new ClientCore( modelId: "model", openAIClient: new OpenAIClient( - new ApiKeyCredential("test"), + "test", new OpenAIClientOptions() { Transport = new HttpClientPipelineTransport(client), @@ -185,4 +188,65 @@ public void ItAddAttributesButDoesNothingIfNullOrEmpty(string? value) Assert.Equal(value, clientCore.Attributes["key"]); } } + + [Fact] + public void ItAddModelIdAttributeAsExpected() + { + // Arrange + var expectedModelId = "modelId"; + + // Act + var clientCore = new ClientCore(expectedModelId, "apikey"); + var clientCoreBreakingGlass = new ClientCore(expectedModelId, new OpenAIClient(" ")); + + // Assert + Assert.True(clientCore.Attributes.ContainsKey(AIServiceExtensions.ModelIdKey)); + Assert.True(clientCoreBreakingGlass.Attributes.ContainsKey(AIServiceExtensions.ModelIdKey)); + Assert.Equal(expectedModelId, clientCore.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal(expectedModelId, clientCoreBreakingGlass.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItAddOrNotOrganizationIdAttributeWhenProvided() + { + // Arrange + var expectedOrganizationId = "organizationId"; + + // Act + var clientCore = new ClientCore("modelId", "apikey", expectedOrganizationId); + var clientCoreWithoutOrgId = new ClientCore("modelId", "apikey"); + + // Assert + Assert.True(clientCore.Attributes.ContainsKey(ClientCore.OrganizationKey)); + Assert.Equal(expectedOrganizationId, clientCore.Attributes[ClientCore.OrganizationKey]); + Assert.False(clientCoreWithoutOrgId.Attributes.ContainsKey(ClientCore.OrganizationKey)); + } + + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new ClientCore(" ", "apikey")); + Assert.Throws(() => new ClientCore("", "apikey")); + Assert.Throws(() => new ClientCore(null!)); + } + + [Fact] + public void ItThrowsWhenNotUsingCustomEndpointAndApiKeyIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new ClientCore("modelId", " ")); + Assert.Throws(() => new ClientCore("modelId", "")); + Assert.Throws(() => new ClientCore("modelId", apiKey: null!)); + } + + [Fact] + public void ItDoesNotThrowWhenUsingCustomEndpointAndApiKeyIsNotProvided() + { + // Act & Assert + ClientCore? clientCore = null; + clientCore = new ClientCore("modelId", " ", endpoint: new Uri("http://localhost")); + clientCore = new ClientCore("modelId", "", endpoint: new Uri("http://localhost")); + clientCore = new ClientCore("modelId", apiKey: null!, endpoint: new Uri("http://localhost")); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs new file mode 100644 index 000000000000..f296000c5245 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; + +public class KernelBuilderExtensionsTests +{ + [Fact] + public void ItCanAddTextEmbeddingGenerationService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextEmbeddingGeneration("model", "key") + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextEmbeddingGenerationServiceWithOpenAIClient() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextEmbeddingGeneration("model", new OpenAIClient("key")) + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextToImageService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextToImage("model", "key") + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextToImageServiceWithOpenAIClient() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key")) + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000000..65db68eea180 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; + +public class ServiceCollectionExtensionsTests +{ + [Fact] + public void ItCanAddTextEmbeddingGenerationService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextEmbeddingGeneration("model", "key") + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextEmbeddingGenerationServiceWithOpenAIClient() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextEmbeddingGeneration("model", new OpenAIClient("key")) + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddImageToTextService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextToImage("model", "key") + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddImageToTextServiceWithOpenAIClient() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key")) + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs index 25cdc4ec61aa..5fb36efc0349 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs @@ -6,13 +6,19 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Services; +using Moq; using OpenAI; using Xunit; namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// public class OpenAITextEmbeddingGenerationServiceTests { [Fact] @@ -43,8 +49,9 @@ public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty() } [Fact] - public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() + public async Task GetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() { + // Arrange using HttpMessageHandlerStub handler = new() { ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) @@ -54,7 +61,6 @@ public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() }; using HttpClient client = new(handler); - // Arrange var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client); // Act @@ -68,6 +74,7 @@ public async Task IGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() [Fact] public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync() { + // Arrange using HttpMessageHandlerStub handler = new() { ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) @@ -77,10 +84,38 @@ public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync() }; using HttpClient client = new(handler); - // Arrange var sut = new OpenAITextEmbeddingGenerationService("model", "apikey", httpClient: client); // Act & Assert await Assert.ThrowsAsync(async () => await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None)); } + + [Fact] + public async Task GetEmbeddingsDoesLogActionAsync() + { + // Arrange + using HttpMessageHandlerStub handler = new() + { + ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-response.txt")) + } + }; + using HttpClient client = new(handler); + + var modelId = "dall-e-2"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + var mockLoggerFactory = new Mock(); + mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + var sut = new OpenAITextEmbeddingGenerationService(modelId, "apiKey", httpClient: client, loggerFactory: mockLoggerFactory.Object); + + // Act + await sut.GenerateEmbeddingsAsync(["description"]); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextEmbeddingGenerationService.GenerateEmbeddingsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs new file mode 100644 index 000000000000..919b864327e8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Services; +using Moq; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.UnitTests.OpenAI.Services; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextToImageServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAITextToImageServiceTests() + { + this._messageHandlerStub = new() + { + ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-to-image-response.txt")) + } + }; + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Fact] + public void ConstructorWorksCorrectly() + { + // Arrange & Act + var sut = new OpenAITextToImageService("model", "api-key", "organization"); + + // Assert + Assert.NotNull(sut); + Assert.Equal("organization", sut.Attributes[ClientCore.OrganizationKey]); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void OpenAIClientConstructorWorksCorrectly() + { + // Arrange + var sut = new OpenAITextToImageService("model", new OpenAIClient("apikey")); + + // Assert + Assert.NotNull(sut); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Theory] + [InlineData(256, 256, "dall-e-2")] + [InlineData(512, 512, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-3")] + [InlineData(1024, 1792, "dall-e-3")] + [InlineData(1792, 1024, "dall-e-3")] + [InlineData(123, 321, "custom-model-1")] + [InlineData(179, 124, "custom-model-2")] + public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) + { + // Arrange + var sut = new OpenAITextToImageService(modelId, "api-key", httpClient: this._httpClient); + Assert.Equal(modelId, sut.Attributes["ModelId"]); + + // Act + var result = await sut.GenerateImageAsync("description", width, height); + + // Assert + Assert.Equal("https://image-url/", result); + } + + [Fact] + public async Task GenerateImageDoesLogActionAsync() + { + // Assert + var modelId = "dall-e-2"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new OpenAITextToImageService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GenerateImageAsync("description", 256, 256); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextToImageService.GenerateImageAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt new file mode 100644 index 000000000000..7d8f7327a5ec --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt @@ -0,0 +1,8 @@ +{ + "created": 1702575371, + "data": [ + { + "url": "https://image-url/" + } + ] +} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs index d11e2799addd..aa15de012084 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs @@ -13,8 +13,6 @@ This class was created to simplify any Text Embeddings Support from the v1 Clien using System.Threading.Tasks; using OpenAI.Embeddings; -#pragma warning disable CA2208 // Instantiate argument exceptions correctly - namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs new file mode 100644 index 000000000000..26d8480fd004 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 02 + +- This class was created focused in the Image Generation using the SDK client instead of the own client in V1. +- Added Checking for empty or whitespace prompt. +- Removed the format parameter as this is never called in V1 code. Plan to implement it in the future once we change the ITextToImageService abstraction, using PromptExecutionSettings. +- Allow custom size for images when the endpoint is not the default OpenAI v1 endpoint. +*/ + +using System.ClientModel; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Images; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Prompt to generate the image + /// Width of the image + /// Height of the image + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task GenerateImageAsync( + string prompt, + int width, + int height, + CancellationToken cancellationToken) + { + Verify.NotNullOrWhiteSpace(prompt); + + var size = new GeneratedImageSize(width, height); + + var imageOptions = new ImageGenerationOptions() + { + Size = size, + ResponseFormat = GeneratedImageFormat.Uri + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.ModelId).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + var generatedImage = response.Value; + + return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs index 12ca2f3d92fe..a6be6d20aa46 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -4,6 +4,11 @@ Phase 01 : This class was created adapting and merging ClientCore and OpenAIClientCore classes. System.ClientModel changes were added and adapted to the code as this package is now used as a dependency over OpenAI package. All logic from original ClientCore and OpenAIClientCore were preserved. + +Phase 02 : +- Moved AddAttributes usage to the constructor, avoiding the need verify and adding it in the services. +- Added ModelId attribute to the OpenAIClient constructor. +- Added WhiteSpace instead of empty string for ApiKey to avoid exception from OpenAI Client on custom endpoints added an issue in OpenAI SDK repo. https://github.com/openai/openai-dotnet/issues/90 */ using System; @@ -17,6 +22,7 @@ All logic from original ClientCore and OpenAIClientCore were preserved. using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Services; using OpenAI; #pragma warning disable CA2208 // Instantiate argument exceptions correctly @@ -28,6 +34,16 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// internal partial class ClientCore { + /// + /// White space constant. + /// + private const string SingleSpace = " "; + + /// + /// Gets the attribute name used to store the organization in the dictionary. + /// + internal const string OrganizationKey = "Organization"; + /// /// Default OpenAI API endpoint. /// @@ -63,15 +79,15 @@ internal partial class ClientCore /// /// Model name. /// OpenAI API Key. - /// OpenAI compatible API endpoint. /// OpenAI Organization Id (usually optional). + /// OpenAI compatible API endpoint. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. internal ClientCore( string modelId, string? apiKey = null, - Uri? endpoint = null, string? organizationId = null, + Uri? endpoint = null, HttpClient? httpClient = null, ILogger? logger = null) { @@ -80,6 +96,8 @@ internal ClientCore( this.Logger = logger ?? NullLogger.Instance; this.ModelId = modelId; + this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + // Accepts the endpoint if provided, otherwise uses the default OpenAI endpoint. this.Endpoint = endpoint ?? httpClient?.BaseAddress; if (this.Endpoint is null) @@ -87,14 +105,23 @@ internal ClientCore( Verify.NotNullOrWhiteSpace(apiKey); // For Public OpenAI Endpoint a key must be provided. this.Endpoint = new Uri(OpenAIV1Endpoint); } + else if (string.IsNullOrEmpty(apiKey)) + { + // Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key. + apiKey = SingleSpace; + } + + this.AddAttribute(AIServiceExtensions.EndpointKey, this.Endpoint.ToString()); var options = GetOpenAIClientOptions(httpClient, this.Endpoint); if (!string.IsNullOrWhiteSpace(organizationId)) { options.AddPolicy(new AddHeaderRequestPolicy("OpenAI-Organization", organizationId!), PipelinePosition.PerCall); + + this.AddAttribute(ClientCore.OrganizationKey, organizationId); } - this.Client = new OpenAIClient(apiKey ?? string.Empty, options); + this.Client = new OpenAIClient(apiKey!, options); } /// @@ -116,6 +143,8 @@ internal ClientCore( this.Logger = logger ?? NullLogger.Instance; this.ModelId = modelId; this.Client = openAIClient; + + this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs new file mode 100644 index 000000000000..567d82726e4b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; + +namespace Microsoft.SemanticKernel; + +/// +/// Sponsor extensions class for . +/// +public static class OpenAIKernelBuilderExtensions +{ + #region Text Embedding + /// + /// Adds the OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The HttpClient to use with this service. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextEmbeddingGenerationService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds the OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextEmbeddingGenerationService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + #endregion + + #region Text to Image + /// + /// Add the OpenAI Dall-E text to image service to the list + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextToImage( + this IKernelBuilder builder, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToImageService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService())); + + return builder; + } + + /// + /// Add the OpenAI Dall-E text to image service to the list + /// + /// The instance to augment. + /// The model to use for image generation. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextToImage( + this IKernelBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToImageService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService())); + + return builder; + } + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs new file mode 100644 index 000000000000..77355de7f24e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; + +namespace Microsoft.SemanticKernel; + +/* Phase 02 +- Add endpoint parameter for both Embedding and TextToImage services extensions. +- Removed unnecessary Validation checks (that are already happening in the service/client constructors) +- Added openAIClient extension for TextToImage service. +- Changed parameters order for TextToImage service extension (modelId comes first). +- Made modelId a required parameter of TextToImage services. + +*/ +/// +/// Sponsor extensions class for . +/// +public static class OpenAIServiceCollectionExtensions +{ + #region Text Embedding + /// + /// Adds the OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// Non-default endpoint for the OpenAI API. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextEmbeddingGeneration( + this IServiceCollection services, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + int? dimensions = null, + Uri? endpoint = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextEmbeddingGenerationService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService(), + dimensions)); + } + + /// + /// Adds the OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// The OpenAI model id. + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceCollection services, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null, + int? dimensions = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextEmbeddingGenerationService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService(), + dimensions)); + } + #endregion + + #region Text to Image + /// + /// Add the OpenAI Dall-E text to image service to the list + /// + /// The instance to augment. + /// The model to use for image generation. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextToImage(this IServiceCollection services, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToImageService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService())); + } + + /// + /// Adds the OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// The OpenAI model id. + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextToImage(this IServiceCollection services, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null, + int? dimensions = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToImageService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService())); + } + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index 49915031b7fc..a4dd48ba75e3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -8,11 +8,14 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.Services; using OpenAI; namespace Microsoft.SemanticKernel.Connectors.OpenAI; +/* Phase 02 +Adding the non-default endpoint parameter to the constructor. +*/ + /// /// OpenAI implementation of /// @@ -28,6 +31,7 @@ public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerat /// Model name /// OpenAI API Key /// OpenAI Organization Id (usually optional) + /// Non-default endpoint for the OpenAI API /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. @@ -35,6 +39,7 @@ public OpenAITextEmbeddingGenerationService( string modelId, string apiKey, string? organization = null, + Uri? endpoint = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, int? dimensions = null) @@ -42,12 +47,11 @@ public OpenAITextEmbeddingGenerationService( this._core = new( modelId: modelId, apiKey: apiKey, + endpoint: endpoint, organizationId: organization, httpClient: httpClient, logger: loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._dimensions = dimensions; } @@ -65,8 +69,6 @@ public OpenAITextEmbeddingGenerationService( int? dimensions = null) { this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._dimensions = dimensions; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs new file mode 100644 index 000000000000..55eca0e112eb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; + +/* Phase 02 +- Breaking the current constructor parameter order to follow the same order as the other services. +- Added custom endpoint support, and removed ApiKey validation, as it is performed by the ClientCore when the Endpoint is not provided. +- Added custom OpenAIClient support. +- Updated "organization" parameter to "organizationId". +- "modelId" parameter is now required in the constructor. + +- Added OpenAIClient breaking glass constructor. +*/ + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI text to image service. +/// +[Experimental("SKEXP0010")] +public class OpenAITextToImageService : ITextToImageService +{ + private readonly ClientCore _core; + + /// + public IReadOnlyDictionary Attributes => this._core.Attributes; + + /// + /// Initializes a new instance of the class. + /// + /// The model to use for image generation. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Non-default endpoint for the OpenAI API. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAITextToImageService( + string modelId, + string? apiKey = null, + string? organizationId = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); + } + + /// + /// Initializes a new instance of the class. + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAITextToImageService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + } + + /// + public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._core.LogActionDetails(); + return this._core.GenerateImageAsync(description, width, height, cancellationToken); + } +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs index 6eca1909a546..bccc92bfa0f3 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs @@ -19,7 +19,7 @@ public sealed class OpenAITextEmbeddingTests .AddUserSecrets() .Build(); - [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] [InlineData("test sentence")] public async Task OpenAITestAsync(string testInputString) { @@ -38,7 +38,7 @@ public async Task OpenAITestAsync(string testInputString) Assert.Equal(3, batchResult.Count); } - [Theory]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] [InlineData(null, 3072)] [InlineData(1024, 1024)] public async Task OpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength) diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs new file mode 100644 index 000000000000..812d41677b28 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextToImage; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; +public sealed class OpenAITextToImageTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Theory(Skip = "This test is for manual verification.")] + [InlineData("dall-e-2", 512, 512)] + [InlineData("dall-e-3", 1024, 1024)] + public async Task OpenAITextToImageByModelTestAsync(string modelId, int width, int height) + { + // Arrange + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToImage").Get(); + Assert.NotNull(openAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddOpenAITextToImage(modelId, apiKey: openAIConfiguration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + // Act + var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", width, height); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } +} diff --git a/dotnet/src/InternalUtilities/test/MoqExtensions.cs b/dotnet/src/InternalUtilities/test/MoqExtensions.cs new file mode 100644 index 000000000000..8fb435e288f9 --- /dev/null +++ b/dotnet/src/InternalUtilities/test/MoqExtensions.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.Extensions.Logging; +using Moq; + +#pragma warning disable CS8620 // Argument cannot be used for parameter due to differences in the nullability of reference types. + +internal static class MoqExtensions +{ + public static void VerifyLog(this Mock> logger, LogLevel logLevel, string message, Times times) + { + logger.Verify( + x => x.Log( + It.Is(l => l == logLevel), + It.IsAny(), + It.Is((v, t) => v.ToString()!.Contains(message)), + It.IsAny(), + It.IsAny>()), + times); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs b/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs index c4c967445a6b..b30f78f3c0ca 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs @@ -5,6 +5,10 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel.Services; +/* Phase 02 +- Changing "description" parameter to "prompt" to better match the OpenAI API and avoid confusion. +*/ + namespace Microsoft.SemanticKernel.TextToImage; /// @@ -16,7 +20,7 @@ public interface ITextToImageService : IAIService /// /// Generate an image matching the given description /// - /// Image description + /// Image generation prompt /// Image width in pixels /// Image height in pixels /// The containing services, plugins, and other state for use throughout the operation. From c8d9adeeaa819f5d5edd67898215ebc9917c5735 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 26 Jun 2024 22:59:02 +0100 Subject: [PATCH 009/226] .Net OpenAI V2 - Internal Utilities - Phase 03 (#6970) - Updating policies using OpenAI SDK approach (GenericPolicy) impl. - Updated Unit Tests - Moved policy impl to openai Utilities. --- dotnet/SK-dotnet.sln | 12 +++ .../Models/PipelineSynchronousPolicyTests.cs | 56 ------------ .../Connectors.OpenAIV2.csproj | 1 + .../Connectors.OpenAIV2/Core/ClientCore.cs | 15 +++- .../Core/Models/AddHeaderRequestPolicy.cs | 23 ----- .../Core/Models/PipelineSynchronousPolicy.cs | 89 ------------------- .../openai/OpenAIUtilities.props | 5 ++ .../Policies/GeneratedActionPipelinePolicy.cs | 45 ++++++++++ .../SemanticKernel.UnitTests.csproj | 2 + .../GenericActionPipelinePolicyTests.cs} | 18 ++-- 10 files changed, 85 insertions(+), 181 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs create mode 100644 dotnet/src/InternalUtilities/openai/OpenAIUtilities.props create mode 100644 dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs rename dotnet/src/{Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs => SemanticKernel.UnitTests/Utilities/OpenAI/GenericActionPipelinePolicyTests.cs} (54%) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 326a35a79ff7..6da6c33ec47a 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -327,6 +327,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "s EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "openai", "openai", "{2E79AD99-632F-411F-B3A5-1BAF3F5F89AB}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\openai\OpenAIUtilities.props = src\InternalUtilities\openai\OpenAIUtilities.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Policies", "Policies", "{7308EF7D-5F9A-47B2-A62F-0898603262A8}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\openai\Policies\GeneratedActionPipelinePolicy.cs = src\InternalUtilities\openai\Policies\GeneratedActionPipelinePolicy.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -932,6 +942,8 @@ Global {FDEB4884-89B9-4656-80A0-57C7464490F7} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {6744272E-8326-48CE-9A3F-6BE227A5E777} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {DB219924-208B-4CDD-8796-EE424689901E} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} + {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} + {7308EF7D-5F9A-47B2-A62F-0898603262A8} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs deleted file mode 100644 index cae4b32b4283..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/PipelineSynchronousPolicyTests.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models; -public class PipelineSynchronousPolicyTests -{ - [Fact] - public async Task ItProcessAsyncWhenSpecializationHasReceivedResponseOverrideShouldCallIt() - { - // Arrange - var first = new MyHttpPipelinePolicyWithoutOverride(); - var last = new MyHttpPipelinePolicyWithOverride(); - - IReadOnlyList policies = [first, last]; - - // Act - await policies[0].ProcessAsync(ClientPipeline.Create().CreateMessage(), policies, 0); - - // Assert - Assert.True(first.CalledProcess); - Assert.True(last.CalledProcess); - Assert.True(last.CalledOnReceivedResponse); - } - - private class MyHttpPipelinePolicyWithoutOverride : PipelineSynchronousPolicy - { - public bool CalledProcess { get; private set; } - - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - this.CalledProcess = true; - base.Process(message, pipeline, currentIndex); - } - - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - this.CalledProcess = true; - return base.ProcessAsync(message, pipeline, currentIndex); - } - } - - private sealed class MyHttpPipelinePolicyWithOverride : MyHttpPipelinePolicyWithoutOverride - { - public bool CalledOnReceivedResponse { get; private set; } - - public override void OnReceivedResponse(PipelineMessage message) - { - this.CalledOnReceivedResponse = true; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index b17b14eb91ef..22f364461818 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -13,6 +13,7 @@ + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs index a6be6d20aa46..355000887f51 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -116,7 +116,7 @@ internal ClientCore( var options = GetOpenAIClientOptions(httpClient, this.Endpoint); if (!string.IsNullOrWhiteSpace(organizationId)) { - options.AddPolicy(new AddHeaderRequestPolicy("OpenAI-Organization", organizationId!), PipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy("OpenAI-Organization", organizationId!), PipelinePosition.PerCall); this.AddAttribute(ClientCore.OrganizationKey, organizationId); } @@ -184,7 +184,7 @@ private static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient Endpoint = endpoint }; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); if (httpClient is not null) { @@ -213,4 +213,15 @@ private static async Task RunRequestAsync(Func> request) throw e.ToHttpOperationException(); } } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + { + return new GenericActionPipelinePolicy((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs deleted file mode 100644 index 2279d639c54e..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/AddHeaderRequestPolicy.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -/* Phase 1 -Added from OpenAI v1 with adapted logic to the System.ClientModel abstraction -*/ - -using System.ClientModel.Primitives; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Helper class to inject headers into System ClientModel Http pipeline -/// -internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : PipelineSynchronousPolicy -{ - private readonly string _headerName = headerName; - private readonly string _headerValue = headerValue; - - public override void OnSendingRequest(PipelineMessage message) - { - message.Request.Headers.Add(this._headerName, this._headerValue); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs deleted file mode 100644 index b7690ead8b7f..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/Models/PipelineSynchronousPolicy.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -/* -Phase 1 -As SystemClient model does not have any specialization or extension ATM, introduced this class with the adapted to use System.ClientModel abstractions. -https://github.com/Azure/azure-sdk-for-net/blob/8bd22837639d54acccc820e988747f8d28bbde4a/sdk/core/Azure.Core/src/Pipeline/HttpPipelineSynchronousPolicy.cs -*/ - -using System; -using System.ClientModel.Primitives; -using System.Collections.Generic; -using System.Reflection; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Represents a that doesn't do any asynchronous or synchronously blocking operations. -/// -internal class PipelineSynchronousPolicy : PipelinePolicy -{ - private static readonly Type[] s_onReceivedResponseParameters = new[] { typeof(PipelineMessage) }; - - private readonly bool _hasOnReceivedResponse = true; - - /// - /// Initializes a new instance of - /// - protected PipelineSynchronousPolicy() - { - var onReceivedResponseMethod = this.GetType().GetMethod(nameof(OnReceivedResponse), BindingFlags.Instance | BindingFlags.Public, null, s_onReceivedResponseParameters, null); - if (onReceivedResponseMethod != null) - { - this._hasOnReceivedResponse = onReceivedResponseMethod.GetBaseDefinition().DeclaringType != onReceivedResponseMethod.DeclaringType; - } - } - - /// - public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - this.OnSendingRequest(message); - if (pipeline.Count > currentIndex + 1) - { - // If there are more policies in the pipeline, continue processing - ProcessNext(message, pipeline, currentIndex); - } - this.OnReceivedResponse(message); - } - - /// - public override ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - if (!this._hasOnReceivedResponse) - { - // If OnReceivedResponse was not overridden we can avoid creating a state machine and return the task directly - this.OnSendingRequest(message); - if (pipeline.Count > currentIndex + 1) - { - // If there are more policies in the pipeline, continue processing - return ProcessNextAsync(message, pipeline, currentIndex); - } - } - - return this.InnerProcessAsync(message, pipeline, currentIndex); - } - - private async ValueTask InnerProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) - { - this.OnSendingRequest(message); - if (pipeline.Count > currentIndex + 1) - { - // If there are more policies in the pipeline, continue processing - await ProcessNextAsync(message, pipeline, currentIndex).ConfigureAwait(false); - } - this.OnReceivedResponse(message); - } - - /// - /// Method is invoked before the request is sent. - /// - /// The containing the request. - public virtual void OnSendingRequest(PipelineMessage message) { } - - /// - /// Method is invoked after the response is received. - /// - /// The containing the response. - public virtual void OnReceivedResponse(PipelineMessage message) { } -} diff --git a/dotnet/src/InternalUtilities/openai/OpenAIUtilities.props b/dotnet/src/InternalUtilities/openai/OpenAIUtilities.props new file mode 100644 index 000000000000..e865b7fe40e9 --- /dev/null +++ b/dotnet/src/InternalUtilities/openai/OpenAIUtilities.props @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs b/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs new file mode 100644 index 000000000000..931f12957965 --- /dev/null +++ b/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* Phase 03 +Adapted from OpenAI SDK original policy with warning updates. + +Original file: https://github.com/openai/openai-dotnet/blob/0b97311f58dfb28bd883d990f68d548da040a807/src/Utility/GenericActionPipelinePolicy.cs#L8 +*/ + +using System; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +/// +/// Generic action pipeline policy for processing messages. +/// +[ExcludeFromCodeCoverage] +internal sealed class GenericActionPipelinePolicy : PipelinePolicy +{ + private readonly Action _processMessageAction; + + internal GenericActionPipelinePolicy(Action processMessageAction) + { + this._processMessageAction = processMessageAction; + } + + public override void Process(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this._processMessageAction(message); + if (currentIndex < pipeline.Count - 1) + { + pipeline[currentIndex + 1].Process(message, pipeline, currentIndex + 1); + } + } + + public override async ValueTask ProcessAsync(PipelineMessage message, IReadOnlyList pipeline, int currentIndex) + { + this._processMessageAction(message); + if (currentIndex < pipeline.Count - 1) + { + await pipeline[currentIndex + 1].ProcessAsync(message, pipeline, currentIndex + 1).ConfigureAwait(false); + } + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj b/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj index e929fe1ca82f..3cbaf6b60797 100644 --- a/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj +++ b/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj @@ -28,6 +28,7 @@ + @@ -38,6 +39,7 @@ + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/GenericActionPipelinePolicyTests.cs similarity index 54% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs rename to dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/GenericActionPipelinePolicyTests.cs index 83ec6a20568d..ca36f300b1c2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/Models/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/GenericActionPipelinePolicyTests.cs @@ -1,39 +1,35 @@ // Copyright (c) Microsoft. All rights reserved. using System.ClientModel.Primitives; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core.Models; +namespace SemanticKernel.UnitTests.Utilities.OpenAI; -public class AddHeaderRequestPolicyTests +public class GenericActionPipelinePolicyTests { [Fact] public void ItCanBeInstantiated() { - // Arrange - var headerName = "headerName"; - var headerValue = "headerValue"; - // Act - var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue); + var addHeaderRequestPolicy = new GenericActionPipelinePolicy((message) => { }); // Assert Assert.NotNull(addHeaderRequestPolicy); } [Fact] - public void ItOnSendingRequestAddsHeaderToRequest() + public void ItProcessAddsHeaderToRequest() { // Arrange var headerName = "headerName"; var headerValue = "headerValue"; - var addHeaderRequestPolicy = new AddHeaderRequestPolicy(headerName, headerValue); + var sut = new GenericActionPipelinePolicy((message) => { message.Request.Headers.Add(headerName, headerValue); }); + var pipeline = ClientPipeline.Create(); var message = pipeline.CreateMessage(); // Act - addHeaderRequestPolicy.OnSendingRequest(message); + sut.Process(message, [sut], 0); // Assert message.Request.Headers.TryGetValue(headerName, out var value); From f8a22b8240940fb220d500be9cecb3e3429ecc6c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 27 Jun 2024 18:34:36 +0100 Subject: [PATCH 010/226] .Net: Migrate Azure Chat Completion Service to AzureOpenAI SDK v2 (#6984) ### Motivation and Context This PR is the next step in a series of follow-up PRs to migrate AzureOpenAIConnector to Azure AI SDK v2. It updates all code related to AzureOpenAI ChatCompletionService to use the Azure AI SDK v2. One of the goals of the PR is to update the code with a minimal number of changes to make the code review as easy as possible, so almost all methods keep their names as they were even though they might not be relevant anymore. This will be fixed in one of the follow-up PRs. ### Description This PR does the following: 1. Migrates AzureOpenAIChatCompletionService, ClientCore, and other model classes both use, to Azure AI SDK v2. 2. Updates ToolCallBehavior classes to return a list of functions and function choice. This change is required because the new SDK model requires both of those for the CompletionsOptions class creation and does not allow setting them after the class is already created, as it used to allow. 3. Adapts related unit tests to the API changes. ### Next steps 1. Add integration tests. 2. Rename internal/private methods that were intentionally left with old, irrelevant names to minimize the code review delta. ### Out of scope: * https://github.com/microsoft/semantic-kernel/issues/6991 ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- ...AzureOpenAIPromptExecutionSettingsTests.cs | 6 +- .../AzureOpenAITestHelper.cs | 10 + ...cs => AzureOpenAIToolCallBehaviorTests.cs} | 82 +- .../AzureOpenAIChatCompletionServiceTests.cs | 127 +-- .../AzureOpenAIChatMessageContentTests.cs | 31 +- .../Core/AzureOpenAIFunctionToolCallTests.cs | 10 +- ...reOpenAIPluginCollectionExtensionsTests.cs | 8 +- .../ClientResultExceptionExtensionsTests.cs | 53 ++ .../RequestFailedExceptionExtensionsTests.cs | 77 -- .../AutoFunctionInvocationFilterTests.cs | 37 +- .../AzureOpenAIFunctionTests.cs | 42 +- .../KernelFunctionMetadataExtensionsTests.cs | 4 +- .../AddHeaderRequestPolicy.cs | 20 - .../AzureOpenAIPromptExecutionSettings.cs | 46 +- ...vior.cs => AzureOpenAIToolCallBehavior.cs} | 86 +- .../AzureOpenAIChatCompletionService.cs | 3 +- ....cs => ClientResultExceptionExtensions.cs} | 9 +- .../Connectors.AzureOpenAI.csproj | 3 +- .../Core/AzureOpenAIChatMessageContent.cs | 45 +- .../Core/AzureOpenAIClientCore.cs | 11 +- .../Core/AzureOpenAIFunction.cs | 20 +- .../Core/AzureOpenAIFunctionToolCall.cs | 52 +- .../AzureOpenAIPluginCollectionExtensions.cs | 4 +- .../AzureOpenAIStreamingChatMessageContent.cs | 35 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 862 +++++++----------- 25 files changed, 714 insertions(+), 969 deletions(-) rename dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/{AzureToolCallBehaviorTests.cs => AzureOpenAIToolCallBehaviorTests.cs} (69%) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs rename dotnet/src/Connectors/Connectors.AzureOpenAI/{AzureToolCallBehavior.cs => AzureOpenAIToolCallBehavior.cs} (78%) rename dotnet/src/Connectors/Connectors.AzureOpenAI/{RequestFailedExceptionExtensions.cs => ClientResultExceptionExtensions.cs} (78%) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs index 0cf1c4e2a9e3..7b50e36c5587 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs @@ -26,12 +26,11 @@ public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() Assert.Equal(1, executionSettings.TopP); Assert.Equal(0, executionSettings.FrequencyPenalty); Assert.Equal(0, executionSettings.PresencePenalty); - Assert.Equal(1, executionSettings.ResultsPerPrompt); Assert.Null(executionSettings.StopSequences); Assert.Null(executionSettings.TokenSelectionBiases); Assert.Null(executionSettings.TopLogprobs); Assert.Null(executionSettings.Logprobs); - Assert.Null(executionSettings.AzureChatExtensionsOptions); + Assert.Null(executionSettings.AzureChatDataSource); Assert.Equal(128, executionSettings.MaxTokens); } @@ -45,7 +44,6 @@ public void ItUsesExistingOpenAIExecutionSettings() TopP = 0.7, FrequencyPenalty = 0.7, PresencePenalty = 0.7, - ResultsPerPrompt = 2, StopSequences = new string[] { "foo", "bar" }, ChatSystemPrompt = "chat system prompt", MaxTokens = 128, @@ -231,7 +229,6 @@ public void PromptExecutionSettingsFreezeWorksAsExpected() // Assert Assert.True(executionSettings.IsFrozen); Assert.Throws(() => executionSettings.ModelId = "gpt-4"); - Assert.Throws(() => executionSettings.ResultsPerPrompt = 2); Assert.Throws(() => executionSettings.Temperature = 1); Assert.Throws(() => executionSettings.TopP = 1); Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); @@ -262,7 +259,6 @@ private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings e Assert.Equal(0.7, executionSettings.TopP); Assert.Equal(0.7, executionSettings.FrequencyPenalty); Assert.Equal(0.7, executionSettings.PresencePenalty); - Assert.Equal(2, executionSettings.ResultsPerPrompt); Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs index 9df4aae40c2d..31a7654fcfc6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAITestHelper.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.IO; +using System.Net.Http; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; @@ -17,4 +18,13 @@ internal static string GetTestResponse(string fileName) { return File.ReadAllText($"./TestData/{fileName}"); } + + /// + /// Reads test response from file and create . + /// + /// Name of the file with test response. + internal static StreamContent GetTestResponseAsStream(string fileName) + { + return new StreamContent(File.OpenRead($"./TestData/{fileName}")); + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs similarity index 69% rename from dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs index 525dabcd26d2..6baa78faae1e 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureToolCallBehaviorTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs @@ -2,23 +2,23 @@ using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureToolCallBehavior; +using OpenAI.Chat; +using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureOpenAIToolCallBehavior; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; /// -/// Unit tests for +/// Unit tests for /// -public sealed class AzureToolCallBehaviorTests +public sealed class AzureOpenAIToolCallBehaviorTests { [Fact] public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() { // Arrange & Act - var behavior = AzureToolCallBehavior.EnableKernelFunctions; + var behavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions; // Assert Assert.IsType(behavior); @@ -30,7 +30,7 @@ public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() { // Arrange & Act const int DefaultMaximumAutoInvokeAttempts = 128; - var behavior = AzureToolCallBehavior.AutoInvokeKernelFunctions; + var behavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions; // Assert Assert.IsType(behavior); @@ -42,7 +42,7 @@ public void EnableFunctionsReturnsEnabledFunctionsInstance() { // Arrange & Act List functions = [new("Plugin", "Function", "description", [], null)]; - var behavior = AzureToolCallBehavior.EnableFunctions(functions); + var behavior = AzureOpenAIToolCallBehavior.EnableFunctions(functions); // Assert Assert.IsType(behavior); @@ -52,7 +52,7 @@ public void EnableFunctionsReturnsEnabledFunctionsInstance() public void RequireFunctionReturnsRequiredFunctionInstance() { // Arrange & Act - var behavior = AzureToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); + var behavior = AzureOpenAIToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); // Assert Assert.IsType(behavior); @@ -63,13 +63,13 @@ public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(null); // Assert - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -77,15 +77,14 @@ public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(kernel); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -93,7 +92,6 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() { // Arrange var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); var plugin = this.GetTestPlugin(); @@ -101,12 +99,12 @@ public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() kernel.Plugins.Add(plugin); // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = kernelFunctions.ConfigureOptions(kernel); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + Assert.Equal(ChatToolChoice.Auto, options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } [Fact] @@ -114,14 +112,13 @@ public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() { // Arrange var enabledFunctions = new EnabledFunctions([], autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act - enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); + var options = enabledFunctions.ConfigureOptions(null); // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); + Assert.Null(options.Choice); + Assert.Null(options.Tools); } [Fact] @@ -130,10 +127,9 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsExc // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null)); Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); } @@ -143,11 +139,10 @@ public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsEx // Arrange var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel)); Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -160,18 +155,17 @@ public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool a var plugin = this.GetTestPlugin(); var functions = plugin.GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); var enabledFunctions = new EnabledFunctions(functions, autoInvoke); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); kernel.Plugins.Add(plugin); // Act - enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); + var options = enabledFunctions.ConfigureOptions(kernel); // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); + Assert.Equal(ChatToolChoice.Auto, options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } [Fact] @@ -180,10 +174,9 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsEx // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null, chatCompletionsOptions)); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null)); Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); } @@ -193,11 +186,10 @@ public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsE // Arrange var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); var kernel = Kernel.CreateBuilder().Build(); // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions)); + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel)); Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); } @@ -207,18 +199,17 @@ public void RequiredFunctionConfigureOptionsAddsTools() // Arrange var plugin = this.GetTestPlugin(); var function = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - var chatCompletionsOptions = new ChatCompletionsOptions(); var requiredFunction = new RequiredFunction(function, autoInvoke: true); var kernel = new Kernel(); kernel.Plugins.Add(plugin); // Act - requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions); + var options = requiredFunction.ConfigureOptions(kernel); // Assert - Assert.NotNull(chatCompletionsOptions.ToolChoice); + Assert.NotNull(options.Choice); - this.AssertTools(chatCompletionsOptions); + this.AssertTools(options.Tools); } private KernelPlugin GetTestPlugin() @@ -233,16 +224,15 @@ private KernelPlugin GetTestPlugin() return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); } - private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) + private void AssertTools(IList? tools) { - Assert.Single(chatCompletionsOptions.Tools); - - var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; + Assert.NotNull(tools); + var tool = Assert.Single(tools); Assert.NotNull(tool); - Assert.Equal("MyPlugin-MyFunction", tool.Name); - Assert.Equal("Test Function", tool.Description); - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.Parameters.ToString()); + Assert.Equal("MyPlugin-MyFunction", tool.FunctionName); + Assert.Equal("Test Function", tool.FunctionDescription); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.FunctionParameters.ToString()); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs index 69c314bdcb46..3b3c90687b45 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs @@ -10,6 +10,7 @@ using System.Text.Json; using System.Threading.Tasks; using Azure.AI.OpenAI; +using Azure.AI.OpenAI.Chat; using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -17,6 +18,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Moq; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.ChatCompletion; @@ -79,7 +81,7 @@ public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFacto public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) { // Arrange & Act - var client = new OpenAIClient("key"); + var client = new AzureOpenAIClient(new Uri("http://host"), "key"); var service = includeLoggerFactory ? new AzureOpenAIChatCompletionService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : new AzureOpenAIChatCompletionService("deployment", client, "model-id"); @@ -106,45 +108,14 @@ public async Task GetTextContentsWorksCorrectlyAsync() Assert.True(result.Count > 0); Assert.Equal("Test chat response", result[0].Text); - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + var usage = result[0].Metadata?["Usage"] as ChatTokenUsage; Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(55, usage.InputTokens); + Assert.Equal(100, usage.OutputTokens); Assert.Equal(155, usage.TotalTokens); } - [Fact] - public async Task GetChatMessageContentsWithEmptyChoicesThrowsExceptionAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{\"id\":\"response-id\",\"object\":\"chat.completion\",\"created\":1704208954,\"model\":\"gpt-4\",\"choices\":[],\"usage\":{\"prompt_tokens\":55,\"completion_tokens\":100,\"total_tokens\":155},\"system_fingerprint\":null}") - }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([])); - - Assert.Equal("Chat completions not found", exception.Message); - } - - [Theory] - [InlineData(0)] - [InlineData(129)] - public async Task GetChatMessageContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new AzureOpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([], settings)); - - Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); - } - [Fact] public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() { @@ -157,22 +128,16 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() TopP = 0.5, FrequencyPenalty = 1.6, PresencePenalty = 1.2, - ResultsPerPrompt = 5, Seed = 567, TokenSelectionBiases = new Dictionary { { 2, 3 } }, StopSequences = ["stop_sequence"], Logprobs = true, TopLogprobs = 5, - AzureChatExtensionsOptions = new AzureChatExtensionsOptions + AzureChatDataSource = new AzureSearchChatDataSource() { - Extensions = - { - new AzureSearchChatExtensionConfiguration - { - SearchEndpoint = new Uri("http://test-search-endpoint"), - IndexName = "test-index-name" - } - } + Endpoint = new Uri("http://test-search-endpoint"), + IndexName = "test-index-name", + Authentication = DataSourceAuthentication.FromApiKey("api-key"), } }; @@ -226,7 +191,6 @@ public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); - Assert.Equal(5, content.GetProperty("n").GetInt32()); Assert.Equal(567, content.GetProperty("seed").GetInt32()); Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); @@ -259,7 +223,7 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje }); // Act - var result = await service.GetChatMessageContentsAsync([], settings); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings); // Assert var requestContent = this._messageHandlerStub.RequestContents[0]; @@ -273,7 +237,7 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje [Theory] [MemberData(nameof(ToolCallBehaviors))] - public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureToolCallBehavior behavior) + public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureOpenAIToolCallBehavior behavior) { // Arrange var kernel = Kernel.CreateBuilder().Build(); @@ -286,20 +250,20 @@ public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureToolCallBehavio }); // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.True(result.Count > 0); Assert.Equal("Test chat response", result[0].Content); - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; + var usage = result[0].Metadata?["Usage"] as ChatTokenUsage; Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); + Assert.Equal(55, usage.InputTokens); + Assert.Equal(100, usage.OutputTokens); Assert.Equal(155, usage.TotalTokens); - Assert.Equal("stop", result[0].Metadata?["FinishReason"]); + Assert.Equal("Stop", result[0].Metadata?["FinishReason"]); } [Fact] @@ -324,7 +288,7 @@ public async Task GetChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -332,7 +296,7 @@ public async Task GetChatMessageContentsWithFunctionCallAsync() this._messageHandlerStub.ResponsesToReturn = [response1, response2]; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.True(result.Count > 0); @@ -360,7 +324,7 @@ public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttempt kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); @@ -372,7 +336,7 @@ public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttempt this._messageHandlerStub.ResponsesToReturn = responses; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); @@ -397,7 +361,7 @@ public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -405,7 +369,7 @@ public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() this._messageHandlerStub.ResponsesToReturn = [response1, response2]; // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); + var result = await service.GetChatMessageContentsAsync(new ChatHistory("System message"), settings, kernel); // Assert Assert.Equal(1, functionCallCount); @@ -447,7 +411,7 @@ public async Task GetStreamingTextContentsWorksCorrectlyAsync() Assert.Equal("Test chat streaming response", enumerator.Current.Text); await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); } [Fact] @@ -469,7 +433,7 @@ public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() Assert.Equal("Test chat streaming response", enumerator.Current.Content); await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); } [Fact] @@ -494,10 +458,10 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_multiple_function_calls_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_multiple_function_calls_test_response.txt") }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; this._messageHandlerStub.ResponsesToReturn = [response1, response2]; @@ -506,10 +470,10 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() await enumerator.MoveNextAsync(); Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); await enumerator.MoveNextAsync(); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); // Keep looping until the end of stream while (await enumerator.MoveNextAsync()) @@ -538,13 +502,13 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvo kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); for (var i = 0; i < ModelResponsesCount; i++) { - responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }); + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }); } this._messageHandlerStub.ResponsesToReturn = responses; @@ -577,10 +541,10 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; this._messageHandlerStub.ResponsesToReturn = [response1, response2]; @@ -590,7 +554,7 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() // Function Tool Call Streaming (One Chunk) await enumerator.MoveNextAsync(); Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); // Chat Completion Streaming (1st Chunk) await enumerator.MoveNextAsync(); @@ -598,7 +562,7 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() // Chat Completion Streaming (2nd Chunk) await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); Assert.Equal(1, functionCallCount); @@ -736,7 +700,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Fake prompt"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act var result = await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -806,7 +770,7 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() new ChatMessageContent(AuthorRole.Assistant, items) ]; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -865,7 +829,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -910,7 +874,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -941,18 +905,15 @@ public void Dispose() this._messageHandlerStub.Dispose(); } - public static TheoryData ToolCallBehaviors => new() + public static TheoryData ToolCallBehaviors => new() { - AzureToolCallBehavior.EnableKernelFunctions, - AzureToolCallBehavior.AutoInvokeKernelFunctions + AzureOpenAIToolCallBehavior.EnableKernelFunctions, + AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; public static TheoryData ResponseFormats => new() { - { new FakeChatCompletionsResponseFormat(), null }, { "json_object", "json_object" }, { "text", "text" } }; - - private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs index 304e62bc9aeb..76e0b2064439 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs @@ -3,9 +3,9 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,10 +18,10 @@ public sealed class AzureOpenAIChatMessageContentTests public void ConstructorsWorkCorrectly() { // Arrange - List toolCalls = [new FakeChatCompletionsToolCall("id")]; + List toolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", "args")]; // Act - var content1 = new AzureOpenAIChatMessageContent(new ChatRole("user"), "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; + var content1 = new AzureOpenAIChatMessageContent(ChatMessageRole.User, "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); // Assert @@ -33,11 +33,9 @@ public void ConstructorsWorkCorrectly() public void GetOpenAIFunctionToolCallsReturnsCorrectList() { // Arrange - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); @@ -64,11 +62,9 @@ public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : new Dictionary { { "key", "value" } }; - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; // Act var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); @@ -82,9 +78,9 @@ public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) Assert.Equal(2, content2.Metadata.Count); Assert.Equal("value", content2.Metadata["key"]); - Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); + Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); - var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; + var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; Assert.NotNull(actualToolCalls); Assert.Equal(2, actualToolCalls.Count); @@ -96,7 +92,7 @@ private void AssertChatMessageContent( AuthorRole expectedRole, string expectedContent, string expectedModelId, - IReadOnlyList expectedToolCalls, + IReadOnlyList expectedToolCalls, AzureOpenAIChatMessageContent actualContent, string? expectedName = null) { @@ -107,9 +103,6 @@ private void AssertChatMessageContent( Assert.Same(expectedToolCalls, actualContent.ToolCalls); } - private sealed class FakeChatCompletionsToolCall(string id) : ChatCompletionsToolCall(id) - { } - private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> { public TValue this[TKey key] => dictionary[key]; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs index 8f16c6ea7db2..766376ee00b9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Text; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,7 +18,7 @@ public sealed class AzureOpenAIFunctionToolCallTests public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) { // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", toolCallName, string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", toolCallName, string.Empty); var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); // Act & Assert @@ -30,7 +30,7 @@ public void FullyQualifiedNameReturnsValidName(string toolCallName, string expec public void ToStringReturnsCorrectValue() { // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); // Act & Assert @@ -75,7 +75,7 @@ public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() var toolCall = toolCalls[0]; Assert.Equal("test-id", toolCall.Id); - Assert.Equal("test-function", toolCall.Name); - Assert.Equal("test-argument", toolCall.Arguments); + Assert.Equal("test-function", toolCall.FunctionName); + Assert.Equal("test-argument", toolCall.FunctionArguments); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs index bbfb636196d3..e0642abc52e1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; @@ -18,7 +18,7 @@ public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); @@ -37,7 +37,7 @@ public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); @@ -56,7 +56,7 @@ public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); // Act var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs new file mode 100644 index 000000000000..d810b2d2a470 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.IO; +using System.Net; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class ClientResultExceptionExtensionsTests +{ + [Fact] + public void ToHttpOperationExceptionWithContentReturnsValidException() + { + // Arrange + using var response = new FakeResponse("Response Content", 500); + var exception = new ClientResultException(response); + + // Act + var actualException = exception.ToHttpOperationException(); + + // Assert + Assert.IsType(actualException); + Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); + Assert.Equal("Response Content", actualException.ResponseContent); + Assert.Same(exception, actualException.InnerException); + } + + #region private + + private sealed class FakeResponse(string responseContent, int status) : PipelineResponse + { + private readonly string _responseContent = responseContent; + public override BinaryData Content => BinaryData.FromString(this._responseContent); + public override int Status { get; } = status; + public override string ReasonPhrase => "Reason Phrase"; + public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } + protected override PipelineResponseHeaders HeadersCore => throw new NotImplementedException(); + public override BinaryData BufferContent(CancellationToken cancellationToken = default) => new(this._responseContent); + public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); + public override void Dispose() { } + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs deleted file mode 100644 index 9fb65039116d..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/RequestFailedExceptionExtensionsTests.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Azure; -using Azure.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class RequestFailedExceptionExtensionsTests -{ - [Theory] - [InlineData(0, null)] - [InlineData(500, HttpStatusCode.InternalServerError)] - public void ToHttpOperationExceptionWithStatusReturnsValidException(int responseStatus, HttpStatusCode? httpStatusCode) - { - // Arrange - var exception = new RequestFailedException(responseStatus, "Error Message"); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(httpStatusCode, actualException.StatusCode); - Assert.Equal("Error Message", actualException.Message); - Assert.Same(exception, actualException.InnerException); - } - - [Fact] - public void ToHttpOperationExceptionWithContentReturnsValidException() - { - // Arrange - using var response = new FakeResponse("Response Content", 500); - var exception = new RequestFailedException(response); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); - Assert.Equal("Response Content", actualException.ResponseContent); - Assert.Same(exception, actualException.InnerException); - } - - #region private - - private sealed class FakeResponse(string responseContent, int status) : Response - { - private readonly string _responseContent = responseContent; - private readonly IEnumerable _headers = []; - - public override BinaryData Content => BinaryData.FromString(this._responseContent); - public override int Status { get; } = status; - public override string ReasonPhrase => "Reason Phrase"; - public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } - public override string ClientRequestId { get => "Client Request Id"; set => throw new NotImplementedException(); } - - public override void Dispose() { } - protected override bool ContainsHeader(string name) => throw new NotImplementedException(); - protected override IEnumerable EnumerateHeaders() => this._headers; -#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - protected override bool TryGetHeader(string name, out string? value) => throw new NotImplementedException(); - protected override bool TryGetHeaderValues(string name, out IEnumerable? values) => throw new NotImplementedException(); -#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs index 270b055d730c..195f71e2758f 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs @@ -64,7 +64,7 @@ public async Task FiltersAreExecutedCorrectlyAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -107,7 +107,7 @@ public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) @@ -167,7 +167,7 @@ public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -227,7 +227,7 @@ public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) var arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }); // Act @@ -277,7 +277,7 @@ public async Task FilterCanOverrideArgumentsAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -309,9 +309,10 @@ public async Task FilterCanHandleExceptionAsync() var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("System message"); // Act var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); @@ -349,7 +350,7 @@ public async Task FilterCanHandleExceptionOnStreamingAsync() var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); var chatHistory = new ChatHistory(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) @@ -395,7 +396,7 @@ public async Task FiltersCanSkipFunctionExecutionAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -429,7 +430,7 @@ public async Task PreFilterCanTerminateOperationAsync() // Act await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -459,7 +460,7 @@ public async Task PreFilterCanTerminateOperationOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; // Act await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) @@ -500,7 +501,7 @@ public async Task PostFilterCanTerminateOperationAsync() // Act var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings { - ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions })); // Assert @@ -544,7 +545,7 @@ public async Task PostFilterCanTerminateOperationOnStreamingAsync() this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureToolCallBehavior.AutoInvokeKernelFunctions }; + var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; List streamingContent = []; @@ -582,18 +583,18 @@ public void Dispose() private static List GetFunctionCallingResponses() { return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) } + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_test_response.json") } ]; } private static List GetFunctionCallingStreamingResponses() { return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) } + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") } ]; } #pragma warning restore CA2000 diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs index bd268ef67991..cf83f89bc783 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs @@ -4,9 +4,9 @@ using System.ComponentModel; using System.Linq; using System.Text.Json; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; @@ -51,11 +51,11 @@ public void ItCanConvertToFunctionDefinitionWithNoPluginName() AzureOpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = sut.ToFunctionDefinition(); + ChatTool result = sut.ToFunctionDefinition(); // Assert - Assert.Equal(sut.FunctionName, result.Name); - Assert.Equal(sut.Description, result.Description); + Assert.Equal(sut.FunctionName, result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); } [Fact] @@ -68,7 +68,7 @@ public void ItCanConvertToFunctionDefinitionWithNullParameters() var result = sut.ToFunctionDefinition(); // Assert - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.Parameters.ToString()); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.FunctionParameters.ToString()); } [Fact] @@ -81,11 +81,11 @@ public void ItCanConvertToFunctionDefinitionWithPluginName() }).GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); // Act - FunctionDefinition result = sut.ToFunctionDefinition(); + ChatTool result = sut.ToFunctionDefinition(); // Assert - Assert.Equal("myplugin-myfunc", result.Name); - Assert.Equal(sut.Description, result.Description); + Assert.Equal("myplugin-myfunc", result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); } [Fact] @@ -103,15 +103,15 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParamete AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + ChatTool functionDefinition = sut.ToFunctionDefinition(); var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); - var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters)); + var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters)); Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); } [Fact] @@ -129,12 +129,12 @@ public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParame AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); + ChatTool functionDefinition = sut.ToFunctionDefinition(); Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); } [Fact] @@ -146,8 +146,8 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() parameters: [new KernelParameterMetadata("param1")]).Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; // Assert Assert.NotNull(pd.properties); @@ -166,8 +166,8 @@ public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescript parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToAzureOpenAIFunction(); // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; // Assert Assert.NotNull(pd.properties); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs index ebf7b67a2f9b..67cd371dfe23 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs @@ -196,7 +196,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() Assert.NotNull(result); Assert.Equal( """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", - result.Parameters.ToString() + result.FunctionParameters.ToString() ); } @@ -231,7 +231,7 @@ public void ItCanCreateValidAzureOpenAIFunctionManualForPrompt() Assert.NotNull(result); Assert.Equal( """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", - result.Parameters.ToString() + result.FunctionParameters.ToString() ); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs deleted file mode 100644 index 8303b2ceaeaf..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AddHeaderRequestPolicy.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.Core; -using Azure.Core.Pipeline; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Helper class to inject headers into Azure SDK HTTP pipeline -/// -internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : HttpPipelineSynchronousPolicy -{ - private readonly string _headerName = headerName; - private readonly string _headerValue = headerValue; - - public override void OnSendingRequest(HttpMessage message) - { - message.Request.Headers.Add(this._headerName, this._headerValue); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs index 69c305f58f34..22141ee8aee0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs @@ -6,9 +6,10 @@ using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; -using Azure.AI.OpenAI; +using Azure.AI.OpenAI.Chat; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Text; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -116,23 +117,6 @@ public IList? StopSequences } } - /// - /// How many completions to generate for each prompt. Default is 1. - /// Note: Because this parameter generates many completions, it can quickly consume your token quota. - /// Use carefully and ensure that you have reasonable settings for max_tokens and stop. - /// - [JsonPropertyName("results_per_prompt")] - public int ResultsPerPrompt - { - get => this._resultsPerPrompt; - - set - { - this.ThrowIfFrozen(); - this._resultsPerPrompt = value; - } - } - /// /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the /// same seed and parameters should return the same result. Determinism is not guaranteed. @@ -153,7 +137,7 @@ public long? Seed /// Gets or sets the response format to use for the completion. /// /// - /// Possible values are: "json_object", "text", object. + /// Possible values are: "json_object", "text", object. /// [Experimental("SKEXP0010")] [JsonPropertyName("response_format")] @@ -207,18 +191,18 @@ public IDictionary? TokenSelectionBiases /// To disable all tool calling, set the property to null (the default). /// /// To request that the model use a specific function, set the property to an instance returned - /// from . + /// from . /// /// /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with + /// instance returned from , called with /// a list of the functions available. /// /// /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply + /// set the property to if the client should simply /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically + /// if the client should attempt to automatically /// invoke the function and send the result back to the service. /// /// @@ -229,7 +213,7 @@ public IDictionary? TokenSelectionBiases /// the function, and sending back the result. The intermediate messages will be retained in the /// if an instance was provided. /// - public AzureToolCallBehavior? ToolCallBehavior + public AzureOpenAIToolCallBehavior? ToolCallBehavior { get => this._toolCallBehavior; @@ -293,14 +277,14 @@ public int? TopLogprobs /// [Experimental("SKEXP0010")] [JsonIgnore] - public AzureChatExtensionsOptions? AzureChatExtensionsOptions + public AzureChatDataSource? AzureChatDataSource { - get => this._azureChatExtensionsOptions; + get => this._azureChatDataSource; set { this.ThrowIfFrozen(); - this._azureChatExtensionsOptions = value; + this._azureChatDataSource = value; } } @@ -338,7 +322,6 @@ public override PromptExecutionSettings Clone() FrequencyPenalty = this.FrequencyPenalty, MaxTokens = this.MaxTokens, StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - ResultsPerPrompt = this.ResultsPerPrompt, Seed = this.Seed, ResponseFormat = this.ResponseFormat, TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, @@ -347,7 +330,7 @@ public override PromptExecutionSettings Clone() ChatSystemPrompt = this.ChatSystemPrompt, Logprobs = this.Logprobs, TopLogprobs = this.TopLogprobs, - AzureChatExtensionsOptions = this.AzureChatExtensionsOptions, + AzureChatDataSource = this.AzureChatDataSource, }; } @@ -417,16 +400,15 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P private double _frequencyPenalty; private int? _maxTokens; private IList? _stopSequences; - private int _resultsPerPrompt = 1; private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; - private AzureToolCallBehavior? _toolCallBehavior; + private AzureOpenAIToolCallBehavior? _toolCallBehavior; private string? _user; private string? _chatSystemPrompt; private bool? _logprobs; private int? _topLogprobs; - private AzureChatExtensionsOptions? _azureChatExtensionsOptions; + private AzureChatDataSource? _azureChatDataSource; #endregion } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs similarity index 78% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs index 4c3baef49268..e9dbd224b2a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureToolCallBehavior.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs @@ -6,12 +6,12 @@ using System.Diagnostics; using System.Linq; using System.Text.Json; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// Represents a behavior for Azure OpenAI tool calls. -public abstract class AzureToolCallBehavior +public abstract class AzureOpenAIToolCallBehavior { // NOTE: Right now, the only tools that are available are for function calling. In the future, // this class can be extended to support additional kinds of tools, including composite ones: @@ -45,7 +45,7 @@ public abstract class AzureToolCallBehavior /// /// If no is available, no function information will be provided to the model. /// - public static AzureToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); + public static AzureOpenAIToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); /// /// Gets an instance that will both provide all of the 's plugins' function information @@ -56,16 +56,16 @@ public abstract class AzureToolCallBehavior /// handling invoking any requested functions and supplying the results back to the model. /// If no is available, no function information will be provided to the model. /// - public static AzureToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); + public static AzureOpenAIToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); /// Gets an instance that will provide the specified list of functions to the model. /// The functions that should be made available to the model. /// true to attempt to automatically handle function call requests; otherwise, false. /// - /// The that may be set into + /// The that may be set into /// to indicate that the specified functions should be made available to the model. /// - public static AzureToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) + public static AzureOpenAIToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) { Verify.NotNull(functions); return new EnabledFunctions(functions, autoInvoke); @@ -75,17 +75,17 @@ public static AzureToolCallBehavior EnableFunctions(IEnumerableThe function the model should request to use. /// true to attempt to automatically handle function call requests; otherwise, false. /// - /// The that may be set into + /// The that may be set into /// to indicate that the specified function should be requested by the model. /// - public static AzureToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) + public static AzureOpenAIToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) { Verify.NotNull(function); return new RequiredFunction(function, autoInvoke); } /// Initializes the instance; prevents external instantiation. - private AzureToolCallBehavior(bool autoInvoke) + private AzureOpenAIToolCallBehavior(bool autoInvoke) { this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; } @@ -118,23 +118,25 @@ private AzureToolCallBehavior(bool autoInvoke) /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. internal virtual bool AllowAnyRequestedKernelFunction => false; - /// Configures the with any tools this provides. - /// The used for the operation. This can be queried to determine what tools to provide into the . - /// The destination to configure. - internal abstract void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options); + /// Returns list of available tools and the way model should use them. + /// The used for the operation. This can be queried to determine what tools to return. + internal abstract (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel); /// - /// Represents a that will provide to the model all available functions from a + /// Represents a that will provide to the model all available functions from a /// provided by the client. Setting this will have no effect if no is provided. /// - internal sealed class KernelFunctions : AzureToolCallBehavior + internal sealed class KernelFunctions : AzureOpenAIToolCallBehavior { internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { + ChatToolChoice? choice = null; + List? tools = null; + // If no kernel is provided, we don't have any tools to provide. if (kernel is not null) { @@ -142,44 +144,50 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o IList functions = kernel.Plugins.GetFunctionsMetadata(); if (functions.Count > 0) { - options.ToolChoice = ChatCompletionsToolChoice.Auto; + choice = ChatToolChoice.Auto; + tools = []; for (int i = 0; i < functions.Count; i++) { - options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition())); + tools.Add(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition()); } } } + + return (tools, choice); } internal override bool AllowAnyRequestedKernelFunction => true; } /// - /// Represents a that provides a specified list of functions to the model. + /// Represents a that provides a specified list of functions to the model. /// - internal sealed class EnabledFunctions : AzureToolCallBehavior + internal sealed class EnabledFunctions : AzureOpenAIToolCallBehavior { private readonly AzureOpenAIFunction[] _openAIFunctions; - private readonly ChatCompletionsFunctionToolDefinition[] _functions; + private readonly ChatTool[] _functions; public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) { this._openAIFunctions = functions.ToArray(); - var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; + var defs = new ChatTool[this._openAIFunctions.Length]; for (int i = 0; i < defs.Length; i++) { - defs[i] = new ChatCompletionsFunctionToolDefinition(this._openAIFunctions[i].ToFunctionDefinition()); + defs[i] = this._openAIFunctions[i].ToFunctionDefinition(); } this._functions = defs; } - public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.Name))}"; + public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.FunctionName))}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { + ChatToolChoice? choice = null; + List? tools = null; + AzureOpenAIFunction[] openAIFunctions = this._openAIFunctions; - ChatCompletionsFunctionToolDefinition[] functions = this._functions; + ChatTool[] functions = this._functions; Debug.Assert(openAIFunctions.Length == functions.Length); if (openAIFunctions.Length > 0) @@ -196,7 +204,8 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); } - options.ToolChoice = ChatCompletionsToolChoice.Auto; + choice = ChatToolChoice.Auto; + tools = []; for (int i = 0; i < openAIFunctions.Length; i++) { // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. @@ -211,29 +220,31 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o } // Add the function. - options.Tools.Add(functions[i]); + tools.Add(functions[i]); } } + + return (tools, choice); } } - /// Represents a that requests the model use a specific function. - internal sealed class RequiredFunction : AzureToolCallBehavior + /// Represents a that requests the model use a specific function. + internal sealed class RequiredFunction : AzureOpenAIToolCallBehavior { private readonly AzureOpenAIFunction _function; - private readonly ChatCompletionsFunctionToolDefinition _tool; - private readonly ChatCompletionsToolChoice _choice; + private readonly ChatTool _tool; + private readonly ChatToolChoice _choice; public RequiredFunction(AzureOpenAIFunction function, bool autoInvoke) : base(autoInvoke) { this._function = function; - this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); - this._choice = new ChatCompletionsToolChoice(this._tool); + this._tool = function.ToFunctionDefinition(); + this._choice = new ChatToolChoice(this._tool); } - public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.Name}"; + public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.FunctionName}"; - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) { bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; @@ -253,8 +264,7 @@ internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions o throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); } - options.ToolChoice = this._choice; - options.Tools.Add(this._tool); + return ([this._tool], this._choice); } /// Gets how many requests are part of a single interaction should include this tool in the request. diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs index e478a301d947..9d771c4f7abb 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextGeneration; +using OpenAI; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -73,7 +74,7 @@ public AzureOpenAIChatCompletionService( /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletionService( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient openAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs similarity index 78% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs index 3857d0191fbe..fd282797e879 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/RequestFailedExceptionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs @@ -1,21 +1,22 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Net; using Azure; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Provides extension methods for the class. +/// Provides extension methods for the class. /// -internal static class RequestFailedExceptionExtensions +internal static class ClientResultExceptionExtensions { /// - /// Converts a to an . + /// Converts a to an . /// /// The original . /// An instance. - public static HttpOperationException ToHttpOperationException(this RequestFailedException exception) + public static HttpOperationException ToHttpOperationException(this ClientResultException exception) { const int NoResponseReceived = 0; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 8e8f53594708..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -13,6 +13,7 @@ + @@ -25,7 +26,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs index 8cbecc909951..ff7183cb0b12 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -2,8 +2,9 @@ using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -13,28 +14,28 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAIChatMessageContent : ChatMessageContent { /// - /// Gets the metadata key for the name property. + /// Gets the metadata key for the tool id. /// - public static string ToolIdProperty => $"{nameof(ChatCompletionsToolCall)}.{nameof(ChatCompletionsToolCall.Id)}"; + public static string ToolIdProperty => "ChatCompletionsToolCall.Id"; /// - /// Gets the metadata key for the list of . + /// Gets the metadata key for the list of . /// - internal static string FunctionToolCallsProperty => $"{nameof(ChatResponseMessage)}.FunctionToolCalls"; + internal static string FunctionToolCallsProperty => "ChatResponseMessage.FunctionToolCalls"; /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelId, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, CreateMetadataDictionary(chatMessage.ToolCalls, metadata)) + internal AzureOpenAIChatMessageContent(OpenAIChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(completion.Role.ToString()), CreateContentItems(completion.Content), modelId, completion, System.Text.Encoding.UTF8, CreateMetadataDictionary(completion.ToolCalls, metadata)) { - this.ToolCalls = chatMessage.ToolCalls; + this.ToolCalls = completion.ToolCalls; } /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(ChatRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + internal AzureOpenAIChatMessageContent(ChatMessageRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; @@ -43,16 +44,32 @@ internal AzureOpenAIChatMessageContent(ChatRole role, string? content, string mo /// /// Initializes a new instance of the class. /// - internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) { this.ToolCalls = toolCalls; } + private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + ChatMessageContentItemCollection collection = []; + + foreach (var part in contentUpdate) + { + // We only support text content for now. + if (part.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new TextContent(part.Text)); + } + } + + return collection; + } + /// /// A list of the tools called by the model. /// - public IReadOnlyList ToolCalls { get; } + public IReadOnlyList ToolCalls { get; } /// /// Retrieve the resulting function from the chat result. @@ -64,7 +81,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() foreach (var toolCall in this.ToolCalls) { - if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + if (toolCall is ChatToolCall functionToolCall) { (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(functionToolCall)); } @@ -79,7 +96,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() } private static IReadOnlyDictionary? CreateMetadataDictionary( - IReadOnlyList toolCalls, + IReadOnlyList toolCalls, IReadOnlyDictionary? original) { // We only need to augment the metadata if there are any tool calls. @@ -107,7 +124,7 @@ public IReadOnlyList GetOpenAIFunctionToolCalls() } // Add the additional entry. - newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); + newDictionary.Add(FunctionToolCallsProperty, toolCalls.Where(ctc => ctc.Kind == ChatToolCallKind.Function).ToList()); return newDictionary; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs index e34b191a83b8..c37321e48c4d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -2,7 +2,6 @@ using System; using System.Net.Http; -using Azure; using Azure.AI.OpenAI; using Azure.Core; using Microsoft.Extensions.Logging; @@ -23,7 +22,7 @@ internal sealed class AzureOpenAIClientCore : ClientCore /// /// OpenAI / Azure OpenAI Client /// - internal override OpenAIClient Client { get; } + internal override AzureOpenAIClient Client { get; } /// /// Initializes a new instance of the class using API Key authentication. @@ -49,7 +48,7 @@ internal AzureOpenAIClientCore( this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, new AzureKeyCredential(apiKey), options); + this.Client = new AzureOpenAIClient(this.Endpoint, apiKey, options); } /// @@ -75,7 +74,7 @@ internal AzureOpenAIClientCore( this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, credential, options); + this.Client = new AzureOpenAIClient(this.Endpoint, credential, options); } /// @@ -84,11 +83,11 @@ internal AzureOpenAIClientCore( /// it's up to the caller to configure the client. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . + /// Custom . /// The to use for logging. If null, no logging will be performed. internal AzureOpenAIClientCore( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient openAIClient, ILogger? logger = null) : base(logger) { Verify.NotNullOrWhiteSpace(deploymentName); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs index 4a3cff49103d..0089b6c29041 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -124,10 +124,10 @@ internal AzureOpenAIFunction( /// /// Converts the representation to the Azure SDK's - /// representation. + /// representation. /// - /// A containing all the function information. - public FunctionDefinition ToFunctionDefinition() + /// A containing all the function information. + public ChatTool ToFunctionDefinition() { BinaryData resultParameters = s_zeroFunctionParametersSchema; @@ -155,12 +155,12 @@ public FunctionDefinition ToFunctionDefinition() }); } - return new FunctionDefinition - { - Name = this.FullyQualifiedName, - Description = this.Description, - Parameters = resultParameters, - }; + return ChatTool.CreateFunctionTool + ( + functionName: this.FullyQualifiedName, + functionDescription: this.Description, + functionParameters: resultParameters + ); } /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs index bea73a474d37..e618f27a9b15 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using System.Text; using System.Text.Json; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -16,15 +16,15 @@ public sealed class AzureOpenAIFunctionToolCall { private string? _fullyQualifiedFunctionName; - /// Initialize the from a . - internal AzureOpenAIFunctionToolCall(ChatCompletionsFunctionToolCall functionToolCall) + /// Initialize the from a . + internal AzureOpenAIFunctionToolCall(ChatToolCall functionToolCall) { Verify.NotNull(functionToolCall); - Verify.NotNull(functionToolCall.Name); + Verify.NotNull(functionToolCall.FunctionName); - string fullyQualifiedFunctionName = functionToolCall.Name; + string fullyQualifiedFunctionName = functionToolCall.FunctionName; string functionName = fullyQualifiedFunctionName; - string? arguments = functionToolCall.Arguments; + string? arguments = functionToolCall.FunctionArguments; string? pluginName = null; int separatorPos = fullyQualifiedFunctionName.IndexOf(AzureOpenAIFunction.NameSeparator, StringComparison.Ordinal); @@ -89,43 +89,43 @@ public override string ToString() /// /// Tracks tooling updates from streaming responses. /// - /// The tool call update to incorporate. + /// The tool call updates to incorporate. /// Lazily-initialized dictionary mapping indices to IDs. /// Lazily-initialized dictionary mapping indices to names. /// Lazily-initialized dictionary mapping indices to arguments. internal static void TrackStreamingToolingUpdate( - StreamingToolCallUpdate? update, + IReadOnlyList? updates, ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) { - if (update is null) + if (updates is null) { // Nothing to track. return; } - // If we have an ID, ensure the index is being tracked. Even if it's not a function update, - // we want to keep track of it so we can send back an error. - if (update.Id is string id) + foreach (var update in updates) { - (toolCallIdsByIndex ??= [])[update.ToolCallIndex] = id; - } + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (update.Id is string id) + { + (toolCallIdsByIndex ??= [])[update.Index] = id; + } - if (update is StreamingFunctionToolCallUpdate ftc) - { // Ensure we're tracking the function's name. - if (ftc.Name is string name) + if (update.FunctionName is string name) { - (functionNamesByIndex ??= [])[ftc.ToolCallIndex] = name; + (functionNamesByIndex ??= [])[update.Index] = name; } // Ensure we're tracking the function's arguments. - if (ftc.ArgumentsUpdate is string argumentsUpdate) + if (update.FunctionArgumentsUpdate is string argumentsUpdate) { - if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(ftc.ToolCallIndex, out StringBuilder? arguments)) + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) { - functionArgumentBuildersByIndex[ftc.ToolCallIndex] = arguments = new(); + functionArgumentBuildersByIndex[update.Index] = arguments = new(); } arguments.Append(argumentsUpdate); @@ -134,20 +134,20 @@ internal static void TrackStreamingToolingUpdate( } /// - /// Converts the data built up by into an array of s. + /// Converts the data built up by into an array of s. /// /// Dictionary mapping indices to IDs. /// Dictionary mapping indices to names. /// Dictionary mapping indices to arguments. - internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + internal static ChatToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) { - ChatCompletionsFunctionToolCall[] toolCalls = []; + ChatToolCall[] toolCalls = []; if (toolCallIdsByIndex is { Count: > 0 }) { - toolCalls = new ChatCompletionsFunctionToolCall[toolCallIdsByIndex.Count]; + toolCalls = new ChatToolCall[toolCallIdsByIndex.Count]; int i = 0; foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) @@ -158,7 +158,7 @@ internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCo functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); - toolCalls[i] = new ChatCompletionsFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); + toolCalls[i] = ChatToolCall.CreateFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); i++; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs index c667183f773c..c903127089dd 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -20,7 +20,7 @@ public static class AzureOpenAIPluginCollectionExtensions /// if the function was found; otherwise, . public static bool TryGetFunctionAndArguments( this IReadOnlyKernelPluginCollection plugins, - ChatCompletionsFunctionToolCall functionToolCall, + ChatToolCall functionToolCall, [NotNullWhen(true)] out KernelFunction? function, out KernelArguments? arguments) => plugins.TryGetFunctionAndArguments(new AzureOpenAIFunctionToolCall(functionToolCall), out function, out arguments); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs index c1843b185f89..9287499e1621 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs @@ -2,8 +2,8 @@ using System.Collections.Generic; using System.Text; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -18,7 +18,7 @@ public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessag /// /// The reason why the completion finished. /// - public CompletionsFinishReason? FinishReason { get; set; } + public ChatFinishReason? FinishReason { get; set; } /// /// Create a new instance of the class. @@ -28,21 +28,22 @@ public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessag /// The model ID used to generate the content /// Additional metadata internal AzureOpenAIStreamingChatMessageContent( - StreamingChatCompletionsUpdate chatUpdate, + StreamingChatCompletionUpdate chatUpdate, int choiceIndex, string modelId, IReadOnlyDictionary? metadata = null) : base( chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, - chatUpdate.ContentUpdate, + null, chatUpdate, choiceIndex, modelId, Encoding.UTF8, metadata) { - this.ToolCallUpdate = chatUpdate.ToolCallUpdate; - this.FinishReason = chatUpdate?.FinishReason; + this.ToolCallUpdate = chatUpdate.ToolCallUpdates; + this.FinishReason = chatUpdate.FinishReason; + this.Items = CreateContentItems(chatUpdate.ContentUpdate); } /// @@ -58,8 +59,8 @@ internal AzureOpenAIStreamingChatMessageContent( internal AzureOpenAIStreamingChatMessageContent( AuthorRole? authorRole, string? content, - StreamingToolCallUpdate? tootToolCallUpdate = null, - CompletionsFinishReason? completionsFinishReason = null, + IReadOnlyList? tootToolCallUpdate = null, + ChatFinishReason? completionsFinishReason = null, int choiceIndex = 0, string? modelId = null, IReadOnlyDictionary? metadata = null) @@ -77,11 +78,27 @@ internal AzureOpenAIStreamingChatMessageContent( } /// Gets any update information in the message about a tool call. - public StreamingToolCallUpdate? ToolCallUpdate { get; } + public IReadOnlyList? ToolCallUpdate { get; } /// public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); /// public override string ToString() => this.Content ?? string.Empty; + + private static StreamingKernelContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + StreamingKernelContentItemCollection collection = []; + + foreach (var content in contentUpdate) + { + // We only support text content for now. + if (content.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new StreamingTextContent(content.Text)); + } + } + + return collection; + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index dda7578da8ea..6486d7348144 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Metrics; @@ -11,15 +13,17 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Azure; using Azure.AI.OpenAI; -using Azure.Core; -using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; +using OpenAI; +using OpenAI.Audio; +using OpenAI.Chat; +using OpenAI.Embeddings; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; #pragma warning disable CA2208 // Instantiate argument exceptions correctly @@ -30,8 +34,11 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// internal abstract class ClientCore { + private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; + private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; + private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; private const string ModelProvider = "openai"; - private const int MaxResultsPerPrompt = 128; + private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); /// /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current @@ -52,7 +59,7 @@ internal abstract class ClientCore private const int MaxInflightAutoInvokes = 128; /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatCompletionsFunctionToolDefinition s_nonInvocableFunctionTool = new() { Name = "NonInvocableTool" }; + private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); /// Tracking for . private static readonly AsyncLocal s_inflightAutoInvokes = new(); @@ -70,7 +77,7 @@ internal ClientCore(ILogger? logger = null) /// /// OpenAI / Azure OpenAI Client /// - internal abstract OpenAIClient Client { get; } + internal abstract AzureOpenAIClient Client { get; } internal Uri? Endpoint { get; set; } = null; @@ -116,171 +123,35 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - /// - /// Creates completions for the prompt and settings. - /// - /// The prompt to complete. - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// The to monitor for cancellation requests. The default is . - /// Completions generated by the remote model - internal async Task> GetTextResultsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - ValidateMaxTokens(textExecutionSettings.MaxTokens); - - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - Completions? responseData = null; - List responseContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) - { - try - { - responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; - if (responseData.Choices.Count == 0) - { - throw new KernelException("Text completions not found"); - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (responseData != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); - } - throw; - } - - responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); - } - - this.LogUsage(responseData.Usage); - - return responseContent; - } - - internal async IAsyncEnumerable GetStreamingTextContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings textExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, AzureOpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - ValidateMaxTokens(textExecutionSettings.MaxTokens); - - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); - - StreamingResponse response; - try - { - response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - Completions completions = responseEnumerator.Current; - foreach (Choice choice in completions.Choices) - { - var openAIStreamingTextContent = new AzureOpenAIStreamingTextContent( - choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); - streamedContents?.Add(openAIStreamingTextContent); - yield return openAIStreamingTextContent; - } - } - } - finally - { - activity?.EndStreaming(streamedContents); - await responseEnumerator.DisposeAsync(); - } - } - - private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) - { - return new Dictionary(8) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, - { nameof(completions.Usage), completions.Usage }, - { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, - - { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, - { nameof(choice.Index), choice.Index }, - }; - } - - private static Dictionary GetChatChoiceMetadata(ChatCompletions completions, ChatChoice chatChoice) + private static Dictionary GetChatChoiceMetadata(OpenAIChatCompletion completions) { +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return new Dictionary(12) { { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, + { nameof(completions.CreatedAt), completions.CreatedAt }, + { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, { nameof(completions.Usage), completions.Usage }, - { nameof(chatChoice.ContentFilterResults), chatChoice.ContentFilterResults }, + { ContentFilterResultsMetadataKey, completions.GetContentFilterResultForResponse() }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(chatChoice.FinishReason), chatChoice.FinishReason?.ToString() }, - - { nameof(chatChoice.FinishDetails), chatChoice.FinishDetails }, - { nameof(chatChoice.LogProbabilityInfo), chatChoice.LogProbabilityInfo }, - { nameof(chatChoice.Index), chatChoice.Index }, - { nameof(chatChoice.Enhancements), chatChoice.Enhancements }, + { nameof(completions.FinishReason), completions.FinishReason.ToString() }, + { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, }; +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } - private static Dictionary GetResponseMetadata(StreamingChatCompletionsUpdate completions) + private static Dictionary GetResponseMetadata(StreamingChatCompletionUpdate completionUpdate) { return new Dictionary(4) { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completionUpdate.Id), completionUpdate.Id }, + { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, + { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completions.FinishReason), completions.FinishReason?.ToString() }, + { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, }; } @@ -312,13 +183,13 @@ internal async Task>> GetEmbeddingsAsync( if (data.Count > 0) { - var embeddingsOptions = new EmbeddingsOptions(this.DeploymentOrModelName, data) + var embeddingsOptions = new EmbeddingGenerationOptions() { Dimensions = dimensions }; - var response = await RunRequestAsync(() => this.Client.GetEmbeddingsAsync(embeddingsOptions, cancellationToken)).ConfigureAwait(false); - var embeddings = response.Value.Data; + var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentOrModelName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var embeddings = response.Value; if (embeddings.Count != data.Count) { @@ -327,7 +198,7 @@ internal async Task>> GetEmbeddingsAsync( for (var i = 0; i < embeddings.Count; i++) { - result.Add(embeddings[i].Embedding); + result.Add(embeddings[i].Vector); } } @@ -382,30 +253,36 @@ internal async Task> GetChatMessageContentsAsy { Verify.NotNull(chat); + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + // Convert the incoming execution settings to OpenAI settings. AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + ValidateMaxTokens(chatExecutionSettings.MaxTokens); - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - // Create the Azure SDK ChatCompletionOptions instance from all available information. - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); + var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); - for (int requestIndex = 1; ; requestIndex++) + for (int requestIndex = 0; ; requestIndex++) { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + // Make the request. - ChatCompletions? responseData = null; - List responseContent; + OpenAIChatCompletion? responseData = null; + AzureOpenAIChatMessageContent responseContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { try { - responseData = (await RunRequestAsync(() => this.Client.GetChatCompletionsAsync(chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + responseData = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatMessages, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + this.LogUsage(responseData.Usage); - if (responseData.Choices.Count == 0) - { - throw new KernelException("Chat completions not found"); - } } catch (Exception ex) when (activity is not null) { @@ -415,21 +292,20 @@ internal async Task> GetChatMessageContentsAsy // Capture available metadata even if the operation failed. activity .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); + .SetPromptTokenUsage(responseData.Usage.InputTokens) + .SetCompletionTokenUsage(responseData.Usage.OutputTokens); } throw; } - responseContent = responseData.Choices.Select(chatChoice => this.GetChatMessage(chatChoice, responseData)).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); + responseContent = this.GetChatMessage(responseData); + activity?.SetCompletionResponse([responseContent], responseData.Usage.InputTokens, responseData.Usage.OutputTokens); } // If we don't want to attempt to invoke any functions, just return the result. - // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. - if (!autoInvoke || responseData.Choices.Count != 1) + if (!toolCallingConfig.AutoInvoke) { - return responseContent; + return [responseContent]; } Debug.Assert(kernel is not null); @@ -439,51 +315,49 @@ internal async Task> GetChatMessageContentsAsy // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool // is specified. - ChatChoice resultChoice = responseData.Choices[0]; - AzureOpenAIChatMessageContent result = this.GetChatMessage(resultChoice, responseData); - if (result.ToolCalls.Count == 0) + if (responseData.ToolCalls.Count == 0) { - return [result]; + return [responseContent]; } if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Tool requests: {Requests}", result.ToolCalls.Count); + this.Logger.LogDebug("Tool requests: {Requests}", responseData.ToolCalls.Count); } if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", result.ToolCalls.OfType().Select(ftc => $"{ftc.Name}({ftc.Arguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", responseData.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); } - // Add the original assistant message to the chatOptions; this is required for the service + // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. Also add the result message to the caller's chat // history: if they don't want it, they can remove it, but this makes the data available, // including metadata like usage. - chatOptions.Messages.Add(GetRequestMessage(resultChoice.Message)); - chat.Add(result); + chatMessages.Add(GetRequestMessage(responseData)); + chat.Add(responseContent); // We must send back a response for every tool call, regardless of whether we successfully executed it or not. // If we successfully execute it, we'll add the result. If we don't, we'll add an error. - for (int toolCallIndex = 0; toolCallIndex < result.ToolCalls.Count; toolCallIndex++) + for (int toolCallIndex = 0; toolCallIndex < responseContent.ToolCalls.Count; toolCallIndex++) { - ChatCompletionsToolCall toolCall = result.ToolCalls[toolCallIndex]; + ChatToolCall functionToolCall = responseContent.ToolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (toolCall is not ChatCompletionsFunctionToolCall functionToolCall) + if (functionToolCall.Kind != ChatToolCallKind.Function) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); continue; } // Parse the function call arguments. - AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + AzureOpenAIFunctionToolCall? azureOpenAIFunctionToolCall; try { - openAIFunctionToolCall = new(functionToolCall); + azureOpenAIFunctionToolCall = new(functionToolCall); } catch (JsonException) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); continue; } @@ -491,16 +365,16 @@ internal async Task> GetChatMessageContentsAsy // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; } @@ -509,9 +383,9 @@ internal async Task> GetChatMessageContentsAsy AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) { Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, + RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, - FunctionCount = result.ToolCalls.Count + FunctionCount = responseContent.ToolCalls.Count }; s_inflightAutoInvokes.Value++; @@ -535,7 +409,7 @@ internal async Task> GetChatMessageContentsAsy catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatOptions, chat, null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); continue; } finally @@ -549,7 +423,7 @@ internal async Task> GetChatMessageContentsAsy object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); // If filter requested termination, returning latest function result. if (invocationContext.Terminate) @@ -562,46 +436,6 @@ internal async Task> GetChatMessageContentsAsy return [chat.Last()]; } } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } } } @@ -613,22 +447,30 @@ internal async IAsyncEnumerable GetStrea { Verify.NotNull(chat); + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); ValidateMaxTokens(chatExecutionSettings.MaxTokens); - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - StringBuilder? contentBuilder = null; Dictionary? toolCallIdsByIndex = null; Dictionary? functionNamesByIndex = null; Dictionary? functionArgumentBuildersByIndex = null; - for (int requestIndex = 1; ; requestIndex++) + var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + // Reset state contentBuilder?.Clear(); toolCallIdsByIndex?.Clear(); @@ -638,18 +480,18 @@ internal async IAsyncEnumerable GetStrea // Stream the response. IReadOnlyDictionary? metadata = null; string? streamedName = null; - ChatRole? streamedRole = default; - CompletionsFinishReason finishReason = default; - ChatCompletionsFunctionToolCall[]? toolCalls = null; + ChatMessageRole? streamedRole = default; + ChatFinishReason finishReason = default; + ChatToolCall[]? toolCalls = null; FunctionCallContent[]? functionCallContents = null; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { // Make the request. - StreamingResponse response; + AsyncResultCollection response; try { - response = await RunRequestAsync(() => this.Client.GetChatCompletionsStreamingAsync(chatOptions, cancellationToken)).ConfigureAwait(false); + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatMessages, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -676,32 +518,44 @@ internal async IAsyncEnumerable GetStrea throw; } - StreamingChatCompletionsUpdate update = responseEnumerator.Current; + StreamingChatCompletionUpdate update = responseEnumerator.Current; metadata = GetResponseMetadata(update); streamedRole ??= update.Role; - streamedName ??= update.AuthorName; + //streamedName ??= update.AuthorName; finishReason = update.FinishReason ?? default; // If we're intending to invoke function calls, we need to consume that function call information. - if (autoInvoke) + if (toolCallingConfig.AutoInvoke) { - if (update.ContentUpdate is { Length: > 0 } contentUpdate) + foreach (var contentPart in update.ContentUpdate) { - (contentBuilder ??= new()).Append(contentUpdate); + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, update.ChoiceIndex ?? 0, this.DeploymentOrModelName, metadata) { AuthorName = streamedName }; + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, 0, this.DeploymentOrModelName, metadata); - if (update.ToolCallUpdate is StreamingFunctionToolCallUpdate functionCallUpdate) + foreach (var functionCallUpdate in update.ToolCallUpdates) { + // Using the code below to distinguish and skip non - function call related updates. + // The Kind property of updates can't be reliably used because it's only initialized for the first update. + if (string.IsNullOrEmpty(functionCallUpdate.Id) && + string.IsNullOrEmpty(functionCallUpdate.FunctionName) && + string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) + { + continue; + } + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( callId: functionCallUpdate.Id, - name: functionCallUpdate.Name, - arguments: functionCallUpdate.ArgumentsUpdate, - functionCallIndex: functionCallUpdate.ToolCallIndex)); + name: functionCallUpdate.FunctionName, + arguments: functionCallUpdate.FunctionArgumentsUpdate, + functionCallIndex: functionCallUpdate.Index)); } streamedContents?.Add(openAIStreamingChatMessageContent); @@ -726,7 +580,7 @@ internal async IAsyncEnumerable GetStrea // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool // is specified. - if (!autoInvoke || + if (!toolCallingConfig.AutoInvoke || toolCallIdsByIndex is not { Count: > 0 }) { yield break; @@ -738,27 +592,27 @@ internal async IAsyncEnumerable GetStrea // Log the requests if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.Name}({fcr.Arguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); } else if (this.Logger.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); } - // Add the original assistant message to the chatOptions; this is required for the service + // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. - chatOptions.Messages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chatMessages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); chat.Add(this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); // Respond to each tooling request. for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) { - ChatCompletionsFunctionToolCall toolCall = toolCalls[toolCallIndex]; + ChatToolCall toolCall = toolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (string.IsNullOrEmpty(toolCall.Name)) + if (string.IsNullOrEmpty(toolCall.FunctionName)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -770,7 +624,7 @@ internal async IAsyncEnumerable GetStrea } catch (JsonException) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -780,14 +634,14 @@ internal async IAsyncEnumerable GetStrea if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -796,7 +650,7 @@ internal async IAsyncEnumerable GetStrea AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) { Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, + RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, FunctionCount = toolCalls.Length }; @@ -822,7 +676,7 @@ internal async IAsyncEnumerable GetStrea catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatOptions, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -836,7 +690,7 @@ internal async IAsyncEnumerable GetStrea object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, toolCall, this.Logger); + AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, toolCall, this.Logger); // If filter requested termination, returning latest function result and breaking request iteration loop. if (invocationContext.Terminate) @@ -852,57 +706,17 @@ internal async IAsyncEnumerable GetStrea yield break; } } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } } } /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionsOptions options, AzureOpenAIFunctionToolCall ftc) + private static bool IsRequestableTool(ChatCompletionOptions options, AzureOpenAIFunctionToolCall ftc) { - IList tools = options.Tools; + IList tools = options.Tools; for (int i = 0; i < tools.Count; i++) { - if (tools[i] is ChatCompletionsFunctionToolDefinition def && - string.Equals(def.Name, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + if (tools[i].Kind == ChatToolKind.Function && + string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) { return true; } @@ -950,22 +764,21 @@ internal void AddAttribute(string key, string? value) /// Gets options to use for an OpenAIClient /// Custom for HTTP requests. - /// Optional API version. /// An instance of . - internal static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, OpenAIClientOptions.ServiceVersion? serviceVersion = null) + internal static AzureOpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient) { - OpenAIClientOptions options = serviceVersion is not null ? - new(serviceVersion.Value) : - new(); + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + }; - options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), HttpPipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); if (httpClient is not null) { - options.Transport = new HttpClientTransport(httpClient); - options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout } return options; @@ -998,129 +811,44 @@ private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptE return chat; } - private static CompletionsOptions CreateCompletionsOptions(string text, AzureOpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) - { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } - - var options = new CompletionsOptions - { - Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - Echo = false, - ChoicesPerPrompt = executionSettings.ResultsPerPrompt, - GenerationSampleCount = executionSettings.ResultsPerPrompt, - LogProbabilityCount = executionSettings.TopLogprobs, - User = executionSettings.User, - DeploymentName = deploymentOrModelName - }; - - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); - } - } - - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } - - return options; - } - - private ChatCompletionsOptions CreateChatCompletionsOptions( + private ChatCompletionOptions CreateChatCompletionsOptions( AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, - Kernel? kernel, - string deploymentOrModelName) + ToolCallingConfig toolCallingConfig, + Kernel? kernel) { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chatHistory), - JsonSerializer.Serialize(executionSettings)); - } - - var options = new ChatCompletionsOptions + var options = new ChatCompletionOptions { MaxTokens = executionSettings.MaxTokens, Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, + TopP = (float?)executionSettings.TopP, FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, PresencePenalty = (float?)executionSettings.PresencePenalty, - ChoiceCount = executionSettings.ResultsPerPrompt, - DeploymentName = deploymentOrModelName, Seed = executionSettings.Seed, User = executionSettings.User, - LogProbabilitiesPerToken = executionSettings.TopLogprobs, - EnableLogProbabilities = executionSettings.Logprobs, - AzureExtensionsOptions = executionSettings.AzureChatExtensionsOptions + TopLogProbabilityCount = executionSettings.TopLogprobs, + IncludeLogProbabilities = executionSettings.Logprobs, + ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, + ToolChoice = toolCallingConfig.Choice, }; - switch (executionSettings.ResponseFormat) + if (executionSettings.AzureChatDataSource is not null) { - case ChatCompletionsResponseFormat formatObject: - // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. - options.ResponseFormat = formatObject; - break; - - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; - - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + options.AddDataSource(executionSettings.AzureChatDataSource); +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - } - break; + if (toolCallingConfig.Tools is { Count: > 0 } tools) + { + options.Tools.AddRange(tools); } - executionSettings.ToolCallBehavior?.ConfigureOptions(kernel, options); if (executionSettings.TokenSelectionBiases is not null) { foreach (var keyValue in executionSettings.TokenSelectionBiases) { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); + options.LogitBiases.Add(keyValue.Key, keyValue.Value); } } @@ -1132,52 +860,51 @@ private ChatCompletionsOptions CreateChatCompletionsOptions( } } + return options; + } + + private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) + { + List messages = []; + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) { - options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); + messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); } foreach (var message in chatHistory) { - options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); } - return options; + return messages; } - private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string contents, string? name, ChatCompletionsFunctionToolCall[]? tools) + private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) { - if (chatRole == ChatRole.User) + if (chatRole == ChatMessageRole.User) { - return new ChatRequestUserMessage(contents) { Name = name }; + return new UserChatMessage(content) { ParticipantName = name }; } - if (chatRole == ChatRole.System) + if (chatRole == ChatMessageRole.System) { - return new ChatRequestSystemMessage(contents) { Name = name }; + return new SystemChatMessage(content) { ParticipantName = name }; } - if (chatRole == ChatRole.Assistant) + if (chatRole == ChatMessageRole.Assistant) { - var msg = new ChatRequestAssistantMessage(contents) { Name = name }; - if (tools is not null) - { - foreach (ChatCompletionsFunctionToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - return msg; + return new AssistantChatMessage(tools, content) { ParticipantName = name }; } throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static List GetRequestMessages(ChatMessageContent message, AzureToolCallBehavior? toolCallBehavior) + private static List GetRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { - return [new ChatRequestSystemMessage(message.Content) { Name = message.AuthorName }]; + return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; } if (message.Role == AuthorRole.Tool) @@ -1187,12 +914,12 @@ private static List GetRequestMessages(ChatMessageContent me if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && toolId?.ToString() is string toolIdString) { - return [new ChatRequestToolMessage(message.Content, toolIdString)]; + return [new ToolChatMessage(toolIdString, message.Content)]; } // Handling function results represented by the FunctionResultContent type. // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - List? toolMessages = null; + List? toolMessages = null; foreach (var item in message.Items) { if (item is not FunctionResultContent resultContent) @@ -1204,13 +931,13 @@ private static List GetRequestMessages(ChatMessageContent me if (resultContent.Result is Exception ex) { - toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.CallId)); + toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); continue; } var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); - toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.CallId)); + toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); } if (toolMessages is not null) @@ -1225,33 +952,33 @@ private static List GetRequestMessages(ChatMessageContent me { if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) { - return [new ChatRequestUserMessage(textContent.Text) { Name = message.AuthorName }]; + return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; } - return [new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch + return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch { - TextContent textContent => new ChatMessageTextContentItem(textContent.Text), + TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), ImageContent imageContent => GetImageContentItem(imageContent), _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") }))) - { Name = message.AuthorName }]; + { ParticipantName = message.AuthorName }]; } if (message.Role == AuthorRole.Assistant) { - var asstMessage = new ChatRequestAssistantMessage(message.Content) { Name = message.AuthorName }; + var toolCalls = new List(); // Handling function calls supplied via either: // ChatCompletionsToolCall.ToolCalls collection items or // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; + IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) { - tools = toolCallsObject as IEnumerable; + tools = toolCallsObject as IEnumerable; if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) { int length = array.GetArrayLength(); - var ftcs = new List(length); + var ftcs = new List(length); for (int i = 0; i < length; i++) { JsonElement e = array[i]; @@ -1262,7 +989,7 @@ private static List GetRequestMessages(ChatMessageContent me name.ValueKind == JsonValueKind.String && arguments.ValueKind == JsonValueKind.String) { - ftcs.Add(new ChatCompletionsFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); + ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); } } tools = ftcs; @@ -1271,7 +998,7 @@ private static List GetRequestMessages(ChatMessageContent me if (tools is not null) { - asstMessage.ToolCalls.AddRange(tools); + toolCalls.AddRange(tools); } // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. @@ -1283,7 +1010,7 @@ private static List GetRequestMessages(ChatMessageContent me continue; } - functionCallIds ??= new HashSet(asstMessage.ToolCalls.Select(t => t.Id)); + functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) { @@ -1292,69 +1019,60 @@ private static List GetRequestMessages(ChatMessageContent me var argument = JsonSerializer.Serialize(callRequest.Arguments); - asstMessage.ToolCalls.Add(new ChatCompletionsFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); + toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); } - return [asstMessage]; + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; } throw new NotSupportedException($"Role {message.Role} is not supported."); } - private static ChatMessageImageContentItem GetImageContentItem(ImageContent imageContent) + private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) { if (imageContent.Data is { IsEmpty: false } data) { - return new ChatMessageImageContentItem(BinaryData.FromBytes(data), imageContent.MimeType); + return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); } if (imageContent.Uri is not null) { - return new ChatMessageImageContentItem(imageContent.Uri); + return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); } throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatRequestMessage GetRequestMessage(ChatResponseMessage message) + private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) { - if (message.Role == ChatRole.System) + if (completion.Role == ChatMessageRole.System) { - return new ChatRequestSystemMessage(message.Content); + return ChatMessage.CreateSystemMessage(completion.Content[0].Text); } - if (message.Role == ChatRole.Assistant) + if (completion.Role == ChatMessageRole.Assistant) { - var msg = new ChatRequestAssistantMessage(message.Content); - if (message.ToolCalls is { Count: > 0 } tools) - { - foreach (ChatCompletionsToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - - return msg; + return ChatMessage.CreateAssistantMessage(completion); } - if (message.Role == ChatRole.User) + if (completion.Role == ChatMessageRole.User) { - return new ChatRequestUserMessage(message.Content); + return ChatMessage.CreateUserMessage(completion.Content); } - throw new NotSupportedException($"Role {message.Role} is not supported."); + throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompletions responseData) + private AzureOpenAIChatMessageContent GetChatMessage(OpenAIChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice)); + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); - message.Items.AddRange(this.GetFunctionCallContents(chatChoice.Message.ToolCalls)); + message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); return message; } - private AzureOpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string content, ChatCompletionsFunctionToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + private AzureOpenAIChatMessageContent GetChatMessage(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) { @@ -1369,21 +1087,21 @@ private AzureOpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string c return message; } - private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) + private List GetFunctionCallContents(IEnumerable toolCalls) { - List? result = null; + List result = []; foreach (var toolCall in toolCalls) { // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. // This allows consumers to work with functions in an LLM-agnostic way. - if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) + if (toolCall.Kind == ChatToolCallKind.Function) { Exception? exception = null; KernelArguments? arguments = null; try { - arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); + arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); if (arguments is not null) { // Iterate over copy of the names to avoid mutating the dictionary while enumerating it @@ -1400,31 +1118,30 @@ private IEnumerable GetFunctionCallContents(IEnumerable(); + return result; } - private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, ChatCompletionsToolCall toolCall, ILogger logger) + private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) { // Log any error if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) @@ -1433,19 +1150,19 @@ private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatH logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); } - // Add the tool response message to the chat options + // Add the tool response message to the chat messages result ??= errorMessage ?? string.Empty; - chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); + chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); // Add the tool response message to the chat history. var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); - if (toolCall is ChatCompletionsFunctionToolCall functionCall) + if (toolCall.Kind == ChatToolCallKind.Function) { // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(functionCall.Name, AzureOpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); + var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); } chat.Add(message); @@ -1459,23 +1176,25 @@ private static void ValidateMaxTokens(int? maxTokens) } } - private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) + private static async Task RunRequestAsync(Func> request) { - if (autoInvoke && resultsPerPrompt != 1) + try { - // We can remove this restriction in the future if valuable. However, multiple results per prompt is rare, - // and limiting this significantly curtails the complexity of the implementation. - throw new ArgumentException($"Auto-invocation of tool calls may only be used with a {nameof(AzureOpenAIPromptExecutionSettings.ResultsPerPrompt)} of 1."); + return await request.Invoke().ConfigureAwait(false); + } + catch (ClientResultException e) + { + throw e.ToHttpOperationException(); } } - private static async Task RunRequestAsync(Func> request) + private static T RunRequest(Func request) { try { - return await request.Invoke().ConfigureAwait(false); + return request.Invoke(); } - catch (RequestFailedException e) + catch (ClientResultException e) { throw e.ToHttpOperationException(); } @@ -1484,8 +1203,8 @@ private static async Task RunRequestAsync(Func> request) /// /// Captures usage details, including token information. /// - /// Instance of with usage details. - private void LogUsage(CompletionsUsage usage) + /// Instance of with token usage details. + private void LogUsage(ChatTokenUsage usage) { if (usage is null) { @@ -1496,12 +1215,12 @@ private void LogUsage(CompletionsUsage usage) if (this.Logger.IsEnabled(LogLevel.Information)) { this.Logger.LogInformation( - "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", - usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens); + "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", + usage.InputTokens, usage.OutputTokens, usage.TotalTokens); } - s_promptTokensCounter.Add(usage.PromptTokens); - s_completionTokensCounter.Add(usage.CompletionTokens); + s_promptTokensCounter.Add(usage.InputTokens); + s_completionTokensCounter.Add(usage.OutputTokens); s_totalTokensCounter.Add(usage.TotalTokens); } @@ -1511,7 +1230,7 @@ private void LogUsage(CompletionsUsage usage) /// The result of the function call. /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, AzureToolCallBehavior? toolCallBehavior) + private static string? ProcessFunctionResult(object functionResult, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (functionResult is string stringResult) { @@ -1571,4 +1290,95 @@ await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context await functionCallCallback(context).ConfigureAwait(false); } } + + private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, AzureOpenAIPromptExecutionSettings executionSettings, int requestIndex) + { + if (executionSettings.ToolCallBehavior is null) + { + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); + + bool autoInvoke = kernel is not null && + executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && + s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + + return new ToolCallingConfig( + Tools: tools ?? [s_nonInvocableFunctionTool], + Choice: choice ?? ChatToolChoice.None, + AutoInvoke: autoInvoke); + } + + private static ChatResponseFormat? GetResponseFormat(AzureOpenAIPromptExecutionSettings executionSettings) + { + switch (executionSettings.ResponseFormat) + { + case ChatResponseFormat formatObject: + // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. + return formatObject; + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + } + break; + } + + return null; + } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + { + return new GenericActionPipelinePolicy((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + } } From 6af09e21bc633123032d5032313fc762df893ff6 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 28 Jun 2024 20:37:46 +0100 Subject: [PATCH 011/226] .Net: Extension methods & integration tests for AzureOpenAIChatCompletionService v2 (#7003) ### Motivation and Context This PR is the next step in a series of follow-up PRs to migrate the AzureOpenAIChatCompletion service to the Azure AI SDK v2. It adds extension methods for the service collection and kernel builder to create and register the AzureOpenAIChatCompletionService. Additionally, it includes integration tests for the service. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --------- Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- ...eOpenAIServiceCollectionExtensionsTests.cs | 63 ++ ...enAIServiceKernelBuilderExtensionsTests.cs | 63 ++ .../ChatHistoryExtensionsTests.cs | 2 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 2 +- .../AzureOpenAIServiceCollectionExtensions.cs | 249 ++++++ .../AzureOpenAIChatCompletionTests.cs | 273 ++++++ ...enAIChatCompletion_FunctionCallingTests.cs | 781 ++++++++++++++++++ ...eOpenAIChatCompletion_NonStreamingTests.cs | 180 ++++ ...zureOpenAIChatCompletion_StreamingTests.cs | 174 ++++ .../IntegrationTestsV2.csproj | 1 + dotnet/src/IntegrationTestsV2/TestHelpers.cs | 55 ++ .../TestSettings/AzureOpenAIConfiguration.cs | 19 + 12 files changed, 1860 insertions(+), 2 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs rename dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/{ => Extensions}/ChatHistoryExtensionsTests.cs (95%) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/TestHelpers.cs create mode 100644 dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs new file mode 100644 index 000000000000..041cee3f3cc9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.TextGeneration; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIServiceCollectionExtensionsTests +{ + #region Chat completion + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAIChatCompletion("deployment-name"), + _ => builder.Services + }; + + // Assert + var chatCompletionService = builder.Build().GetRequiredService(); + Assert.True(chatCompletionService is AzureOpenAIChatCompletionService); + + var textGenerationService = builder.Build().GetRequiredService(); + Assert.True(textGenerationService is AzureOpenAIChatCompletionService); + } + + #endregion + + public enum InitializationType + { + ApiKey, + TokenCredential, + OpenAIClientInline, + OpenAIClientInServiceProvider, + OpenAIClientEndpoint, + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs new file mode 100644 index 000000000000..6025eb1d447f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.TextGeneration; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIServiceKernelBuilderExtensionsTests +{ + #region Chat completion + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAIChatCompletion("deployment-name"), + _ => builder + }; + + // Assert + var chatCompletionService = builder.Build().GetRequiredService(); + Assert.True(chatCompletionService is AzureOpenAIChatCompletionService); + + var textGenerationService = builder.Build().GetRequiredService(); + Assert.True(textGenerationService is AzureOpenAIChatCompletionService); + } + + #endregion + + public enum InitializationType + { + ApiKey, + TokenCredential, + OpenAIClientInline, + OpenAIClientInServiceProvider, + OpenAIClientEndpoint, + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs similarity index 95% rename from dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs index a0579f6d6c72..94fc1e5d1a5c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatHistoryExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs @@ -7,7 +7,7 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; public class ChatHistoryExtensionsTests { [Fact] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 6486d7348144..4152f2137409 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -126,7 +126,7 @@ internal ClientCore(ILogger? logger = null) private static Dictionary GetChatChoiceMetadata(OpenAIChatCompletion completions) { #pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - return new Dictionary(12) + return new Dictionary(8) { { nameof(completions.Id), completions.Id }, { nameof(completions.CreatedAt), completions.CreatedAt }, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs new file mode 100644 index 000000000000..782889c4542c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using Azure; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextGeneration; + +#pragma warning disable IDE0039 // Use local function + +namespace Microsoft.SemanticKernel; + +/// +/// Provides extension methods for and related classes to configure Azure OpenAI connectors. +/// +public static class AzureOpenAIServiceCollectionExtensions +{ + #region Chat Completion + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + new AzureKeyCredential(apiKey), + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + public static IServiceCollection AddAzureOpenAIChatCompletion( + this IServiceCollection services, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + new AzureKeyCredential(apiKey), + HttpClientProvider.GetHttpClient(serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + services.AddKeyedSingleton(serviceId, factory); + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + credentials, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + public static IServiceCollection AddAzureOpenAIChatCompletion( + this IServiceCollection services, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + credentials, + HttpClientProvider.GetHttpClient(serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + services.AddKeyedSingleton(serviceId, factory); + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + + Func factory = (serviceProvider, _) => + new(deploymentName, azureOpenAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + public static IServiceCollection AddAzureOpenAIChatCompletion( + this IServiceCollection services, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + + Func factory = (serviceProvider, _) => + new(deploymentName, azureOpenAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, factory); + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + #endregion + + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => + new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); + + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => + new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs new file mode 100644 index 000000000000..04f1be7e45c7 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -0,0 +1,273 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class AzureOpenAIChatCompletionTests +{ + [Fact] + //[Fact(Skip = "Skipping while we investigate issue with GitHub actions.")] + public async Task ItCanUseAzureOpenAiChatForTextGenerationAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var func = kernel.CreateFunctionFromPrompt( + "List the two planets after '{{$input}}', excluding moons, using bullet points.", + new AzureOpenAIPromptExecutionSettings()); + + // Act + var result = await func.InvokeAsync(kernel, new() { [InputParameterName] = "Jupiter" }); + + // Assert + Assert.NotNull(result); + Assert.Contains("Saturn", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + Assert.Contains("Uranus", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task AzureOpenAIStreamingTestAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "ChatPlugin"); + + StringBuilder fullResult = new(); + + var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; + + // Act + await foreach (var content in kernel.InvokeStreamingAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt })) + { + fullResult.Append(content); + } + + // Assert + Assert.Contains("Pike Place", fullResult.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task AzureOpenAIHttpRetryPolicyTestAsync() + { + // Arrange + List statusCodes = []; + + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + + this._kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration!.ChatDeploymentName!, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: "INVALID_KEY"); + + this._kernelBuilder.Services.ConfigureHttpClientDefaults(c => + { + // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example + c.AddStandardResilienceHandler().Configure(o => + { + o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.Unauthorized); + o.Retry.OnRetry = args => + { + statusCodes.Add(args.Outcome.Result?.StatusCode); + return ValueTask.CompletedTask; + }; + }); + }); + + var target = this._kernelBuilder.Build(); + + var plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); + + var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; + + // Act + var exception = await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = prompt })); + + // Assert + Assert.All(statusCodes, s => Assert.Equal(HttpStatusCode.Unauthorized, s)); + Assert.Equal(HttpStatusCode.Unauthorized, ((HttpOperationException)exception).StatusCode); + } + + [Fact] + public async Task AzureOpenAIShouldReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); + + // Act + var result = await kernel.InvokeAsync(plugins["FunPlugin"]["Limerick"]); + + // Assert + Assert.NotNull(result.Metadata); + + // Usage + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + + // ContentFilterResults + Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + } + + [Theory(Skip = "This test is for manual verification.")] + [InlineData("\n")] + [InlineData("\r\n")] + public async Task CompletionWithDifferentLineEndingsAsync(string lineEnding) + { + // Arrange + var prompt = + "Given a json input and a request. Apply the request on the json input and return the result. " + + $"Put the result in between tags{lineEnding}" + + $$"""Input:{{lineEnding}}{"name": "John", "age": 30}{{lineEnding}}{{lineEnding}}Request:{{lineEnding}}name"""; + + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "ChatPlugin"); + + // Act + FunctionResult actual = await kernel.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); + + // Assert + Assert.Contains("John", actual.GetValue(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ChatSystemPromptIsNotIgnoredAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await kernel.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?", new(settings)); + + // Assert + Assert.Contains("I don't know", result.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task SemanticKernelVersionHeaderIsSentAsync() + { + // Arrange + using var defaultHandler = new HttpClientHandler(); + using var httpHeaderHandler = new HttpHeaderHandler(defaultHandler); + using var httpClient = new HttpClient(httpHeaderHandler); + + var kernel = this.CreateAndInitializeKernel(httpClient); + + // Act + var result = await kernel.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?"); + + // Assert + Assert.NotNull(httpHeaderHandler.RequestHeaders); + Assert.True(httpHeaderHandler.RequestHeaders.TryGetValues("Semantic-Kernel-Version", out var values)); + } + + //[Theory(Skip = "This test is for manual verification.")] + [Theory] + [InlineData(null, null)] + [InlineData(false, null)] + [InlineData(true, 2)] + [InlineData(true, 5)] + public async Task LogProbsDataIsReturnedWhenRequestedAsync(bool? logprobs, int? topLogprobs) + { + // Arrange + var settings = new AzureOpenAIPromptExecutionSettings { Logprobs = logprobs, TopLogprobs = topLogprobs }; + + var kernel = this.CreateAndInitializeKernel(); + + // Act + var result = await kernel.InvokePromptAsync("Hi, can you help me today?", new(settings)); + + var logProbabilityInfo = result.Metadata?["LogProbabilityInfo"] as IReadOnlyList; + + // Assert + Assert.NotNull(logProbabilityInfo); + + if (logprobs is true) + { + Assert.NotNull(logProbabilityInfo); + Assert.Equal(topLogprobs, logProbabilityInfo[0].TopLogProbabilities.Count); + } + else + { + Assert.Empty(logProbabilityInfo); + } + } + + #region internals + + private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); + Assert.NotNull(azureOpenAIConfiguration.ApiKey); + Assert.NotNull(azureOpenAIConfiguration.Endpoint); + Assert.NotNull(azureOpenAIConfiguration.ServiceId); + + this._kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey, + serviceId: azureOpenAIConfiguration.ServiceId, + httpClient: httpClient); + + return this._kernelBuilder.Build(); + } + + private const string InputParameterName = "input"; + private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + private sealed class HttpHeaderHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) + { + public System.Net.Http.Headers.HttpRequestHeaders? RequestHeaders { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.RequestHeaders = request.Headers; + return await base.SendAsync(request, cancellationToken); + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs new file mode 100644 index 000000000000..5bbbd60c9005 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -0,0 +1,781 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; + +public sealed class AzureOpenAIChatCompletionFunctionCallingTests +{ + [Fact] + public async Task CanAutoInvokeKernelFunctionsAsync() + { + // Arrange + var invokedFunctions = new List(); + + var filter = new FakeFunctionFilter(async (context, next) => + { + invokedFunctions.Add($"{context.Function.Name}({string.Join(", ", context.Arguments)})"); + await next(context); + }); + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.FunctionInvocationFilters.Add(filter); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); + + // Assert + Assert.Contains("rain", result.GetValue(), StringComparison.InvariantCulture); + Assert.Contains("GetCurrentUtcTime()", invokedFunctions); + Assert.Contains("Get_Weather_For_City([cityName, Boston])", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsStreamingAsync() + { + // Arrange + var invokedFunctions = new List(); + + var filter = new FakeFunctionFilter(async (context, next) => + { + invokedFunctions.Add($"{context.Function.Name}({string.Join(", ", context.Arguments)})"); + await next(context); + }); + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.FunctionInvocationFilters.Add(filter); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in kernel.InvokePromptStreamingAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))) + { + stringBuilder.Append(update); + } + + // Assert + Assert.Contains("rain", stringBuilder.ToString(), StringComparison.InvariantCulture); + Assert.Contains("GetCurrentUtcTime()", invokedFunctions); + Assert.Contains("Get_Weather_For_City([cityName, Boston])", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("42.8", result.GetValue(), StringComparison.InvariantCulture); // The WeatherPlugin always returns 42.8 for Dublin, Ireland. + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("10", result.GetValue(), StringComparison.InvariantCulture); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithEnumTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("rain", result.GetValue(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); + + var builder = new StringBuilder(); + + await foreach (var update in streamingResult) + { + builder.Append(update.ToString()); + } + + var result = builder.ToString(); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Current way of handling function calls manually using connector specific chat message content class. + var toolCalls = ((AzureOpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + + while (toolCalls.Count > 0) + { + // Adding LLM function call request to chat history + chatHistory.Add(result); + + // Iterating over the requested function calls and invoking them + foreach (var toolCall in toolCalls) + { + string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? + JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : + "Unable to find function. Please try again!"; + + // Adding the result of the function call to the chat history + chatHistory.Add(new ChatMessageContent( + AuthorRole.Tool, + content, + metadata: new Dictionary(1) { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + } + + // Sending the functions invocation results back to the LLM to get the final response + result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + toolCalls = ((AzureOpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + } + + // Assert + Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(result.ToChatMessage()); + } + + // Sending the functions invocation results to the LLM to get the final response + messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("Add the \"Error\" keyword to the response, if you are unable to answer a question or an error has happen."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); + } + + // Sending the functions execution results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.NotNull(messageContent.Content); + + Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length > 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.AddMessage(AuthorRole.Tool, [result]); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + messageContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + // Sending the functions invocation results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ItFailsIfNoFunctionResultProvidedAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(result); + + var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); + + // Assert + Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + var functionResult = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(functionResult.ToChatMessage()); + } + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("rain", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var result = new StringBuilder(); + + // Act + await foreach (var contentUpdate in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + result.Append(contentUpdate.Content); + } + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("Add the \"Error\" keyword to the response, if you are unable to answer a question or an error has happen."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); + } + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("error", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + var functionResult = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(functionResult.ToChatMessage()); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + fcContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("tornado", result, StringComparison.InvariantCultureIgnoreCase); + } + + private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); + Assert.NotNull(azureOpenAIConfiguration.ApiKey); + Assert.NotNull(azureOpenAIConfiguration.Endpoint); + + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); + + var kernel = kernelBuilder.Build(); + + if (importHelperPlugin) + { + kernel.ImportPluginFromFunctions("HelperFunctions", + [ + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), + kernel.CreateFunctionFromMethod((string cityName) => + { + return cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }; + }, "Get_Weather_For_City", "Gets the current weather for the specified city"), + kernel.CreateFunctionFromMethod((WeatherParameters parameters) => + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + }, "Get_Current_Temperature", "Get current temperature."), + kernel.CreateFunctionFromMethod((double temperatureInFahrenheit) => + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + }, "Convert_Temperature_From_Fahrenheit_To_Celsius", "Convert temperature from Fahrenheit to Celsius.") + ]); + } + + return kernel; + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + private sealed class FakeFunctionFilter : IFunctionInvocationFilter + { + private readonly Func, Task>? _onFunctionInvocation; + + public FakeFunctionFilter( + Func, Task>? onFunctionInvocation = null) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) => + this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs new file mode 100644 index 000000000000..72d5ff34dec4 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.TextGeneration; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class AzureOpenAIChatCompletionNonStreamingTests +{ + [Fact] + public async Task ChatCompletionShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await chatCompletion.GetChatMessageContentAsync("What is the capital of France?", settings, kernel); + + // Assert + Assert.Contains("I don't know", result.Content); + } + + [Fact] + public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var chatHistory = new ChatHistory("Reply \"I don't know\" to every question."); + chatHistory.AddUserMessage("What is the capital of France?"); + + // Act + var result = await chatCompletion.GetChatMessageContentAsync(chatHistory, null, kernel); + + // Assert + Assert.Contains("I don't know", result.Content); + Assert.NotNull(result.Metadata); + + Assert.True(result.Metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(result.Metadata.ContainsKey("PromptFilterResults")); + + Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); + + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + + Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + + Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + + Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.Empty((logProbabilityInfo as IReadOnlyList)!); + } + + [Fact] + public async Task TextGenerationShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await textGeneration.GetTextContentAsync("What is the capital of France?", settings, kernel); + + // Assert + Assert.Contains("I don't know", result.Text); + } + + [Fact] + public async Task TextGenerationShouldReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + // Act + var result = await textGeneration.GetTextContentAsync("Reply \"I don't know\" to every question. What is the capital of France?", null, kernel); + + // Assert + Assert.Contains("I don't know", result.Text); + Assert.NotNull(result.Metadata); + + Assert.True(result.Metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(result.Metadata.ContainsKey("PromptFilterResults")); + + Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); + + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + + Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + + Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + + Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.Empty((logProbabilityInfo as IReadOnlyList)!); + } + + #region internals + + private Kernel CreateAndInitializeKernel() + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); + Assert.NotNull(azureOpenAIConfiguration.ApiKey); + Assert.NotNull(azureOpenAIConfiguration.Endpoint); + + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); + + return kernelBuilder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs new file mode 100644 index 000000000000..57fb1c73fb72 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.TextGeneration; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class AzureOpenAIChatCompletionStreamingTests +{ + [Fact] + public async Task ChatCompletionShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in chatCompletion.GetStreamingChatMessageContentsAsync("What is the capital of France?", settings, kernel)) + { + stringBuilder.Append(update.Content); + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + } + + [Fact] + public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var chatHistory = new ChatHistory("Reply \"I don't know\" to every question."); + chatHistory.AddUserMessage("What is the capital of France?"); + + var stringBuilder = new StringBuilder(); + var metadata = new Dictionary(); + + // Act + await foreach (var update in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, null, kernel)) + { + stringBuilder.Append(update.Content); + + foreach (var key in update.Metadata!.Keys) + { + metadata[key] = update.Metadata[key]; + } + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + Assert.NotNull(metadata); + + Assert.True(metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(metadata.ContainsKey("SystemFingerprint")); + + Assert.True(metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + } + + [Fact] + public async Task TextGenerationShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("What is the capital of France?", settings, kernel)) + { + stringBuilder.Append(update); + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + } + + [Fact] + public async Task TextGenerationShouldReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + // Act + var stringBuilder = new StringBuilder(); + var metadata = new Dictionary(); + + // Act + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("Reply \"I don't know\" to every question. What is the capital of France?", null, kernel)) + { + stringBuilder.Append(update); + + foreach (var key in update.Metadata!.Keys) + { + metadata[key] = update.Metadata[key]; + } + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + Assert.NotNull(metadata); + + Assert.True(metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(metadata.ContainsKey("SystemFingerprint")); + + Assert.True(metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + } + + #region internals + + private Kernel CreateAndInitializeKernel() + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); + Assert.NotNull(azureOpenAIConfiguration.ApiKey); + Assert.NotNull(azureOpenAIConfiguration.Endpoint); + + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); + + return kernelBuilder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj index f3c704a27307..13bcc5ba0f44 100644 --- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj +++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj @@ -42,6 +42,7 @@ + diff --git a/dotnet/src/IntegrationTestsV2/TestHelpers.cs b/dotnet/src/IntegrationTestsV2/TestHelpers.cs new file mode 100644 index 000000000000..350370d6c056 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/TestHelpers.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using Microsoft.SemanticKernel; + +namespace SemanticKernel.IntegrationTestsV2; + +internal static class TestHelpers +{ + private const string PluginsFolder = "../../../../../../prompt_template_samples"; + + internal static void ImportAllSamplePlugins(Kernel kernel) + { + ImportSamplePromptFunctions(kernel, PluginsFolder, + "ChatPlugin", + "SummarizePlugin", + "WriterPlugin", + "CalendarPlugin", + "ChildrensBookPlugin", + "ClassificationPlugin", + "CodingPlugin", + "FunPlugin", + "IntentDetectionPlugin", + "MiscPlugin", + "QAPlugin"); + } + + internal static void ImportAllSampleSkills(Kernel kernel) + { + ImportSamplePromptFunctions(kernel, "./skills", "FunSkill"); + } + + internal static IReadOnlyKernelPluginCollection ImportSamplePlugins(Kernel kernel, params string[] pluginNames) + { + return ImportSamplePromptFunctions(kernel, PluginsFolder, pluginNames); + } + + internal static IReadOnlyKernelPluginCollection ImportSamplePromptFunctions(Kernel kernel, string path, params string[] pluginNames) + { + string? currentAssemblyDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location); + if (string.IsNullOrWhiteSpace(currentAssemblyDirectory)) + { + throw new InvalidOperationException("Unable to determine current assembly directory."); + } + + string parentDirectory = Path.GetFullPath(Path.Combine(currentAssemblyDirectory, path)); + + return new KernelPluginCollection( + from pluginName in pluginNames + select kernel.ImportPluginFromPromptDirectory(Path.Combine(parentDirectory, pluginName))); + } +} diff --git a/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs b/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs new file mode 100644 index 000000000000..6a15a4c89dd7 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace SemanticKernel.IntegrationTests.TestSettings; + +[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", + Justification = "Configuration classes are instantiated through IConfiguration.")] +internal sealed class AzureOpenAIConfiguration(string serviceId, string deploymentName, string endpoint, string apiKey, string? chatDeploymentName = null, string? modelId = null, string? chatModelId = null, string? embeddingModelId = null) +{ + public string ServiceId { get; set; } = serviceId; + public string DeploymentName { get; set; } = deploymentName; + public string ApiKey { get; set; } = apiKey; + public string? ChatDeploymentName { get; set; } = chatDeploymentName ?? deploymentName; + public string ModelId { get; set; } = modelId ?? deploymentName; + public string ChatModelId { get; set; } = chatModelId ?? deploymentName; + public string EmbeddingModelId { get; set; } = embeddingModelId ?? "text-embedding-ada-002"; + public string Endpoint { get; set; } = endpoint; +} From 05374c8d278284e8644338c4800d3f603c53f56f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 1 Jul 2024 12:30:25 +0100 Subject: [PATCH 012/226] .Net: Copy AzureOpenAITextEmbeddingGenerationService to Connectors.AzureOpenAI project (#7022) ### Motivation and Context This PR prepares the AzureOpenAITextEmbeddingGenerationService for migration to the new Azure AI SDK v2. The AzureOpenAITextEmbeddingGenerationService is copied to the Connectors.AzureOpenAI project as is and excluded from compilation to simplify code review of the next PR, which will refactor it to use the new Azure SDK. The next PR will also add unit/integration tests, along with service collection and kernel builder extension methods. ### Description The AzureOpenAITextEmbeddingGenerationService class was copied as is to the Connectors.AzureOpenAI project. The class build action was set to none to exclude it temporarily from the compilation process until it gets refactored to the new SDK. --- ...eOpenAIServiceCollectionExtensionsTests.cs | 2 +- ...enAIServiceKernelBuilderExtensionsTests.cs | 2 +- .../Connectors.AzureOpenAI.csproj | 8 ++ ...ureOpenAITextEmbeddingGenerationService.cs | 111 ++++++++++++++++++ 4 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs index 041cee3f3cc9..152a968a6bb1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -12,7 +12,7 @@ namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; /// -/// Unit tests for class. +/// Unit tests for the service collection extensions in the class. /// public sealed class AzureOpenAIServiceCollectionExtensionsTests { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs index 6025eb1d447f..13c5d31ce427 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs @@ -12,7 +12,7 @@ namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; /// -/// Unit tests for class. +/// Unit tests for the kernel builder extensions in the class. /// public sealed class AzureOpenAIServiceKernelBuilderExtensionsTests { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..29fbd3da46d3 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,10 +21,18 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs new file mode 100644 index 000000000000..9119a9005939 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI text embedding service. +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService +{ + private readonly AzureOpenAIClientCore _core; + private readonly int? _dimensions; + + /// + /// Creates a new client instance using API Key auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + public AzureOpenAITextEmbeddingGenerationService( + string deploymentName, + string endpoint, + string apiKey, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null, + int? dimensions = null) + { + this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._dimensions = dimensions; + } + + /// + /// Creates a new client instance supporting AAD auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + public AzureOpenAITextEmbeddingGenerationService( + string deploymentName, + string endpoint, + TokenCredential credential, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null, + int? dimensions = null) + { + this._core = new(deploymentName, endpoint, credential, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._dimensions = dimensions; + } + + /// + /// Creates a new client. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// The to use for logging. If null, no logging will be performed. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + public AzureOpenAITextEmbeddingGenerationService( + string deploymentName, + OpenAIClient openAIClient, + string? modelId = null, + ILoggerFactory? loggerFactory = null, + int? dimensions = null) + { + this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._dimensions = dimensions; + } + + /// + public IReadOnlyDictionary Attributes => this._core.Attributes; + + /// + public Task>> GenerateEmbeddingsAsync( + IList data, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); + } +} From c4c187811e7273a7ed62956d16a5a545f97557f5 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 1 Jul 2024 18:06:42 +0100 Subject: [PATCH 013/226] .Net: AzureOpenAIChatCompletionService Functionality Cleanup (#7024) ### Motivation and Context This PR is a follow-up to the one that migrated the AzureOpenAIChatCompletionService class to Azure AI SDK v2 but did not properly refactor it, leaving class, member, and variable names unchanged to minimize the number of changes and simplify the code review process. ### Description This PR does the following: **1. No functional changes - just deletion and renaming.** 2. Renames ClientCore class methods and method variables to reflect their actual purpose/functionality. 3. Renames class members of other classes related to and used by the AzureOpenAIChatCompletionService. 4. Renames AzureOpenAIPromptExecutionSettings class to AzureOpenAIChatCompletionExecutionSettings to indicate that it belongs to the chat completion service and not to any other one. 5. Removes the AzureOpenAIStreamingTextContent class used by the text generation service, which has been deprecated and is not supported by Azure AI SDK v2. 6. Improves the resiliency of AzureOpenAIChatCompletionService integration tests by using a base integration class with a preconfigured resilience handler to handle 429 responses. --- .../Core/AzureOpenAIFunctionToolCallTests.cs | 4 +- .../AzureOpenAIStreamingTextContentTests.cs | 41 ------ .../ChatHistoryExtensions.cs | 4 +- .../Core/AzureOpenAIClientCore.cs | 4 +- .../Core/AzureOpenAIFunctionToolCall.cs | 2 +- .../AzureOpenAIStreamingChatMessageContent.cs | 10 +- .../Core/AzureOpenAIStreamingTextContent.cs | 51 -------- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 118 +++++++++--------- .../AzureOpenAIServiceCollectionExtensions.cs | 4 +- .../IntegrationTestsV2/BaseIntegrationTest.cs | 37 ++++++ .../AzureOpenAIChatCompletionTests.cs | 17 +-- ...enAIChatCompletion_FunctionCallingTests.cs | 7 +- ...eOpenAIChatCompletion_NonStreamingTests.cs | 4 +- ...zureOpenAIChatCompletion_StreamingTests.cs | 4 +- 14 files changed, 127 insertions(+), 180 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs create mode 100644 dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs index 766376ee00b9..d8342b4991d4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs @@ -46,7 +46,7 @@ public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() var functionArgumentBuildersByIndex = new Dictionary(); // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); @@ -64,7 +64,7 @@ public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs deleted file mode 100644 index a58df5676aca..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIStreamingTextContentTests.cs +++ /dev/null @@ -1,41 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIStreamingTextContentTests -{ - [Fact] - public void ToByteArrayWorksCorrectly() - { - // Arrange - var expectedBytes = Encoding.UTF8.GetBytes("content"); - var content = new AzureOpenAIStreamingTextContent("content", 0, "model-id"); - - // Act - var actualBytes = content.ToByteArray(); - - // Assert - Assert.Equal(expectedBytes, actualBytes); - } - - [Theory] - [InlineData(null, "")] - [InlineData("content", "content")] - public void ToStringWorksCorrectly(string? content, string expectedString) - { - // Arrange - var textContent = new AzureOpenAIStreamingTextContent(content!, 0, "model-id"); - - // Act - var actualString = textContent.ToString(); - - // Assert - Assert.Equal(expectedString, actualString); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs index 23412f666e23..5d49fdf91b46 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs @@ -43,7 +43,7 @@ public static async IAsyncEnumerable AddStreamingMe (contentBuilder ??= new()).Append(contentUpdate); } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); // Is always expected to have at least one chunk with the role provided from a streaming message streamedRole ??= chatMessage.Role; @@ -62,7 +62,7 @@ public static async IAsyncEnumerable AddStreamingMe role, contentBuilder?.ToString() ?? string.Empty, messageContents[0].ModelId!, - AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), + AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), metadata) { AuthorName = streamedName }); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs index c37321e48c4d..348f65781734 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs @@ -44,7 +44,7 @@ internal AzureOpenAIClientCore( Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); Verify.NotNullOrWhiteSpace(apiKey); - var options = GetOpenAIClientOptions(httpClient); + var options = GetAzureOpenAIClientOptions(httpClient); this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); @@ -70,7 +70,7 @@ internal AzureOpenAIClientCore( Verify.NotNullOrWhiteSpace(endpoint); Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - var options = GetOpenAIClientOptions(httpClient); + var options = GetAzureOpenAIClientOptions(httpClient); this.DeploymentOrModelName = deploymentName; this.Endpoint = new Uri(endpoint); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs index e618f27a9b15..361c617f31a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs @@ -139,7 +139,7 @@ internal static void TrackStreamingToolingUpdate( /// Dictionary mapping indices to IDs. /// Dictionary mapping indices to names. /// Dictionary mapping indices to arguments. - internal static ChatToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + internal static ChatToolCall[] ConvertToolCallUpdatesToFunctionToolCalls( ref Dictionary? toolCallIdsByIndex, ref Dictionary? functionNamesByIndex, ref Dictionary? functionArgumentBuildersByIndex) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs index 9287499e1621..fce885482899 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs @@ -41,7 +41,7 @@ internal AzureOpenAIStreamingChatMessageContent( Encoding.UTF8, metadata) { - this.ToolCallUpdate = chatUpdate.ToolCallUpdates; + this.ToolCallUpdates = chatUpdate.ToolCallUpdates; this.FinishReason = chatUpdate.FinishReason; this.Items = CreateContentItems(chatUpdate.ContentUpdate); } @@ -51,7 +51,7 @@ internal AzureOpenAIStreamingChatMessageContent( /// /// Author role of the message /// Content of the message - /// Tool call update + /// Tool call updates /// Completion finish reason /// Index of the choice /// The model ID used to generate the content @@ -59,7 +59,7 @@ internal AzureOpenAIStreamingChatMessageContent( internal AzureOpenAIStreamingChatMessageContent( AuthorRole? authorRole, string? content, - IReadOnlyList? tootToolCallUpdate = null, + IReadOnlyList? toolCallUpdates = null, ChatFinishReason? completionsFinishReason = null, int choiceIndex = 0, string? modelId = null, @@ -73,12 +73,12 @@ internal AzureOpenAIStreamingChatMessageContent( Encoding.UTF8, metadata) { - this.ToolCallUpdate = tootToolCallUpdate; + this.ToolCallUpdates = toolCallUpdates; this.FinishReason = completionsFinishReason; } /// Gets any update information in the message about a tool call. - public IReadOnlyList? ToolCallUpdate { get; } + public IReadOnlyList? ToolCallUpdates { get; } /// public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs deleted file mode 100644 index 9d9497fd68d5..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingTextContent.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Azure OpenAI specialized streaming text content. -/// -/// -/// Represents a text content chunk that was streamed from the remote model. -/// -public sealed class AzureOpenAIStreamingTextContent : StreamingTextContent -{ - /// - /// Create a new instance of the class. - /// - /// Text update - /// Index of the choice - /// The model ID used to generate the content - /// Inner chunk object - /// Metadata information - internal AzureOpenAIStreamingTextContent( - string text, - int choiceIndex, - string modelId, - object? innerContentObject = null, - IReadOnlyDictionary? metadata = null) - : base( - text, - choiceIndex, - modelId, - innerContentObject, - Encoding.UTF8, - metadata) - { - } - - /// - public override byte[] ToByteArray() - { - return this.Encoding.GetBytes(this.ToString()); - } - - /// - public override string ToString() - { - return this.Text ?? string.Empty; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 4152f2137409..9dea5efb2cf9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -123,7 +123,7 @@ internal ClientCore(ILogger? logger = null) unit: "{token}", description: "Number of tokens used"); - private static Dictionary GetChatChoiceMetadata(OpenAIChatCompletion completions) + private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) { #pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. return new Dictionary(8) @@ -142,7 +142,7 @@ internal ClientCore(ILogger? logger = null) #pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } - private static Dictionary GetResponseMetadata(StreamingChatCompletionUpdate completionUpdate) + private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) { return new Dictionary(4) { @@ -265,47 +265,47 @@ internal async Task> GetChatMessageContentsAsy ValidateMaxTokens(chatExecutionSettings.MaxTokens); - var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); for (int requestIndex = 0; ; requestIndex++) { var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Make the request. - OpenAIChatCompletion? responseData = null; - AzureOpenAIChatMessageContent responseContent; + OpenAIChatCompletion? chatCompletion = null; + AzureOpenAIChatMessageContent chatMessageContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) { try { - responseData = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatMessages, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; - this.LogUsage(responseData.Usage); + this.LogUsage(chatCompletion.Usage); } catch (Exception ex) when (activity is not null) { activity.SetError(ex); - if (responseData != null) + if (chatCompletion != null) { // Capture available metadata even if the operation failed. activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.InputTokens) - .SetCompletionTokenUsage(responseData.Usage.OutputTokens); + .SetResponseId(chatCompletion.Id) + .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) + .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); } throw; } - responseContent = this.GetChatMessage(responseData); - activity?.SetCompletionResponse([responseContent], responseData.Usage.InputTokens, responseData.Usage.OutputTokens); + chatMessageContent = this.CreateChatMessageContent(chatCompletion); + activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); } // If we don't want to attempt to invoke any functions, just return the result. if (!toolCallingConfig.AutoInvoke) { - return [responseContent]; + return [chatMessageContent]; } Debug.Assert(kernel is not null); @@ -315,37 +315,37 @@ internal async Task> GetChatMessageContentsAsy // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool // is specified. - if (responseData.ToolCalls.Count == 0) + if (chatCompletion.ToolCalls.Count == 0) { - return [responseContent]; + return [chatMessageContent]; } if (this.Logger.IsEnabled(LogLevel.Debug)) { - this.Logger.LogDebug("Tool requests: {Requests}", responseData.ToolCalls.Count); + this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); } if (this.Logger.IsEnabled(LogLevel.Trace)) { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", responseData.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); } // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. Also add the result message to the caller's chat // history: if they don't want it, they can remove it, but this makes the data available, // including metadata like usage. - chatMessages.Add(GetRequestMessage(responseData)); - chat.Add(responseContent); + chatForRequest.Add(CreateRequestMessage(chatCompletion)); + chat.Add(chatMessageContent); // We must send back a response for every tool call, regardless of whether we successfully executed it or not. // If we successfully execute it, we'll add the result. If we don't, we'll add an error. - for (int toolCallIndex = 0; toolCallIndex < responseContent.ToolCalls.Count; toolCallIndex++) + for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) { - ChatToolCall functionToolCall = responseContent.ToolCalls[toolCallIndex]; + ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (functionToolCall.Kind != ChatToolCallKind.Function) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); continue; } @@ -357,7 +357,7 @@ internal async Task> GetChatMessageContentsAsy } catch (JsonException) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); continue; } @@ -367,14 +367,14 @@ internal async Task> GetChatMessageContentsAsy if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; } @@ -385,7 +385,7 @@ internal async Task> GetChatMessageContentsAsy Arguments = functionArgs, RequestSequenceIndex = requestIndex, FunctionSequenceIndex = toolCallIndex, - FunctionCount = responseContent.ToolCalls.Count + FunctionCount = chatMessageContent.ToolCalls.Count }; s_inflightAutoInvokes.Value++; @@ -409,7 +409,7 @@ internal async Task> GetChatMessageContentsAsy catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatMessages, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); continue; } finally @@ -423,7 +423,7 @@ internal async Task> GetChatMessageContentsAsy object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); // If filter requested termination, returning latest function result. if (invocationContext.Terminate) @@ -463,13 +463,13 @@ internal async IAsyncEnumerable GetStrea Dictionary? functionNamesByIndex = null; Dictionary? functionArgumentBuildersByIndex = null; - var chatMessages = CreateChatCompletionMessages(chatExecutionSettings, chat); + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); for (int requestIndex = 0; ; requestIndex++) { var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); // Reset state contentBuilder?.Clear(); @@ -491,7 +491,7 @@ internal async IAsyncEnumerable GetStrea AsyncResultCollection response; try { - response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatMessages, chatOptions, cancellationToken)); + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -518,16 +518,16 @@ internal async IAsyncEnumerable GetStrea throw; } - StreamingChatCompletionUpdate update = responseEnumerator.Current; - metadata = GetResponseMetadata(update); - streamedRole ??= update.Role; + StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; + metadata = GetChatCompletionMetadata(chatCompletionUpdate); + streamedRole ??= chatCompletionUpdate.Role; //streamedName ??= update.AuthorName; - finishReason = update.FinishReason ?? default; + finishReason = chatCompletionUpdate.FinishReason ?? default; // If we're intending to invoke function calls, we need to consume that function call information. if (toolCallingConfig.AutoInvoke) { - foreach (var contentPart in update.ContentUpdate) + foreach (var contentPart in chatCompletionUpdate.ContentUpdate) { if (contentPart.Kind == ChatMessageContentPartKind.Text) { @@ -535,12 +535,12 @@ internal async IAsyncEnumerable GetStrea } } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(update, 0, this.DeploymentOrModelName, metadata); + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentOrModelName, metadata); - foreach (var functionCallUpdate in update.ToolCallUpdates) + foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) { // Using the code below to distinguish and skip non - function call related updates. // The Kind property of updates can't be reliably used because it's only initialized for the first update. @@ -563,7 +563,7 @@ internal async IAsyncEnumerable GetStrea } // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( + toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); // Translate all entries into FunctionCallContent instances for diagnostics purposes. @@ -601,8 +601,8 @@ internal async IAsyncEnumerable GetStrea // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. - chatMessages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - chat.Add(this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); // Respond to each tooling request. for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) @@ -612,7 +612,7 @@ internal async IAsyncEnumerable GetStrea // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (string.IsNullOrEmpty(toolCall.FunctionName)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -624,7 +624,7 @@ internal async IAsyncEnumerable GetStrea } catch (JsonException) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -634,14 +634,14 @@ internal async IAsyncEnumerable GetStrea if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatMessages, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -676,7 +676,7 @@ internal async IAsyncEnumerable GetStrea catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatMessages, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -690,7 +690,7 @@ internal async IAsyncEnumerable GetStrea object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatMessages, chat, stringResult, errorMessage: null, toolCall, this.Logger); + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); // If filter requested termination, returning latest function result and breaking request iteration loop. if (invocationContext.Terminate) @@ -765,7 +765,7 @@ internal void AddAttribute(string key, string? value) /// Gets options to use for an OpenAIClient /// Custom for HTTP requests. /// An instance of . - internal static AzureOpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient) + internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient) { AzureOpenAIClientOptions options = new() { @@ -811,7 +811,7 @@ private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptE return chat; } - private ChatCompletionOptions CreateChatCompletionsOptions( + private ChatCompletionOptions CreateChatCompletionOptions( AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, ToolCallingConfig toolCallingConfig, @@ -874,13 +874,13 @@ private static List CreateChatCompletionMessages(AzureOpenAIPromptE foreach (var message in chatHistory) { - messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); + messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); } return messages; } - private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) + private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) { if (chatRole == ChatMessageRole.User) { @@ -900,7 +900,7 @@ private static ChatMessage GetRequestMessage(ChatMessageRole chatRole, string co throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static List GetRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) + private static List CreateRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { @@ -1043,7 +1043,7 @@ private static ChatMessageContentPart GetImageContentItem(ImageContent imageCont throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) + private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) { if (completion.Role == ChatMessageRole.System) { @@ -1063,16 +1063,16 @@ private static ChatMessage GetRequestMessage(OpenAIChatCompletion completion) throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent GetChatMessage(OpenAIChatCompletion completion) + private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatChoiceMetadata(completion)); + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatCompletionMetadata(completion)); message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); return message; } - private AzureOpenAIChatMessageContent GetChatMessage(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index 782889c4542c..f946d09026a0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -242,8 +242,8 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( #endregion private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); } diff --git a/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs b/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs new file mode 100644 index 000000000000..a86274d4f8ce --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; +using Microsoft.SemanticKernel; + +namespace SemanticKernel.IntegrationTestsV2; + +public class BaseIntegrationTest +{ + protected IKernelBuilder CreateKernelBuilder() + { + var builder = Kernel.CreateBuilder(); + + builder.Services.ConfigureHttpClientDefaults(c => + { + c.AddStandardResilienceHandler().Configure(o => + { + o.Retry.ShouldRetryAfterHeader = true; + o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.TooManyRequests); + o.CircuitBreaker = new HttpCircuitBreakerStrategyOptions + { + SamplingDuration = TimeSpan.FromSeconds(40.0), // The duration should be least double of an attempt timeout + }; + o.AttemptTimeout = new HttpTimeoutStrategyOptions + { + Timeout = TimeSpan.FromSeconds(20.0) // Doubling the default 10s timeout + }; + }); + }); + + return builder; + } +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs index 04f1be7e45c7..69509508af98 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -22,7 +22,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionTests +public sealed class AzureOpenAIChatCompletionTests : BaseIntegrationTest { [Fact] //[Fact(Skip = "Skipping while we investigate issue with GitHub actions.")] @@ -74,13 +74,15 @@ public async Task AzureOpenAIHttpRetryPolicyTestAsync() var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - this._kernelBuilder.AddAzureOpenAIChatCompletion( + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration!.ChatDeploymentName!, modelId: azureOpenAIConfiguration.ChatModelId, endpoint: azureOpenAIConfiguration.Endpoint, apiKey: "INVALID_KEY"); - this._kernelBuilder.Services.ConfigureHttpClientDefaults(c => + kernelBuilder.Services.ConfigureHttpClientDefaults(c => { // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example c.AddStandardResilienceHandler().Configure(o => @@ -94,7 +96,7 @@ public async Task AzureOpenAIHttpRetryPolicyTestAsync() }); }); - var target = this._kernelBuilder.Build(); + var target = kernelBuilder.Build(); var plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); @@ -237,7 +239,9 @@ private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) Assert.NotNull(azureOpenAIConfiguration.Endpoint); Assert.NotNull(azureOpenAIConfiguration.ServiceId); - this._kernelBuilder.AddAzureOpenAIChatCompletion( + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, modelId: azureOpenAIConfiguration.ChatModelId, endpoint: azureOpenAIConfiguration.Endpoint, @@ -245,11 +249,10 @@ private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) serviceId: azureOpenAIConfiguration.ServiceId, httpClient: httpClient); - return this._kernelBuilder.Build(); + return kernelBuilder.Build(); } private const string InputParameterName = "input"; - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 5bbbd60c9005..f90102d62834 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -12,12 +12,11 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using OpenAI.Chat; using SemanticKernel.IntegrationTests.TestSettings; -using SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; using Xunit; -namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; -public sealed class AzureOpenAIChatCompletionFunctionCallingTests +public sealed class AzureOpenAIChatCompletionFunctionCallingTests : BaseIntegrationTest { [Fact] public async Task CanAutoInvokeKernelFunctionsAsync() @@ -707,7 +706,7 @@ private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs index 72d5ff34dec4..5847ad29a6d1 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -17,7 +17,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionNonStreamingTests +public sealed class AzureOpenAIChatCompletionNonStreamingTests : BaseIntegrationTest { [Fact] public async Task ChatCompletionShouldUseChatSystemPromptAsync() @@ -158,7 +158,7 @@ private Kernel CreateAndInitializeKernel() Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs index 57fb1c73fb72..f340064b2ee3 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs @@ -16,7 +16,7 @@ namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class AzureOpenAIChatCompletionStreamingTests +public sealed class AzureOpenAIChatCompletionStreamingTests : BaseIntegrationTest { [Fact] public async Task ChatCompletionShouldUseChatSystemPromptAsync() @@ -152,7 +152,7 @@ private Kernel CreateAndInitializeKernel() Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.Endpoint); - var kernelBuilder = Kernel.CreateBuilder(); + var kernelBuilder = base.CreateKernelBuilder(); kernelBuilder.AddAzureOpenAIChatCompletion( deploymentName: azureOpenAIConfiguration.ChatDeploymentName, From 294124510f14b78b31b895fd9a3c51986acca3cd Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:15:10 +0100 Subject: [PATCH 014/226] .Net: Migrate AzureOpenAITextEmbeddingGenerationService to Azure AI SDK v2 (#7030) ### Motivation, Context and Description This PR migrates the AzureOpenAITextEmbeddingGenerationService to Azure AI SDK v2: 1. The AzureOpenAITextEmbeddingGenerationService class is updated to use the new AzureOpenAIClient from Azure AI SDK v2. 2. Service collection and kernel builder extension methods for registering the service are copied to the new Connectors.AzureOpenAI project. 3. Unit tests are added (copied and adapted) for the service and the extension methods. 4. Integration tests are added (copied and adapted) for the service. --- .../Connectors.AzureOpenAI.UnitTests.csproj | 4 +- ...eOpenAIServiceCollectionExtensionsTests.cs | 50 +++- ...enAIServiceKernelBuilderExtensionsTests.cs | 36 +++ ...enAITextEmbeddingGenerationServiceTests.cs | 90 ++++++++ .../text-embeddings-multiple-response.txt | 20 ++ .../TestData/text-embeddings-response.txt | 15 ++ .../Connectors.AzureOpenAI.csproj | 8 - .../AzureOpenAIServiceCollectionExtensions.cs | 218 +++++++++++++++++- ...ureOpenAITextEmbeddingGenerationService.cs | 4 +- .../AzureOpenAITextEmbeddingTests.cs | 71 ++++++ .../test/AssertExtensions.cs | 2 +- 11 files changed, 494 insertions(+), 24 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-response.txt create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj index 5952d571a09f..a0a695a6719c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -8,7 +8,7 @@ true enable false - $(NoWarn);SKEXP0001;SKEXP0010;CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111 + $(NoWarn);SKEXP0001;SKEXP0010;CA2007,CA1806,CA1869,CA1861,IDE0300,VSTHRD111,IDE1006 @@ -27,7 +27,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs index 152a968a6bb1..ca4899258b21 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -7,6 +7,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -21,8 +22,8 @@ public sealed class AzureOpenAIServiceCollectionExtensionsTests [Theory] [InlineData(InitializationType.ApiKey)] [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) { // Arrange @@ -37,8 +38,8 @@ public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(Initia { InitializationType.ApiKey => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), InitializationType.TokenCredential => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAIChatCompletion("deployment-name"), + InitializationType.ClientInline => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.Services.AddAzureOpenAIChatCompletion("deployment-name"), _ => builder.Services }; @@ -52,12 +53,47 @@ public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(Initia #endregion + #region Text embeddings + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void ServiceCollectionAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.ClientInline => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextEmbeddingGenerationService); + } + + #endregion + public enum InitializationType { ApiKey, TokenCredential, - OpenAIClientInline, - OpenAIClientInServiceProvider, - OpenAIClientEndpoint, + ClientInline, + ClientInServiceProvider, + ClientEndpoint, } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs index 13c5d31ce427..8c5515516ca5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs @@ -7,6 +7,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -52,6 +53,41 @@ public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(Initializa #endregion + #region Text embeddings + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), + InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.NotNull(service); + Assert.True(service is AzureOpenAITextEmbeddingGenerationService); + } + + #endregion + public enum InitializationType { ApiKey, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs new file mode 100644 index 000000000000..738364429cff --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Services; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public class AzureOpenAITextEmbeddingGenerationServiceTests +{ + [Fact] + public void ItCanBeInstantiatedAndPropertiesSetAsExpected() + { + // Arrange + var sut = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", modelId: "model", dimensions: 2); + var sutWithAzureOpenAIClient = new AzureOpenAITextEmbeddingGenerationService("deployment-name", new AzureOpenAIClient(new Uri("https://endpoint"), new ApiKeyCredential("apiKey")), modelId: "model", dimensions: 2); + + // Assert + Assert.NotNull(sut); + Assert.NotNull(sutWithAzureOpenAIClient); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal("model", sutWithAzureOpenAIClient.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty() + { + // Arrange + var sut = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key"); + + // Act + var result = await sut.GenerateEmbeddingsAsync([], null, CancellationToken.None); + + // Assert + Assert.Empty(result); + } + + [Fact] + public async Task GetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsWhitespace() + { + // Arrange + using HttpMessageHandlerStub handler = new() + { + ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-response.txt")) + } + }; + using HttpClient client = new(handler); + + var sut = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", httpClient: client); + + // Act + var result = await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None); + + // Assert + Assert.Single(result); + Assert.Equal(4, result[0].Length); + } + + [Fact] + public async Task ItThrowsIfNumberOfResultsDiffersFromInputsAsync() + { + // Arrange + using HttpMessageHandlerStub handler = new() + { + ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-embeddings-multiple-response.txt")) + } + }; + using HttpClient client = new(handler); + + var sut = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", httpClient: client); + + // Act & Assert + await Assert.ThrowsAsync(async () => await sut.GenerateEmbeddingsAsync(["test"], null, CancellationToken.None)); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt new file mode 100644 index 000000000000..46a9581cf0cc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt @@ -0,0 +1,20 @@ +{ + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + }, + { + "object": "embedding", + "index": 1, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 7, + "total_tokens": 7 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-response.txt new file mode 100644 index 000000000000..c715b851b78c --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-embeddings-response.txt @@ -0,0 +1,15 @@ +{ + "object": "list", + "data": [ + { + "object": "embedding", + "index": 0, + "embedding": "zcyMP83MDEAzM1NAzcyMQA==" + } + ], + "model": "text-embedding-ada-002", + "usage": { + "prompt_tokens": 7, + "total_tokens": 7 + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 29fbd3da46d3..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,18 +21,10 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index f946d09026a0..e25eac02789b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Azure; using Azure.AI.OpenAI; @@ -9,6 +10,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.TextGeneration; @@ -44,7 +46,6 @@ public static IKernelBuilder AddAzureOpenAIChatCompletion( HttpClient? httpClient = null) { Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.NotNullOrWhiteSpace(apiKey); @@ -83,7 +84,6 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( string? modelId = null) { Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.NotNullOrWhiteSpace(apiKey); @@ -124,7 +124,6 @@ public static IKernelBuilder AddAzureOpenAIChatCompletion( HttpClient? httpClient = null) { Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.NotNull(credentials); @@ -163,7 +162,6 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( string? modelId = null) { Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); Verify.NotNullOrWhiteSpace(endpoint); Verify.NotNull(credentials); @@ -241,6 +239,218 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( #endregion + #region Text Embedding + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( + this IServiceCollection services, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + int? dimensions = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService(), + dimensions)); + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credential, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + Verify.NotNull(builder); + Verify.NotNull(credential); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + credential, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( + this IServiceCollection services, + string deploymentName, + string endpoint, + TokenCredential credential, + string? serviceId = null, + string? modelId = null, + int? dimensions = null) + { + Verify.NotNull(services); + Verify.NotNull(credential); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + credential, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService(), + dimensions)); + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + azureOpenAIClient ?? serviceProvider.GetRequiredService(), + modelId, + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( + this IServiceCollection services, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null, + int? dimensions = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + azureOpenAIClient ?? serviceProvider.GetRequiredService(), + modelId, + serviceProvider.GetService(), + dimensions)); + } + + #endregion + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs index 9119a9005939..31159da6f0a5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -79,13 +79,13 @@ public AzureOpenAITextEmbeddingGenerationService( /// Creates a new client. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. + /// Custom for HTTP requests. /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. public AzureOpenAITextEmbeddingGenerationService( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient openAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null, int? dimensions = null) diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs new file mode 100644 index 000000000000..1dfc39670416 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Embeddings; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +public sealed class AzureOpenAITextEmbeddingTests +{ + public AzureOpenAITextEmbeddingTests() + { + var config = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); + Assert.NotNull(config); + this._azureOpenAIConfiguration = config; + } + + [Theory] + [InlineData("test sentence")] + public async Task AzureOpenAITestAsync(string testInputString) + { + // Arrange + var embeddingGenerator = new AzureOpenAITextEmbeddingGenerationService( + this._azureOpenAIConfiguration.DeploymentName, + this._azureOpenAIConfiguration.Endpoint, + this._azureOpenAIConfiguration.ApiKey); + + // Act + var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString); + var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]); + + // Assert + Assert.Equal(AdaVectorLength, singleResult.Length); + Assert.Equal(3, batchResult.Count); + } + + [Theory] + [InlineData(null, 3072)] + [InlineData(1024, 1024)] + public async Task AzureOpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength) + { + // Arrange + const string TestInputString = "test sentence"; + + var embeddingGenerator = new AzureOpenAITextEmbeddingGenerationService( + "text-embedding-3-large", + this._azureOpenAIConfiguration.Endpoint, + this._azureOpenAIConfiguration.ApiKey, + dimensions: dimensions); + + // Act + var result = await embeddingGenerator.GenerateEmbeddingAsync(TestInputString); + + // Assert + Assert.Equal(expectedVectorLength, result.Length); + } + + private readonly AzureOpenAIConfiguration _azureOpenAIConfiguration; + + private const int AdaVectorLength = 1536; + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); +} diff --git a/dotnet/src/InternalUtilities/test/AssertExtensions.cs b/dotnet/src/InternalUtilities/test/AssertExtensions.cs index cf201d169366..4caf63589cbc 100644 --- a/dotnet/src/InternalUtilities/test/AssertExtensions.cs +++ b/dotnet/src/InternalUtilities/test/AssertExtensions.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using Xunit; +using Assert = Xunit.Assert; namespace SemanticKernel.UnitTests; From 5bc3a78214f76b527b86f1430def3040c20b2d1f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 2 Jul 2024 11:54:32 +0100 Subject: [PATCH 015/226] .Net: Move AzureOpenAIChatCompletionService to the Services folder (#7048) ### Motivation, Context and Description This PR moves the AzureOpenAIChatCompletionService from the ChatCompletion folder to the Services folder to align the AzureOpenAI project structure with that of OpenAIV2. Additionally, the AzureOpenAIChatCompletionServiceTests unit tests were also moved to the appropriate folder for consistency. --- .../AzureOpenAIChatCompletionServiceTests.cs | 10 +++++----- .../AzureOpenAIChatCompletionService.cs | 0 2 files changed, 5 insertions(+), 5 deletions(-) rename dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/{ChatCompletion => Services}/AzureOpenAIChatCompletionServiceTests.cs (99%) rename dotnet/src/Connectors/Connectors.AzureOpenAI/{ChatCompletion => Services}/AzureOpenAIChatCompletionService.cs (100%) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs similarity index 99% rename from dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs index 3b3c90687b45..13e09bd39e71 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs @@ -20,7 +20,7 @@ using Moq; using OpenAI.Chat; -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.ChatCompletion; +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; /// /// Unit tests for @@ -690,7 +690,7 @@ public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndS public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() { // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }); @@ -752,7 +752,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT public async Task FunctionCallsShouldBeReturnedToLLMAsync() { // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); @@ -810,7 +810,7 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() { // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); @@ -858,7 +858,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() { // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) + this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs From 3ebe6effee31fa59472e49ca03c0fa0e60db3d90 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:07:32 +0100 Subject: [PATCH 016/226] .Net: OpenAI V2 - Migrate Audio Services Phase 04 (#7029) ### Motivation and Context - Audio to Text Services + UT + IT - Text to Audio Services + UT + IT - Added missing extension methods for Breaking Glass `OpenAIClient` - Moved ClientResultExtension + UT to utilities - Added and Moved Extensions + UT --- .../ClientResultExceptionExtensionsTests.cs | 53 ----- .../Connectors.OpenAIV2.UnitTests.csproj | 2 +- .../KernelBuilderExtensionsTests.cs | 62 ++++++ .../ServiceCollectionExtensionsTests.cs | 62 ++++++ .../Services/OpenAIAudioToTextServiceTests.cs | 144 ++++++++++++ .../Services/OpenAITextToAudioServiceTests.cs | 205 ++++++++++++++++++ .../Services/OpenAITextToImageServiceTests.cs | 2 +- ...OpenAIAudioToTextExecutionSettingsTests.cs | 122 +++++++++++ ...OpenAITextToAudioExecutionSettingsTests.cs | 108 +++++++++ .../Core/ClientCore.AudioToText.cs | 89 ++++++++ .../Core/ClientCore.TextToAudio.cs | 67 ++++++ .../OpenAIKernelBuilderExtensions.cs | 142 +++++++++++- .../OpenAIServiceCollectionExtensions.cs | 125 ++++++++++- .../Services/OpenAIAudioToTextService.cs | 79 +++++++ .../OpenAITextEmbbedingGenerationService.cs | 12 +- .../Services/OpenAITextToAudioService.cs | 79 +++++++ .../Services/OpenAITextToImageService.cs | 12 +- .../OpenAIAudioToTextExecutionSettings.cs | 189 ++++++++++++++++ .../OpenAITextToAudioExecutionSettings.cs | 129 +++++++++++ .../OpenAI/OpenAIAudioToTextTests.cs | 48 ++++ .../OpenAI/OpenAITextToAudioTests.cs | 41 ++++ .../TestData/test_audio.wav | Bin 0 -> 222798 bytes .../ClientResultExceptionExtensions.cs | 5 +- .../ClientResultExceptionExtensionsTests.cs | 3 +- .../Utilities/OpenAI}/MockPipelineResponse.cs | 2 +- .../Utilities/OpenAI}/MockResponseHeaders.cs | 2 +- 26 files changed, 1708 insertions(+), 76 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_audio.wav rename dotnet/src/{Connectors/Connectors.OpenAIV2 => InternalUtilities/openai}/Extensions/ClientResultExceptionExtensions.cs (94%) rename dotnet/src/{Connectors/Connectors.OpenAIV2.UnitTests => SemanticKernel.UnitTests}/Extensions/ClientResultExceptionExtensionsTests.cs (95%) rename dotnet/src/{Connectors/Connectors.OpenAIV2.UnitTests/Utils => SemanticKernel.UnitTests/Utilities/OpenAI}/MockPipelineResponse.cs (98%) rename dotnet/src/{Connectors/Connectors.OpenAIV2.UnitTests/Utils => SemanticKernel.UnitTests/Utilities/OpenAI}/MockResponseHeaders.cs (94%) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs deleted file mode 100644 index d810b2d2a470..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/ClientResultExceptionExtensionsTests.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel; -using System.ClientModel.Primitives; -using System.IO; -using System.Net; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class ClientResultExceptionExtensionsTests -{ - [Fact] - public void ToHttpOperationExceptionWithContentReturnsValidException() - { - // Arrange - using var response = new FakeResponse("Response Content", 500); - var exception = new ClientResultException(response); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); - Assert.Equal("Response Content", actualException.ResponseContent); - Assert.Same(exception, actualException.InnerException); - } - - #region private - - private sealed class FakeResponse(string responseContent, int status) : PipelineResponse - { - private readonly string _responseContent = responseContent; - public override BinaryData Content => BinaryData.FromString(this._responseContent); - public override int Status { get; } = status; - public override string ReasonPhrase => "Reason Phrase"; - public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } - protected override PipelineResponseHeaders HeadersCore => throw new NotImplementedException(); - public override BinaryData BufferContent(CancellationToken cancellationToken = default) => new(this._responseContent); - public override ValueTask BufferContentAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public override void Dispose() { } - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj index 0a100b3c13a6..80e71aa16760 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj @@ -37,7 +37,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs index f296000c5245..bfa71f7e5ab3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; using Xunit; @@ -70,4 +72,64 @@ public void ItCanAddTextToImageServiceWithOpenAIClient() // Assert Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } + + [Fact] + public void ItCanAddTextToAudioService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextToAudio("model", "key") + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextToAudioServiceWithOpenAIClient() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key")) + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddAudioToTextService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAIAudioToText("model", "key") + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddAudioToTextServiceWithOpenAIClient() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAIAudioToText("model", new OpenAIClient("key")) + .Build() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 65db68eea180..79c8024bb93f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -2,8 +2,10 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; using Xunit; @@ -71,4 +73,64 @@ public void ItCanAddImageToTextServiceWithOpenAIClient() // Assert Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } + + [Fact] + public void ItCanAddTextToAudioService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextToAudio("model", "key") + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddTextToAudioServiceWithOpenAIClient() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key")) + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddAudioToTextService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAIAudioToText("model", "key") + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void ItCanAddAudioToTextServiceWithOpenAIClient() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAIAudioToText("model", new OpenAIClient("key")) + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs new file mode 100644 index 000000000000..9648670d3de5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using OpenAI; +using Xunit; +using static Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIAudioToTextServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAIAudioToTextServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAIAudioToTextService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIAudioToTextService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new OpenAIAudioToTextService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIAudioToTextService("model-id", client); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default }, "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word }, "word")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment }, "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Word }, "word", "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Segment }, "word", "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Word }, "word", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Default }, "word", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Segment }, "segment", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Default }, "segment", "0")] + public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] granularities, params string[] expectedGranularities) + { + // Arrange + var service = new OpenAIAudioToTextService("model-id", "api-key", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var settings = new OpenAIAudioToTextExecutionSettings("file.mp3") { Granularities = granularities }; + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestContent); + Assert.NotNull(result); + + var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); + + foreach (var granularity in expectedGranularities) + { + var expectedMultipart = $"{granularity}\r\n{multiPartBreak}"; + Assert.Contains(expectedMultipart, multiPartData); + } + } + + [Fact] + public async Task GetTextContentByDefaultWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("file.mp3")); + + // Assert + Assert.NotNull(result); + Assert.Equal("Test audio-to-text response", result[0].Text); + } + + [Fact] + public async Task GetTextContentsDoesLogActionAsync() + { + // Assert + var modelId = "whisper-1"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new OpenAIAudioToTextService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetTextContentsAsync(new(new byte[] { 0x01, 0x02 }, "text/plain")); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIAudioToTextService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs new file mode 100644 index 000000000000..e8fdb7b46b1e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextToAudioServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAITextToAudioServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAITextToAudioService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAITextToAudioService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [MemberData(nameof(ExecutionSettings))] + public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType) + { + // Arrange + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + await using var stream = new MemoryStream([0x00, 0x00, 0xFF, 0x7F]); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var exception = await Assert.ThrowsAnyAsync(async () => await service.GetAudioContentsAsync("Some text", settings)); + + // Assert + Assert.NotNull(exception); + Assert.IsType(expectedExceptionType, exception); + } + + [Fact] + public async Task GetAudioContentByDefaultWorksCorrectlyAsync() + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text"); + + // Assert + var audioData = result[0].Data!.Value; + Assert.False(audioData.IsEmpty); + Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); + } + + [Theory] + [InlineData("echo", "wav")] + [InlineData("fable", "opus")] + [InlineData("onyx", "flac")] + [InlineData("nova", "aac")] + [InlineData("shimmer", "pcm")] + public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string format) + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings(voice) { ResponseFormat = format }); + + // Assert + var requestBody = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + var audioData = result[0].Data!.Value; + Assert.Contains($"\"voice\":\"{voice}\"", requestBody); + Assert.Contains($"\"response_format\":\"{format}\"", requestBody); + Assert.False(audioData.IsEmpty); + Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); + } + + [Fact] + public async Task GetAudioContentThrowsWhenVoiceIsNotSupportedAsync() + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice"))); + } + + [Fact] + public async Task GetAudioContentThrowsWhenFormatIsNotSupportedAsync() + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" })); + } + + [Theory] + [InlineData(true, "http://local-endpoint")] + [InlineData(false, "https://api.openai.com")] + public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAddress, string expectedBaseAddress) + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + if (useHttpClientBaseAddress) + { + this._httpClient.BaseAddress = new Uri("http://local-endpoint"); + } + + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text"); + + // Assert + Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); + } + + [Fact] + public async Task GetAudioContentDoesLogActionAsync() + { + // Assert + var modelId = "whisper-1"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new OpenAITextToAudioService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetAudioContentsAsync("description"); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextToAudioService.GetAudioContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + public static TheoryData ExecutionSettings => new() + { + { new OpenAITextToAudioExecutionSettings("invalid"), typeof(NotSupportedException) }, + }; +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs index 919b864327e8..f449059e8ab5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -11,7 +11,7 @@ using OpenAI; using Xunit; -namespace SemanticKernel.Connectors.UnitTests.OpenAI.Services; +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; /// /// Unit tests for class. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs new file mode 100644 index 000000000000..e01345c82f03 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UniTests.Settings; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIAudioToTextExecutionSettingsTests +{ + [Fact] + public void ItReturnsDefaultSettingsWhenSettingsAreNull() + { + Assert.NotNull(OpenAIAudioToTextExecutionSettings.FromExecutionSettings(null)); + } + + [Fact] + public void ItReturnsValidOpenAIAudioToTextExecutionSettings() + { + // Arrange + var audioToTextSettings = new OpenAIAudioToTextExecutionSettings("file.mp3") + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = "text", + Temperature = 0.2f + }; + + // Act + var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(audioToTextSettings); + + // Assert + Assert.Same(audioToTextSettings, settings); + } + + [Fact] + public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() + { + // Arrange + var json = """ + { + "model_id": "model_id", + "language": "en", + "filename": "file.mp3", + "prompt": "prompt", + "response_format": "text", + "temperature": 0.2 + } + """; + + var executionSettings = JsonSerializer.Deserialize(json); + + // Act + var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); + + // Assert + Assert.NotNull(settings); + Assert.Equal("model_id", settings.ModelId); + Assert.Equal("en", settings.Language); + Assert.Equal("file.mp3", settings.Filename); + Assert.Equal("prompt", settings.Prompt); + Assert.Equal("text", settings.ResponseFormat); + Assert.Equal(0.2f, settings.Temperature); + } + + [Fact] + public void ItClonesAllProperties() + { + var settings = new OpenAIAudioToTextExecutionSettings() + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = "text", + Temperature = 0.2f, + Filename = "something.mp3", + }; + + var clone = (OpenAIAudioToTextExecutionSettings)settings.Clone(); + Assert.NotSame(settings, clone); + + Assert.Equal("model_id", clone.ModelId); + Assert.Equal("en", clone.Language); + Assert.Equal("prompt", clone.Prompt); + Assert.Equal("text", clone.ResponseFormat); + Assert.Equal(0.2f, clone.Temperature); + Assert.Equal("something.mp3", clone.Filename); + } + + [Fact] + public void ItFreezesAndPreventsMutation() + { + var settings = new OpenAIAudioToTextExecutionSettings() + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = "text", + Temperature = 0.2f, + Filename = "something.mp3", + }; + + settings.Freeze(); + Assert.True(settings.IsFrozen); + + Assert.Throws(() => settings.ModelId = "new_model"); + Assert.Throws(() => settings.Language = "some_format"); + Assert.Throws(() => settings.Prompt = "prompt"); + Assert.Throws(() => settings.ResponseFormat = "something"); + Assert.Throws(() => settings.Temperature = 0.2f); + Assert.Throws(() => settings.Filename = "something"); + + settings.Freeze(); // idempotent + Assert.True(settings.IsFrozen); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs new file mode 100644 index 000000000000..f30478e15acf --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs @@ -0,0 +1,108 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UniTests.Settings; + +/// +/// Unit tests for class. +/// +public sealed class OpenAITextToAudioExecutionSettingsTests +{ + [Fact] + public void ItReturnsDefaultSettingsWhenSettingsAreNull() + { + Assert.NotNull(OpenAITextToAudioExecutionSettings.FromExecutionSettings(null)); + } + + [Fact] + public void ItReturnsValidOpenAITextToAudioExecutionSettings() + { + // Arrange + var textToAudioSettings = new OpenAITextToAudioExecutionSettings("voice") + { + ModelId = "model_id", + ResponseFormat = "mp3", + Speed = 1.0f + }; + + // Act + var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(textToAudioSettings); + + // Assert + Assert.Same(textToAudioSettings, settings); + } + + [Fact] + public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() + { + // Arrange + var json = """ + { + "model_id": "model_id", + "voice": "voice", + "response_format": "mp3", + "speed": 1.2 + } + """; + + var executionSettings = JsonSerializer.Deserialize(json); + + // Act + var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + + // Assert + Assert.NotNull(settings); + Assert.Equal("model_id", settings.ModelId); + Assert.Equal("voice", settings.Voice); + Assert.Equal("mp3", settings.ResponseFormat); + Assert.Equal(1.2f, settings.Speed); + } + + [Fact] + public void ItClonesAllProperties() + { + var textToAudioSettings = new OpenAITextToAudioExecutionSettings() + { + ModelId = "some_model", + ResponseFormat = "some_format", + Speed = 3.14f, + Voice = "something" + }; + + var clone = (OpenAITextToAudioExecutionSettings)textToAudioSettings.Clone(); + Assert.NotSame(textToAudioSettings, clone); + + Assert.Equal("some_model", clone.ModelId); + Assert.Equal("some_format", clone.ResponseFormat); + Assert.Equal(3.14f, clone.Speed); + Assert.Equal("something", clone.Voice); + } + + [Fact] + public void ItFreezesAndPreventsMutation() + { + var textToAudioSettings = new OpenAITextToAudioExecutionSettings() + { + ModelId = "some_model", + ResponseFormat = "some_format", + Speed = 3.14f, + Voice = "something" + }; + + textToAudioSettings.Freeze(); + Assert.True(textToAudioSettings.IsFrozen); + + Assert.Throws(() => textToAudioSettings.ModelId = "new_model"); + Assert.Throws(() => textToAudioSettings.ResponseFormat = "some_format"); + Assert.Throws(() => textToAudioSettings.Speed = 3.14f); + Assert.Throws(() => textToAudioSettings.Voice = "something"); + + textToAudioSettings.Freeze(); // idempotent + Assert.True(textToAudioSettings.IsFrozen); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs new file mode 100644 index 000000000000..77ec85fe9c10 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Audio; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Input audio to generate the text + /// Audio-to-text execution settings for the prompt + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task> GetTextFromAudioContentsAsync( + AudioContent input, + PromptExecutionSettings? executionSettings, + CancellationToken cancellationToken) + { + if (!input.CanRead) + { + throw new ArgumentException("The input audio content is not readable.", nameof(input)); + } + + OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; + AudioTranscriptionOptions? audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); + + Verify.ValidFilename(audioExecutionSettings?.Filename); + + using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); + + AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; + + return [new(responseData.Text, this.ModelId, metadata: GetResponseMetadata(responseData))]; + } + + /// + /// Converts to type. + /// + /// Instance of . + /// Instance of . + private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) + => new() + { + Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), + Language = executionSettings.Language, + Prompt = executionSettings.Prompt, + Temperature = executionSettings.Temperature + }; + + private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) + { + AudioTimestampGranularities result = AudioTimestampGranularities.Default; + + if (granularities is not null) + { + foreach (var granularity in granularities) + { + var openAIGranularity = granularity switch + { + OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, + OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, + _ => AudioTimestampGranularities.Default + }; + + result |= openAIGranularity; + } + } + + return result; + } + + private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) + => new(3) + { + [nameof(audioTranscription.Language)] = audioTranscription.Language, + [nameof(audioTranscription.Duration)] = audioTranscription.Duration, + [nameof(audioTranscription.Segments)] = audioTranscription.Segments + }; +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs new file mode 100644 index 000000000000..75e484a489aa --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Audio; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Prompt to generate the image + /// Text to Audio execution settings for the prompt + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task> GetAudioContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + CancellationToken cancellationToken) + { + Verify.NotNullOrWhiteSpace(prompt); + + OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings?.ResponseFormat); + SpeechGenerationOptions options = new() + { + ResponseFormat = responseFormat, + Speed = audioExecutionSettings?.Speed, + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); + + return [new AudioContent(response.Value.ToArray(), mimeType)]; + } + + private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice) + => voice?.ToUpperInvariant() switch + { + "ALLOY" => GeneratedSpeechVoice.Alloy, + "ECHO" => GeneratedSpeechVoice.Echo, + "FABLE" => GeneratedSpeechVoice.Fable, + "ONYX" => GeneratedSpeechVoice.Onyx, + "NOVA" => GeneratedSpeechVoice.Nova, + "SHIMMER" => GeneratedSpeechVoice.Shimmer, + _ => throw new NotSupportedException($"The voice '{voice}' is not supported."), + }; + + private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) + => format?.ToUpperInvariant() switch + { + "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"), + "MP3" => (GeneratedSpeechFormat.Mp3, "audio/mpeg"), + "OPUS" => (GeneratedSpeechFormat.Opus, "audio/opus"), + "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"), + "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"), + "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), + _ => throw new NotSupportedException($"The format '{format}' is not supported.") + }; +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index 567d82726e4b..ce4a4d9866e0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -1,13 +1,19 @@ // Copyright (c) Microsoft. All rights reserved. +/* Phase 4 +- Added missing OpenAIClient extensions for audio +- Updated the Experimental attribute to the correct value 0001 -> 0010 (Connector) + */ using System; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -89,7 +95,7 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( #region Text to Image /// - /// Add the OpenAI Dall-E text to image service to the list + /// Add the OpenAI text-to-image service to the list /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -115,7 +121,7 @@ public static IKernelBuilder AddOpenAITextToImage( } /// - /// Add the OpenAI Dall-E text to image service to the list + /// Add the OpenAI text-to-image service to the list /// /// The instance to augment. /// The model to use for image generation. @@ -149,4 +155,136 @@ public static IKernelBuilder AddOpenAITextToImage( return builder; } #endregion + + #region Text to Audio + + /// + /// Adds the OpenAI text-to-audio service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextToAudio( + this IKernelBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToAudioService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService())); + + return builder; + } + + /// + /// Add the OpenAI text-to-audio service to the list + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAITextToAudio( + this IKernelBuilder builder, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToAudioService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService())); + + return builder; + } + + #endregion + + #region Audio-to-Text + + /// + /// Adds the OpenAI audio-to-text service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAIAudioToText( + this IKernelBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + + OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + + return builder; + } + + /// + /// Adds the OpenAI audio-to-text service to the list. + /// + /// The instance to augment. + /// OpenAI model id + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAIAudioToText( + this IKernelBuilder builder, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(builder); + + OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + + return builder; + } + + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index 77355de7f24e..769634c1cea7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -4,9 +4,11 @@ using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -89,7 +91,7 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceC #region Text to Image /// - /// Add the OpenAI Dall-E text to image service to the list + /// Add the OpenAI text-to-image service to the list /// /// The instance to augment. /// The model to use for image generation. @@ -143,4 +145,125 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se serviceProvider.GetService())); } #endregion + + #region Text to Audio + + /// + /// Adds the OpenAI text-to-audio service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextToAudio( + this IServiceCollection services, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToAudioService( + modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService())); + } + + /// + /// Adds the OpenAI text-to-audio service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAITextToAudio( + this IServiceCollection services, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(services); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAITextToAudioService( + modelId, + openAIClient ?? serviceProvider.GetRequiredService(), + serviceProvider.GetService())); + } + + #endregion + + #region Audio-to-Text + + /// + /// Adds the OpenAI audio-to-text service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// Non-default endpoint for the OpenAI API. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAIAudioToText( + this IServiceCollection services, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + Uri? endpoint = null) + { + Verify.NotNull(services); + + OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + apiKey, + orgId, + endpoint, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, (Func)Factory); + + return services; + } + + /// + /// Adds the OpenAI audio-to-text service to the list. + /// + /// The instance to augment. + /// OpenAI model id + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAIAudioToText( + this IServiceCollection services, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(services); + + OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, (Func)Factory); + + return services; + } + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs new file mode 100644 index 000000000000..a226d6c59040 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Services; +using OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI text-to-audio service. +/// +[Experimental("SKEXP0010")] +public sealed class OpenAIAudioToTextService : IAudioToTextService +{ + /// + /// OpenAI text-to-audio client for HTTP operations. + /// + private readonly ClientCore _client; + + /// + /// Gets the attribute name used to store the organization in the dictionary. + /// + public static string OrganizationKey => "Organization"; + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + /// Creates an instance of the with API key auth. + /// + /// Model name + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Non-default endpoint for the OpenAI API. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIAudioToTextService( + string modelId, + string apiKey, + string? organization = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + } + + /// + /// Creates an instance of the with API key auth. + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIAudioToTextService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + } + + /// + public Task> GetTextContentsAsync( + AudioContent content, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index a4dd48ba75e3..ea607b2565b3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -22,7 +22,7 @@ Adding the non-default endpoint parameter to the constructor. [Experimental("SKEXP0010")] public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService { - private readonly ClientCore _core; + private readonly ClientCore _client; private readonly int? _dimensions; /// @@ -44,7 +44,7 @@ public OpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new( + this._client = new( modelId: modelId, apiKey: apiKey, endpoint: endpoint, @@ -68,12 +68,12 @@ public OpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); this._dimensions = dimensions; } /// - public IReadOnlyDictionary Attributes => this._core.Attributes; + public IReadOnlyDictionary Attributes => this._client.Attributes; /// public Task>> GenerateEmbeddingsAsync( @@ -81,7 +81,7 @@ public Task>> GenerateEmbeddingsAsync( Kernel? kernel = null, CancellationToken cancellationToken = default) { - this._core.LogActionDetails(); - return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); + this._client.LogActionDetails(); + return this._client.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs new file mode 100644 index 000000000000..87346eefb1b5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToAudio; +using OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI text-to-audio service. +/// +[Experimental("SKEXP0010")] +public sealed class OpenAITextToAudioService : ITextToAudioService +{ + /// + /// OpenAI text-to-audio client for HTTP operations. + /// + private readonly ClientCore _client; + + /// + /// Gets the attribute name used to store the organization in the dictionary. + /// + public static string OrganizationKey => "Organization"; + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + /// Creates an instance of the with API key auth. + /// + /// Model name + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Non-default endpoint for the OpenAI API. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAITextToAudioService( + string modelId, + string apiKey, + string? organization = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + } + + /// + /// Creates an instance of the with API key auth. + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAITextToAudioService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + } + + /// + public Task> GetAudioContentsAsync( + string text, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 55eca0e112eb..1a6038aa3f43 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -28,10 +28,10 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; [Experimental("SKEXP0010")] public class OpenAITextToImageService : ITextToImageService { - private readonly ClientCore _core; + private readonly ClientCore _client; /// - public IReadOnlyDictionary Attributes => this._core.Attributes; + public IReadOnlyDictionary Attributes => this._client.Attributes; /// /// Initializes a new instance of the class. @@ -50,7 +50,7 @@ public OpenAITextToImageService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._core = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); + this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); } /// @@ -64,13 +64,13 @@ public OpenAITextToImageService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { - this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); } /// public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) { - this._core.LogActionDetails(); - return this._core.GenerateImageAsync(description, width, height, cancellationToken); + this._client.LogActionDetails(); + return this._client.GenerateImageAsync(description, width, height, cancellationToken); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs new file mode 100644 index 000000000000..5d87768c5ddd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Execution settings for OpenAI audio-to-text request. +/// +[Experimental("SKEXP0010")] +public sealed class OpenAIAudioToTextExecutionSettings : PromptExecutionSettings +{ + /// + /// Filename or identifier associated with audio data. + /// Should be in format {filename}.{extension} + /// + [JsonPropertyName("filename")] + public string Filename + { + get => this._filename; + + set + { + this.ThrowIfFrozen(); + this._filename = value; + } + } + + /// + /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es'). + /// + [JsonPropertyName("language")] + public string? Language + { + get => this._language; + + set + { + this.ThrowIfFrozen(); + this._language = value; + } + } + + /// + /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. + /// + [JsonPropertyName("prompt")] + public string? Prompt + { + get => this._prompt; + + set + { + this.ThrowIfFrozen(); + this._prompt = value; + } + } + + /// + /// The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. Default is 'json'. + /// + [JsonPropertyName("response_format")] + public string ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The sampling temperature, between 0 and 1. + /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + /// If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + /// Default is 0. + /// + [JsonPropertyName("temperature")] + public float Temperature + { + get => this._temperature; + + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. + /// + [JsonPropertyName("granularities")] + public IReadOnlyList? Granularities { get; set; } + + /// + /// Creates an instance of class with default filename - "file.mp3". + /// + public OpenAIAudioToTextExecutionSettings() + : this(DefaultFilename) + { + } + + /// + /// Creates an instance of class. + /// + /// Filename or identifier associated with audio data. Should be in format {filename}.{extension} + public OpenAIAudioToTextExecutionSettings(string filename) + { + this._filename = filename; + } + + /// + public override PromptExecutionSettings Clone() + { + return new OpenAIAudioToTextExecutionSettings(this.Filename) + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + ResponseFormat = this.ResponseFormat, + Language = this.Language, + Prompt = this.Prompt + }; + } + + /// + /// Converts to derived type. + /// + /// Instance of . + /// Instance of . + public static OpenAIAudioToTextExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings) + { + if (executionSettings is null) + { + return new OpenAIAudioToTextExecutionSettings(); + } + + if (executionSettings is OpenAIAudioToTextExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + + return openAIExecutionSettings!; + } + + /// + /// The timestamp granularities available to populate transcriptions. + /// + public enum TimeStampGranularities + { + /// + /// Not specified. + /// + Default = 0, + + /// + /// The transcription is segmented by word. + /// + Word = 1, + + /// + /// The timestamp of transcription is by segment. + /// + Segment = 2, + } + + #region private ================================================================================ + + private const string DefaultFilename = "file.mp3"; + + private float _temperature = 0; + private string _responseFormat = "json"; + private string _filename; + private string? _language; + private string? _prompt; + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs new file mode 100644 index 000000000000..8fca703901eb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* Phase 4 +Bringing the OpenAITextToAudioExecutionSettings class to the OpenAIV2 connector as is + +*/ + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Execution settings for OpenAI text-to-audio request. +/// +[Experimental("SKEXP0001")] +public sealed class OpenAITextToAudioExecutionSettings : PromptExecutionSettings +{ + /// + /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. + /// + [JsonPropertyName("voice")] + public string Voice + { + get => this._voice; + + set + { + this.ThrowIfFrozen(); + this._voice = value; + } + } + + /// + /// The format to audio in. Supported formats are mp3, opus, aac, and flac. + /// + [JsonPropertyName("response_format")] + public string ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. + /// + [JsonPropertyName("speed")] + public float Speed + { + get => this._speed; + + set + { + this.ThrowIfFrozen(); + this._speed = value; + } + } + + /// + /// Creates an instance of class with default voice - "alloy". + /// + public OpenAITextToAudioExecutionSettings() + : this(DefaultVoice) + { + } + + /// + /// Creates an instance of class. + /// + /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. + public OpenAITextToAudioExecutionSettings(string? voice) + { + this._voice = voice ?? DefaultVoice; + } + + /// + public override PromptExecutionSettings Clone() + { + return new OpenAITextToAudioExecutionSettings(this.Voice) + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Speed = this.Speed, + ResponseFormat = this.ResponseFormat + }; + } + + /// + /// Converts to derived type. + /// + /// Instance of . + /// Instance of . + public static OpenAITextToAudioExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings) + { + if (executionSettings is null) + { + return new OpenAITextToAudioExecutionSettings(); + } + + if (executionSettings is OpenAITextToAudioExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + + return openAIExecutionSettings!; + } + + #region private ================================================================================ + + private const string DefaultVoice = "alloy"; + + private float _speed = 1.0f; + private string _responseFormat = "mp3"; + private string _voice; + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs new file mode 100644 index 000000000000..f1ead5f9b9c5 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAIAudioToTextTests() +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Fact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + public async Task OpenAIAudioToTextTestAsync() + { + // Arrange + const string Filename = "test_audio.wav"; + + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIAudioToText").Get(); + Assert.NotNull(openAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddOpenAIAudioToText(openAIConfiguration.ModelId, openAIConfiguration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + await using Stream audio = File.OpenRead($"./TestData/{Filename}"); + var audioData = await BinaryData.FromStreamAsync(audio); + + // Act + var result = await service.GetTextContentAsync(new AudioContent(audioData, mimeType: "audio/wav"), new OpenAIAudioToTextExecutionSettings(Filename)); + + // Assert + Assert.Contains("The sun rises in the east and sets in the west.", result.Text, StringComparison.OrdinalIgnoreCase); + } +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs new file mode 100644 index 000000000000..c2818abe2502 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextToAudio; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; + +public sealed class OpenAITextToAudioTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Fact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + public async Task OpenAITextToAudioTestAsync() + { + // Arrange + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToAudio").Get(); + Assert.NotNull(openAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddOpenAITextToAudio(openAIConfiguration.ModelId, openAIConfiguration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + // Act + var result = await service.GetAudioContentAsync("The sun rises in the east and sets in the west."); + + // Assert + var audioData = result.Data!.Value; + Assert.False(audioData.IsEmpty); + } +} diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav b/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav new file mode 100644 index 0000000000000000000000000000000000000000..c6d0edd9a93178162afd3446a32be7cccb822743 GIT binary patch literal 222798 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%0AVO=q3r{Tu-u_WnIi}k?YCb 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 z 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

w(#uq=ZnfnEQq-^Yei1L6g9(J~XOt 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 literal 0 HcmV?d00001 diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs similarity index 94% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs rename to dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs index 7da92e5826ba..75cc074b862d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ClientResultExceptionExtensions.cs +++ b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs @@ -7,13 +7,14 @@ Preserved the logic as is. */ using System.ClientModel; +using System.Diagnostics.CodeAnalysis; using System.Net; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel; ///

/// Provides extension methods for the class. /// +[ExcludeFromCodeCoverage] internal static class ClientResultExceptionExtensions { /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs b/dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs similarity index 95% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs rename to dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs index 0b95f904d893..f7a4e947ec38 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Extensions/ClientResultExceptionExtensionsTests.cs @@ -2,10 +2,9 @@ using System.ClientModel; using System.ClientModel.Primitives; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Xunit; -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; +namespace SemanticKernel.UnitTests.Utilities.OpenAI; public class ClientResultExceptionExtensionsTests { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs similarity index 98% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs rename to dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs index 6fe18b9c1684..2e254c53d04e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockPipelineResponse.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs @@ -12,7 +12,7 @@ This class was imported and adapted from the System.ClientModel Unit Tests. using System.Threading; using System.Threading.Tasks; -namespace SemanticKernel.Connectors.OpenAI.UnitTests; +namespace SemanticKernel.UnitTests.Utilities.OpenAI; public class MockPipelineResponse : PipelineResponse { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs similarity index 94% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs rename to dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs index fceef64e4bae..97c9776b4b25 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Utils/MockResponseHeaders.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs @@ -9,7 +9,7 @@ This class was imported and adapted from the System.ClientModel Unit Tests. using System.ClientModel.Primitives; using System.Collections.Generic; -namespace SemanticKernel.Connectors.OpenAI.UnitTests; +namespace SemanticKernel.UnitTests.Utilities.OpenAI; public class MockResponseHeaders : PipelineResponseHeaders { From f2665044758b799e6ca629fad1072f90c8879e71 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:19:14 +0100 Subject: [PATCH 017/226] .Net: Split ClientCore class (#7060) ### Motivation and Context The ClientCore class has absorbed functionality relevant to different Azure OpenAI-related services. As a first step to reduce its size, it makes sense to split it into small chunks, where each chunk would be related to a service that uses it. Later, we can move those chunks to the services themselves. ### Description This PR does the following: 1. Does not change any logic in any of the services. 2. Splits the `ClientCore` class into `ClientCore.ChatCompletion` and `ClientCore.Embeddings` files. 3. Refactors the `AzureOpenAIChatCompletionService` and `AzureOpenAITextEmbeddingGenerationService` to use the relevant `ClientCore` pieces. 4. Removes the `AzureOpenAIClientCore` class --- .../Core/AzureOpenAIClientCore.cs | 101 -- .../Core/ClientCore.ChatCompletion.cs | 1203 +++++++++++++++ .../Core/ClientCore.Embeddings.cs | 55 + .../Connectors.AzureOpenAI/Core/ClientCore.cs | 1326 +---------------- .../AzureOpenAIChatCompletionService.cs | 2 +- ...ureOpenAITextEmbeddingGenerationService.cs | 2 +- 6 files changed, 1331 insertions(+), 1358 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs deleted file mode 100644 index 348f65781734..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIClientCore.cs +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Core implementation for Azure OpenAI clients, providing common functionality and properties. -/// -internal sealed class AzureOpenAIClientCore : ClientCore -{ - /// - /// Gets the key used to store the deployment name in the dictionary. - /// - public static string DeploymentNameKey => "DeploymentName"; - - /// - /// OpenAI / Azure OpenAI Client - /// - internal override AzureOpenAIClient Client { get; } - - /// - /// Initializes a new instance of the class using API Key authentication. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - string endpoint, - string apiKey, - HttpClient? httpClient = null, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - Verify.NotNullOrWhiteSpace(apiKey); - - var options = GetAzureOpenAIClientOptions(httpClient); - - this.DeploymentOrModelName = deploymentName; - this.Endpoint = new Uri(endpoint); - this.Client = new AzureOpenAIClient(this.Endpoint, apiKey, options); - } - - /// - /// Initializes a new instance of the class supporting AAD authentication. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - string endpoint, - TokenCredential credential, - HttpClient? httpClient = null, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - - var options = GetAzureOpenAIClientOptions(httpClient); - - this.DeploymentOrModelName = deploymentName; - this.Endpoint = new Uri(endpoint); - this.Client = new AzureOpenAIClient(this.Endpoint, credential, options); - } - - /// - /// Initializes a new instance of the class using the specified OpenAIClient. - /// Note: instances created this way might not have the default diagnostics settings, - /// it's up to the caller to configure the client. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - AzureOpenAIClient openAIClient, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNull(openAIClient); - - this.DeploymentOrModelName = deploymentName; - this.Client = openAIClient; - - this.AddAttribute(DeploymentNameKey, deploymentName); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs new file mode 100644 index 000000000000..e118a4b440e9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -0,0 +1,1203 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; +using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; + private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; + private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; + private const string ModelProvider = "openai"; + private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); + + /// + /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current + /// asynchronous chain of execution. + /// + /// + /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that + /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, + /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close + /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. + /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in + /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that + /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, + /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent + /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made + /// configurable should need arise. + /// + private const int MaxInflightAutoInvokes = 128; + + /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. + private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); + + /// Tracking for . + private static readonly AsyncLocal s_inflightAutoInvokes = new(); + + /// + /// Instance of for metrics. + /// + private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); + + /// + /// Instance of to keep track of the number of prompt tokens used. + /// + private static readonly Counter s_promptTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.prompt", + unit: "{token}", + description: "Number of prompt tokens used"); + + /// + /// Instance of to keep track of the number of completion tokens used. + /// + private static readonly Counter s_completionTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.completion", + unit: "{token}", + description: "Number of completion tokens used"); + + /// + /// Instance of to keep track of the total number of tokens used. + /// + private static readonly Counter s_totalTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.total", + unit: "{token}", + description: "Number of tokens used"); + + private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) + { +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return new Dictionary(8) + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.CreatedAt), completions.CreatedAt }, + { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, + { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completions.Usage), completions.Usage }, + { ContentFilterResultsMetadataKey, completions.GetContentFilterResultForResponse() }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completions.FinishReason), completions.FinishReason.ToString() }, + { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, + }; +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) + { + return new Dictionary(4) + { + { nameof(completionUpdate.Id), completionUpdate.Id }, + { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, + { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, + }; + } + + /// + /// Generate a new chat message + /// + /// Chat history + /// Execution settings for the completion API. + /// The containing services, plugins, and other state for use throughout the operation. + /// Async cancellation token + /// Generated chat message in string format + internal async Task> GetChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + + // Convert the incoming execution settings to OpenAI settings. + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) + { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + + // Make the request. + OpenAIChatCompletion? chatCompletion = null; + AzureOpenAIChatMessageContent chatMessageContent; + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + { + try + { + chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + + this.LogUsage(chatCompletion.Usage); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + if (chatCompletion != null) + { + // Capture available metadata even if the operation failed. + activity + .SetResponseId(chatCompletion.Id) + .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) + .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); + } + throw; + } + + chatMessageContent = this.CreateChatMessageContent(chatCompletion); + activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); + } + + // If we don't want to attempt to invoke any functions, just return the result. + if (!toolCallingConfig.AutoInvoke) + { + return [chatMessageContent]; + } + + Debug.Assert(kernel is not null); + + // Get our single result and extract the function call information. If this isn't a function call, or if it is + // but we're unable to find the function or extract the relevant information, just return the single result. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + if (chatCompletion.ToolCalls.Count == 0) + { + return [chatMessageContent]; + } + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); + } + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); + } + + // Add the original assistant message to the chat messages; this is required for the service + // to understand the tool call responses. Also add the result message to the caller's chat + // history: if they don't want it, they can remove it, but this makes the data available, + // including metadata like usage. + chatForRequest.Add(CreateRequestMessage(chatCompletion)); + chat.Add(chatMessageContent); + + // We must send back a response for every tool call, regardless of whether we successfully executed it or not. + // If we successfully execute it, we'll add the result. If we don't, we'll add an error. + for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) + { + ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (functionToolCall.Kind != ChatToolCallKind.Function) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + AzureOpenAIFunctionToolCall? azureOpenAIFunctionToolCall; + try + { + azureOpenAIFunctionToolCall = new(functionToolCall); + } + catch (JsonException) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = chatMessageContent.ToolCalls.Count + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + + // If filter requested termination, returning latest function result. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + return [chat.Last()]; + } + } + } + } + + internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + + AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) + { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + + // Reset state + contentBuilder?.Clear(); + toolCallIdsByIndex?.Clear(); + functionNamesByIndex?.Clear(); + functionArgumentBuildersByIndex?.Clear(); + + // Stream the response. + IReadOnlyDictionary? metadata = null; + string? streamedName = null; + ChatMessageRole? streamedRole = default; + ChatFinishReason finishReason = default; + ChatToolCall[]? toolCalls = null; + FunctionCallContent[]? functionCallContents = null; + + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + { + // Make the request. + AsyncResultCollection response; + try + { + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); + List? streamedContents = activity is not null ? [] : null; + try + { + while (true) + { + try + { + if (!await responseEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; + metadata = GetChatCompletionMetadata(chatCompletionUpdate); + streamedRole ??= chatCompletionUpdate.Role; + //streamedName ??= update.AuthorName; + finishReason = chatCompletionUpdate.FinishReason ?? default; + + // If we're intending to invoke function calls, we need to consume that function call information. + if (toolCallingConfig.AutoInvoke) + { + foreach (var contentPart in chatCompletionUpdate.ContentUpdate) + { + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } + } + + AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + } + + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentOrModelName, metadata); + + foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) + { + // Using the code below to distinguish and skip non - function call related updates. + // The Kind property of updates can't be reliably used because it's only initialized for the first update. + if (string.IsNullOrEmpty(functionCallUpdate.Id) && + string.IsNullOrEmpty(functionCallUpdate.FunctionName) && + string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) + { + continue; + } + + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( + callId: functionCallUpdate.Id, + name: functionCallUpdate.FunctionName, + arguments: functionCallUpdate.FunctionArgumentsUpdate, + functionCallIndex: functionCallUpdate.Index)); + } + + streamedContents?.Add(openAIStreamingChatMessageContent); + yield return openAIStreamingChatMessageContent; + } + + // Translate all entries into ChatCompletionsFunctionToolCall instances. + toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( + ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + // Translate all entries into FunctionCallContent instances for diagnostics purposes. + functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); + } + finally + { + activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); + await responseEnumerator.DisposeAsync(); + } + } + + // If we don't have a function to invoke, we're done. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + if (!toolCallingConfig.AutoInvoke || + toolCallIdsByIndex is not { Count: > 0 }) + { + yield break; + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + // Log the requests + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); + } + else if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); + } + + // Add the original assistant message to the chat messages; this is required for the service + // to understand the tool call responses. + chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + + // Respond to each tooling request. + for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) + { + ChatToolCall toolCall = toolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (string.IsNullOrEmpty(toolCall.FunctionName)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + try + { + openAIFunctionToolCall = new(toolCall); + } + catch (JsonException) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = toolCalls.Length + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); + + // If filter requested termination, returning latest function result and breaking request iteration loop. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + var lastChatMessage = chat.Last(); + + yield return new AzureOpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); + yield break; + } + } + } + } + + internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + ChatHistory chat = CreateNewChat(prompt, chatSettings); + + await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) + { + yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); + } + } + + internal async Task> GetChatAsTextContentsAsync( + string text, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ChatHistory chat = CreateNewChat(text, chatSettings); + return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) + .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) + .ToList(); + } + + /// Checks if a tool call is for a function that was defined. + private static bool IsRequestableTool(ChatCompletionOptions options, AzureOpenAIFunctionToolCall ftc) + { + IList tools = options.Tools; + for (int i = 0; i < tools.Count; i++) + { + if (tools[i].Kind == ChatToolKind.Function && + string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Create a new empty chat instance + /// + /// Optional chat instructions for the AI service + /// Execution settings + /// Chat object + private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptExecutionSettings? executionSettings = null) + { + var chat = new ChatHistory(); + + // If settings is not provided, create a new chat with the text as the system prompt + AuthorRole textRole = AuthorRole.System; + + if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) + { + chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); + textRole = AuthorRole.User; + } + + if (!string.IsNullOrWhiteSpace(text)) + { + chat.AddMessage(textRole, text!); + } + + return chat; + } + + private ChatCompletionOptions CreateChatCompletionOptions( + AzureOpenAIPromptExecutionSettings executionSettings, + ChatHistory chatHistory, + ToolCallingConfig toolCallingConfig, + Kernel? kernel) + { + var options = new ChatCompletionOptions + { + MaxTokens = executionSettings.MaxTokens, + Temperature = (float?)executionSettings.Temperature, + TopP = (float?)executionSettings.TopP, + FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + PresencePenalty = (float?)executionSettings.PresencePenalty, + Seed = executionSettings.Seed, + User = executionSettings.User, + TopLogProbabilityCount = executionSettings.TopLogprobs, + IncludeLogProbabilities = executionSettings.Logprobs, + ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, + ToolChoice = toolCallingConfig.Choice, + }; + + if (executionSettings.AzureChatDataSource is not null) + { +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + options.AddDataSource(executionSettings.AzureChatDataSource); +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + if (toolCallingConfig.Tools is { Count: > 0 } tools) + { + options.Tools.AddRange(tools); + } + + if (executionSettings.TokenSelectionBiases is not null) + { + foreach (var keyValue in executionSettings.TokenSelectionBiases) + { + options.LogitBiases.Add(keyValue.Key, keyValue.Value); + } + } + + if (executionSettings.StopSequences is { Count: > 0 }) + { + foreach (var s in executionSettings.StopSequences) + { + options.StopSequences.Add(s); + } + } + + return options; + } + + private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) + { + List messages = []; + + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) + { + messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); + } + + foreach (var message in chatHistory) + { + messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); + } + + return messages; + } + + private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) + { + if (chatRole == ChatMessageRole.User) + { + return new UserChatMessage(content) { ParticipantName = name }; + } + + if (chatRole == ChatMessageRole.System) + { + return new SystemChatMessage(content) { ParticipantName = name }; + } + + if (chatRole == ChatMessageRole.Assistant) + { + return new AssistantChatMessage(tools, content) { ParticipantName = name }; + } + + throw new NotImplementedException($"Role {chatRole} is not implemented"); + } + + private static List CreateRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) + { + if (message.Role == AuthorRole.System) + { + return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Tool) + { + // Handling function results represented by the TextContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) + if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && + toolId?.ToString() is string toolIdString) + { + return [new ToolChatMessage(toolIdString, message.Content)]; + } + + // Handling function results represented by the FunctionResultContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) + List? toolMessages = null; + foreach (var item in message.Items) + { + if (item is not FunctionResultContent resultContent) + { + continue; + } + + toolMessages ??= []; + + if (resultContent.Result is Exception ex) + { + toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); + continue; + } + + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + + toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); + } + + if (toolMessages is not null) + { + return toolMessages; + } + + throw new NotSupportedException("No function result provided in the tool message."); + } + + if (message.Role == AuthorRole.User) + { + if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) + { + return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; + } + + return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch + { + TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), + ImageContent imageContent => GetImageContentItem(imageContent), + _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") + }))) + { ParticipantName = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Assistant) + { + var toolCalls = new List(); + + // Handling function calls supplied via either: + // ChatCompletionsToolCall.ToolCalls collection items or + // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. + IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; + if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) + { + tools = toolCallsObject as IEnumerable; + if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) + { + int length = array.GetArrayLength(); + var ftcs = new List(length); + for (int i = 0; i < length; i++) + { + JsonElement e = array[i]; + if (e.TryGetProperty("Id", out JsonElement id) && + e.TryGetProperty("Name", out JsonElement name) && + e.TryGetProperty("Arguments", out JsonElement arguments) && + id.ValueKind == JsonValueKind.String && + name.ValueKind == JsonValueKind.String && + arguments.ValueKind == JsonValueKind.String) + { + ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); + } + } + tools = ftcs; + } + } + + if (tools is not null) + { + toolCalls.AddRange(tools); + } + + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. + HashSet? functionCallIds = null; + foreach (var item in message.Items) + { + if (item is not FunctionCallContent callRequest) + { + continue; + } + + functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); + + if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) + { + continue; + } + + var argument = JsonSerializer.Serialize(callRequest.Arguments); + + toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); + } + + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; + } + + throw new NotSupportedException($"Role {message.Role} is not supported."); + } + + private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) + { + if (imageContent.Data is { IsEmpty: false } data) + { + return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); + } + + if (imageContent.Uri is not null) + { + return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); + } + + throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); + } + + private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) + { + if (completion.Role == ChatMessageRole.System) + { + return ChatMessage.CreateSystemMessage(completion.Content[0].Text); + } + + if (completion.Role == ChatMessageRole.Assistant) + { + return ChatMessage.CreateAssistantMessage(completion); + } + + if (completion.Role == ChatMessageRole.User) + { + return ChatMessage.CreateUserMessage(completion.Content); + } + + throw new NotSupportedException($"Role {completion.Role} is not supported."); + } + + private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) + { + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatCompletionMetadata(completion)); + + message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); + + return message; + } + + private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + { + var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) + { + AuthorName = authorName, + }; + + if (functionCalls is not null) + { + message.Items.AddRange(functionCalls); + } + + return message; + } + + private List GetFunctionCallContents(IEnumerable toolCalls) + { + List result = []; + + foreach (var toolCall in toolCalls) + { + // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. + // This allows consumers to work with functions in an LLM-agnostic way. + if (toolCall.Kind == ChatToolCallKind.Function) + { + Exception? exception = null; + KernelArguments? arguments = null; + try + { + arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); + if (arguments is not null) + { + // Iterate over copy of the names to avoid mutating the dictionary while enumerating it + var names = arguments.Names.ToArray(); + foreach (var name in names) + { + arguments[name] = arguments[name]?.ToString(); + } + } + } + catch (JsonException ex) + { + exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", toolCall.FunctionName, toolCall.Id); + } + } + + var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); + + var functionCallContent = new FunctionCallContent( + functionName: functionName.Name, + pluginName: functionName.PluginName, + id: toolCall.Id, + arguments: arguments) + { + InnerContent = toolCall, + Exception = exception + }; + + result.Add(functionCallContent); + } + } + + return result; + } + + private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) + { + // Log any error + if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) + { + Debug.Assert(result is null); + logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); + } + + // Add the tool response message to the chat messages + result ??= errorMessage ?? string.Empty; + chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); + + // Add the tool response message to the chat history. + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + + if (toolCall.Kind == ChatToolCallKind.Function) + { + // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. + // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. + var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); + } + + chat.Add(message); + } + + private static void ValidateMaxTokens(int? maxTokens) + { + if (maxTokens.HasValue && maxTokens < 1) + { + throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); + } + } + + /// + /// Captures usage details, including token information. + /// + /// Instance of with token usage details. + private void LogUsage(ChatTokenUsage usage) + { + if (usage is null) + { + this.Logger.LogDebug("Token usage information unavailable."); + return; + } + + if (this.Logger.IsEnabled(LogLevel.Information)) + { + this.Logger.LogInformation( + "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", + usage.InputTokens, usage.OutputTokens, usage.TotalTokens); + } + + s_promptTokensCounter.Add(usage.InputTokens); + s_completionTokensCounter.Add(usage.OutputTokens); + s_totalTokensCounter.Add(usage.TotalTokens); + } + + /// + /// Processes the function result. + /// + /// The result of the function call. + /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. + /// A string representation of the function result. + private static string? ProcessFunctionResult(object functionResult, AzureOpenAIToolCallBehavior? toolCallBehavior) + { + if (functionResult is string stringResult) + { + return stringResult; + } + + // This is an optimization to use ChatMessageContent content directly + // without unnecessary serialization of the whole message content class. + if (functionResult is ChatMessageContent chatMessageContent) + { + return chatMessageContent.ToString(); + } + + // For polymorphic serialization of unknown in advance child classes of the KernelContent class, + // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. + // For more details about the polymorphic serialization, see the article at: + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 +#pragma warning disable CS0618 // Type or member is obsolete + return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Executes auto function invocation filters and/or function itself. + /// This method can be moved to when auto function invocation logic will be extracted to common place. + /// + private static async Task OnAutoFunctionInvocationAsync( + Kernel kernel, + AutoFunctionInvocationContext context, + Func functionCallCallback) + { + await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); + + return context; + } + + /// + /// This method will execute auto function invocation filters and function recursively. + /// If there are no registered filters, just function will be executed. + /// If there are registered filters, filter on position will be executed. + /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. + /// Function will be always executed as last step after all filters. + /// + private static async Task InvokeFilterOrFunctionAsync( + IList? autoFunctionInvocationFilters, + Func functionCallCallback, + AutoFunctionInvocationContext context, + int index = 0) + { + if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) + { + await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, + (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); + } + else + { + await functionCallCallback(context).ConfigureAwait(false); + } + } + + private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, AzureOpenAIPromptExecutionSettings executionSettings, int requestIndex) + { + if (executionSettings.ToolCallBehavior is null) + { + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); + + bool autoInvoke = kernel is not null && + executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && + s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + + return new ToolCallingConfig( + Tools: tools ?? [s_nonInvocableFunctionTool], + Choice: choice ?? ChatToolChoice.None, + AutoInvoke: autoInvoke); + } + + private static ChatResponseFormat? GetResponseFormat(AzureOpenAIPromptExecutionSettings executionSettings) + { + switch (executionSettings.ResponseFormat) + { + case ChatResponseFormat formatObject: + // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. + return formatObject; + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + } + break; + } + + return null; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs new file mode 100644 index 000000000000..cc7f6ffdda04 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Embeddings; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an embedding from the given . + /// + /// List of strings to generate embeddings for + /// The containing services, plugins, and other state for use throughout the operation. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The to monitor for cancellation requests. The default is . + /// List of embeddings + internal async Task>> GetEmbeddingsAsync( + IList data, + Kernel? kernel, + int? dimensions, + CancellationToken cancellationToken) + { + var result = new List>(data.Count); + + if (data.Count > 0) + { + var embeddingsOptions = new EmbeddingGenerationOptions() + { + Dimensions = dimensions + }; + + var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentOrModelName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var embeddings = response.Value; + + if (embeddings.Count != data.Count) + { + throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); + } + + for (var i = 0; i < embeddings.Count; i++) + { + result.Add(embeddings[i].Vector); + } + } + + return result; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 9dea5efb2cf9..dc45fdaea59d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -4,70 +4,27 @@ using System.ClientModel; using System.ClientModel.Primitives; using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Linq; using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Azure.AI.OpenAI; +using Azure.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Diagnostics; using Microsoft.SemanticKernel.Http; using OpenAI; -using OpenAI.Audio; -using OpenAI.Chat; -using OpenAI.Embeddings; -using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; - -#pragma warning disable CA2208 // Instantiate argument exceptions correctly namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// /// Base class for AI clients that provides common functionality for interacting with OpenAI services. /// -internal abstract class ClientCore +internal partial class ClientCore { - private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; - private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; - private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; - private const string ModelProvider = "openai"; - private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); - /// - /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current - /// asynchronous chain of execution. + /// Gets the key used to store the deployment name in the dictionary. /// - /// - /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that - /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, - /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close - /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. - /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in - /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that - /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, - /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent - /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made - /// configurable should need arise. - /// - private const int MaxInflightAutoInvokes = 128; - - /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); - - /// Tracking for . - private static readonly AsyncLocal s_inflightAutoInvokes = new(); - - internal ClientCore(ILogger? logger = null) - { - this.Logger = logger ?? NullLogger.Instance; - } + internal static string DeploymentNameKey => "DeploymentName"; /// /// Model Id or Deployment Name @@ -75,10 +32,13 @@ internal ClientCore(ILogger? logger = null) internal string DeploymentOrModelName { get; set; } = string.Empty; /// - /// OpenAI / Azure OpenAI Client + /// Azure OpenAI Client /// - internal abstract AzureOpenAIClient Client { get; } + internal AzureOpenAIClient Client { get; } + /// + /// Azure OpenAI API endpoint. + /// internal Uri? Endpoint { get; set; } = null; /// @@ -92,674 +52,85 @@ internal ClientCore(ILogger? logger = null) internal Dictionary Attributes { get; } = []; /// - /// Instance of for metrics. - /// - private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); - - /// - /// Instance of to keep track of the number of prompt tokens used. + /// Initializes a new instance of the class. /// - private static readonly Counter s_promptTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.prompt", - unit: "{token}", - description: "Number of prompt tokens used"); - - /// - /// Instance of to keep track of the number of completion tokens used. - /// - private static readonly Counter s_completionTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.completion", - unit: "{token}", - description: "Number of completion tokens used"); - - /// - /// Instance of to keep track of the total number of tokens used. - /// - private static readonly Counter s_totalTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.total", - unit: "{token}", - description: "Number of tokens used"); - - private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + internal ClientCore( + string deploymentName, + string endpoint, + string apiKey, + HttpClient? httpClient = null, + ILogger? logger = null) { -#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - return new Dictionary(8) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.CreatedAt), completions.CreatedAt }, - { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, - { nameof(completions.Usage), completions.Usage }, - { ContentFilterResultsMetadataKey, completions.GetContentFilterResultForResponse() }, + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); + Verify.NotNullOrWhiteSpace(apiKey); - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, - }; -#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } + var options = GetAzureOpenAIClientOptions(httpClient); - private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) - { - return new Dictionary(4) - { - { nameof(completionUpdate.Id), completionUpdate.Id }, - { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, - { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, - }; - } + this.Logger = logger ?? NullLogger.Instance; + this.DeploymentOrModelName = deploymentName; + this.Endpoint = new Uri(endpoint); + this.Client = new AzureOpenAIClient(this.Endpoint, apiKey, options); - private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) - { - return new Dictionary(3) - { - { nameof(audioTranscription.Language), audioTranscription.Language }, - { nameof(audioTranscription.Duration), audioTranscription.Duration }, - { nameof(audioTranscription.Segments), audioTranscription.Segments } - }; + this.AddAttribute(DeploymentNameKey, deploymentName); } /// - /// Generates an embedding from the given . + /// Initializes a new instance of the class. /// - /// List of strings to generate embeddings for - /// The containing services, plugins, and other state for use throughout the operation. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The to monitor for cancellation requests. The default is . - /// List of embeddings - internal async Task>> GetEmbeddingsAsync( - IList data, - Kernel? kernel, - int? dimensions, - CancellationToken cancellationToken) + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + internal ClientCore( + string deploymentName, + string endpoint, + TokenCredential credential, + HttpClient? httpClient = null, + ILogger? logger = null) { - var result = new List>(data.Count); - - if (data.Count > 0) - { - var embeddingsOptions = new EmbeddingGenerationOptions() - { - Dimensions = dimensions - }; + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentOrModelName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); - var embeddings = response.Value; + var options = GetAzureOpenAIClientOptions(httpClient); - if (embeddings.Count != data.Count) - { - throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); - } - - for (var i = 0; i < embeddings.Count; i++) - { - result.Add(embeddings[i].Vector); - } - } + this.Logger = logger ?? NullLogger.Instance; + this.DeploymentOrModelName = deploymentName; + this.Endpoint = new Uri(endpoint); + this.Client = new AzureOpenAIClient(this.Endpoint, credential, options); - return result; + this.AddAttribute(DeploymentNameKey, deploymentName); } - //internal async Task> GetTextContentFromAudioAsync( - // AudioContent content, - // PromptExecutionSettings? executionSettings, - // CancellationToken cancellationToken) - //{ - // Verify.NotNull(content.Data); - // var audioData = content.Data.Value; - // if (audioData.IsEmpty) - // { - // throw new ArgumentException("Audio data cannot be empty", nameof(content)); - // } - - // OpenAIAudioToTextExecutionSettings? audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); - - // Verify.ValidFilename(audioExecutionSettings?.Filename); - - // var audioOptions = new AudioTranscriptionOptions - // { - // AudioData = BinaryData.FromBytes(audioData), - // DeploymentName = this.DeploymentOrModelName, - // Filename = audioExecutionSettings.Filename, - // Language = audioExecutionSettings.Language, - // Prompt = audioExecutionSettings.Prompt, - // ResponseFormat = audioExecutionSettings.ResponseFormat, - // Temperature = audioExecutionSettings.Temperature - // }; - - // AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioTranscriptionAsync(audioOptions, cancellationToken)).ConfigureAwait(false)).Value; - - // return [new(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData))]; - //} - /// - /// Generate a new chat message + /// Initializes a new instance of the class.. + /// Note: instances created this way might not have the default diagnostics settings, + /// it's up to the caller to configure the client. /// - /// Chat history - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// Async cancellation token - /// Generated chat message in string format - internal async Task> GetChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chat), - JsonSerializer.Serialize(executionSettings)); - } - - // Convert the incoming execution settings to OpenAI settings. - AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - - for (int requestIndex = 0; ; requestIndex++) - { - var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - - var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); - - // Make the request. - OpenAIChatCompletion? chatCompletion = null; - AzureOpenAIChatMessageContent chatMessageContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) - { - try - { - chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; - - this.LogUsage(chatCompletion.Usage); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (chatCompletion != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(chatCompletion.Id) - .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) - .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); - } - throw; - } - - chatMessageContent = this.CreateChatMessageContent(chatCompletion); - activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); - } - - // If we don't want to attempt to invoke any functions, just return the result. - if (!toolCallingConfig.AutoInvoke) - { - return [chatMessageContent]; - } - - Debug.Assert(kernel is not null); - - // Get our single result and extract the function call information. If this isn't a function call, or if it is - // but we're unable to find the function or extract the relevant information, just return the single result. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - if (chatCompletion.ToolCalls.Count == 0) - { - return [chatMessageContent]; - } - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); - } - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); - } - - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. Also add the result message to the caller's chat - // history: if they don't want it, they can remove it, but this makes the data available, - // including metadata like usage. - chatForRequest.Add(CreateRequestMessage(chatCompletion)); - chat.Add(chatMessageContent); - - // We must send back a response for every tool call, regardless of whether we successfully executed it or not. - // If we successfully execute it, we'll add the result. If we don't, we'll add an error. - for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) - { - ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (functionToolCall.Kind != ChatToolCallKind.Function) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - AzureOpenAIFunctionToolCall? azureOpenAIFunctionToolCall; - try - { - azureOpenAIFunctionToolCall = new(functionToolCall); - } - catch (JsonException) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) - { - Arguments = functionArgs, - RequestSequenceIndex = requestIndex, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = chatMessageContent.ToolCalls.Count - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); - - // If filter requested termination, returning latest function result. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - return [chat.Last()]; - } - } - } - } - - internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chat), - JsonSerializer.Serialize(executionSettings)); - } - - AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - StringBuilder? contentBuilder = null; - Dictionary? toolCallIdsByIndex = null; - Dictionary? functionNamesByIndex = null; - Dictionary? functionArgumentBuildersByIndex = null; - - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - - for (int requestIndex = 0; ; requestIndex++) - { - var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - - var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); - - // Reset state - contentBuilder?.Clear(); - toolCallIdsByIndex?.Clear(); - functionNamesByIndex?.Clear(); - functionArgumentBuildersByIndex?.Clear(); - - // Stream the response. - IReadOnlyDictionary? metadata = null; - string? streamedName = null; - ChatMessageRole? streamedRole = default; - ChatFinishReason finishReason = default; - ChatToolCall[]? toolCalls = null; - FunctionCallContent[]? functionCallContents = null; - - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) - { - // Make the request. - AsyncResultCollection response; - try - { - response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; - metadata = GetChatCompletionMetadata(chatCompletionUpdate); - streamedRole ??= chatCompletionUpdate.Role; - //streamedName ??= update.AuthorName; - finishReason = chatCompletionUpdate.FinishReason ?? default; - - // If we're intending to invoke function calls, we need to consume that function call information. - if (toolCallingConfig.AutoInvoke) - { - foreach (var contentPart in chatCompletionUpdate.ContentUpdate) - { - if (contentPart.Kind == ChatMessageContentPartKind.Text) - { - (contentBuilder ??= new()).Append(contentPart.Text); - } - } - - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - } - - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentOrModelName, metadata); - - foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) - { - // Using the code below to distinguish and skip non - function call related updates. - // The Kind property of updates can't be reliably used because it's only initialized for the first update. - if (string.IsNullOrEmpty(functionCallUpdate.Id) && - string.IsNullOrEmpty(functionCallUpdate.FunctionName) && - string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) - { - continue; - } - - openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( - callId: functionCallUpdate.Id, - name: functionCallUpdate.FunctionName, - arguments: functionCallUpdate.FunctionArgumentsUpdate, - functionCallIndex: functionCallUpdate.Index)); - } - - streamedContents?.Add(openAIStreamingChatMessageContent); - yield return openAIStreamingChatMessageContent; - } - - // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( - ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - - // Translate all entries into FunctionCallContent instances for diagnostics purposes. - functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); - } - finally - { - activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); - await responseEnumerator.DisposeAsync(); - } - } - - // If we don't have a function to invoke, we're done. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - if (!toolCallingConfig.AutoInvoke || - toolCallIdsByIndex is not { Count: > 0 }) - { - yield break; - } - - // Get any response content that was streamed. - string content = contentBuilder?.ToString() ?? string.Empty; - - // Log the requests - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); - } - else if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); - } - - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. - chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); - - // Respond to each tooling request. - for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) - { - ChatToolCall toolCall = toolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (string.IsNullOrEmpty(toolCall.FunctionName)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - AzureOpenAIFunctionToolCall? openAIFunctionToolCall; - try - { - openAIFunctionToolCall = new(toolCall); - } - catch (JsonException) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) - { - Arguments = functionArgs, - RequestSequenceIndex = requestIndex, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = toolCalls.Length - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); - - // If filter requested termination, returning latest function result and breaking request iteration loop. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - var lastChatMessage = chat.Last(); - - yield return new AzureOpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); - yield break; - } - } - } - } - - /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionOptions options, AzureOpenAIFunctionToolCall ftc) + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// The to use for logging. If null, no logging will be performed. + internal ClientCore( + string deploymentName, + AzureOpenAIClient openAIClient, + ILogger? logger = null) { - IList tools = options.Tools; - for (int i = 0; i < tools.Count; i++) - { - if (tools[i].Kind == ChatToolKind.Function && - string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - ChatHistory chat = CreateNewChat(prompt, chatSettings); - - await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) - { - yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); - } - } - - internal async Task> GetChatAsTextContentsAsync( - string text, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNull(openAIClient); - ChatHistory chat = CreateNewChat(text, chatSettings); - return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) - .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) - .ToList(); - } + this.Logger = logger ?? NullLogger.Instance; + this.DeploymentOrModelName = deploymentName; + this.Client = openAIClient; - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } + this.AddAttribute(DeploymentNameKey, deploymentName); } /// Gets options to use for an OpenAIClient @@ -784,395 +155,11 @@ internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? return options; } - /// - /// Create a new empty chat instance - /// - /// Optional chat instructions for the AI service - /// Execution settings - /// Chat object - private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptExecutionSettings? executionSettings = null) - { - var chat = new ChatHistory(); - - // If settings is not provided, create a new chat with the text as the system prompt - AuthorRole textRole = AuthorRole.System; - - if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) - { - chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); - textRole = AuthorRole.User; - } - - if (!string.IsNullOrWhiteSpace(text)) - { - chat.AddMessage(textRole, text!); - } - - return chat; - } - - private ChatCompletionOptions CreateChatCompletionOptions( - AzureOpenAIPromptExecutionSettings executionSettings, - ChatHistory chatHistory, - ToolCallingConfig toolCallingConfig, - Kernel? kernel) - { - var options = new ChatCompletionOptions - { - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - TopP = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - Seed = executionSettings.Seed, - User = executionSettings.User, - TopLogProbabilityCount = executionSettings.TopLogprobs, - IncludeLogProbabilities = executionSettings.Logprobs, - ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, - ToolChoice = toolCallingConfig.Choice, - }; - - if (executionSettings.AzureChatDataSource is not null) - { -#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - options.AddDataSource(executionSettings.AzureChatDataSource); -#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - if (toolCallingConfig.Tools is { Count: > 0 } tools) - { - options.Tools.AddRange(tools); - } - - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.LogitBiases.Add(keyValue.Key, keyValue.Value); - } - } - - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } - - return options; - } - - private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) - { - List messages = []; - - if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) - { - messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); - } - - foreach (var message in chatHistory) - { - messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); - } - - return messages; - } - - private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) - { - if (chatRole == ChatMessageRole.User) - { - return new UserChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.System) - { - return new SystemChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.Assistant) - { - return new AssistantChatMessage(tools, content) { ParticipantName = name }; - } - - throw new NotImplementedException($"Role {chatRole} is not implemented"); - } - - private static List CreateRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) - { - if (message.Role == AuthorRole.System) - { - return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Tool) - { - // Handling function results represented by the TextContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) - if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && - toolId?.ToString() is string toolIdString) - { - return [new ToolChatMessage(toolIdString, message.Content)]; - } - - // Handling function results represented by the FunctionResultContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - List? toolMessages = null; - foreach (var item in message.Items) - { - if (item is not FunctionResultContent resultContent) - { - continue; - } - - toolMessages ??= []; - - if (resultContent.Result is Exception ex) - { - toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); - continue; - } - - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); - - toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); - } - - if (toolMessages is not null) - { - return toolMessages; - } - - throw new NotSupportedException("No function result provided in the tool message."); - } - - if (message.Role == AuthorRole.User) - { - if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) - { - return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; - } - - return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch - { - TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), - ImageContent imageContent => GetImageContentItem(imageContent), - _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") - }))) - { ParticipantName = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Assistant) - { - var toolCalls = new List(); - - // Handling function calls supplied via either: - // ChatCompletionsToolCall.ToolCalls collection items or - // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; - if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) - { - tools = toolCallsObject as IEnumerable; - if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) - { - int length = array.GetArrayLength(); - var ftcs = new List(length); - for (int i = 0; i < length; i++) - { - JsonElement e = array[i]; - if (e.TryGetProperty("Id", out JsonElement id) && - e.TryGetProperty("Name", out JsonElement name) && - e.TryGetProperty("Arguments", out JsonElement arguments) && - id.ValueKind == JsonValueKind.String && - name.ValueKind == JsonValueKind.String && - arguments.ValueKind == JsonValueKind.String) - { - ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); - } - } - tools = ftcs; - } - } - - if (tools is not null) - { - toolCalls.AddRange(tools); - } - - // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. - HashSet? functionCallIds = null; - foreach (var item in message.Items) - { - if (item is not FunctionCallContent callRequest) - { - continue; - } - - functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); - - if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) - { - continue; - } - - var argument = JsonSerializer.Serialize(callRequest.Arguments); - - toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); - } - - return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; - } - - throw new NotSupportedException($"Role {message.Role} is not supported."); - } - - private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) - { - if (imageContent.Data is { IsEmpty: false } data) - { - return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); - } - - if (imageContent.Uri is not null) - { - return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); - } - - throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); - } - - private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) - { - if (completion.Role == ChatMessageRole.System) - { - return ChatMessage.CreateSystemMessage(completion.Content[0].Text); - } - - if (completion.Role == ChatMessageRole.Assistant) - { - return ChatMessage.CreateAssistantMessage(completion); - } - - if (completion.Role == ChatMessageRole.User) - { - return ChatMessage.CreateUserMessage(completion.Content); - } - - throw new NotSupportedException($"Role {completion.Role} is not supported."); - } - - private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) - { - var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatCompletionMetadata(completion)); - - message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); - - return message; - } - - private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) - { - var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) - { - AuthorName = authorName, - }; - - if (functionCalls is not null) - { - message.Items.AddRange(functionCalls); - } - - return message; - } - - private List GetFunctionCallContents(IEnumerable toolCalls) - { - List result = []; - - foreach (var toolCall in toolCalls) - { - // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. - // This allows consumers to work with functions in an LLM-agnostic way. - if (toolCall.Kind == ChatToolCallKind.Function) - { - Exception? exception = null; - KernelArguments? arguments = null; - try - { - arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); - if (arguments is not null) - { - // Iterate over copy of the names to avoid mutating the dictionary while enumerating it - var names = arguments.Names.ToArray(); - foreach (var name in names) - { - arguments[name] = arguments[name]?.ToString(); - } - } - } - catch (JsonException ex) - { - exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", toolCall.FunctionName, toolCall.Id); - } - } - - var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); - - var functionCallContent = new FunctionCallContent( - functionName: functionName.Name, - pluginName: functionName.PluginName, - id: toolCall.Id, - arguments: arguments) - { - InnerContent = toolCall, - Exception = exception - }; - - result.Add(functionCallContent); - } - } - - return result; - } - - private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) - { - // Log any error - if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) - { - Debug.Assert(result is null); - logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); - } - - // Add the tool response message to the chat messages - result ??= errorMessage ?? string.Empty; - chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); - - // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); - - if (toolCall.Kind == ChatToolCallKind.Function) - { - // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. - // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); - } - - chat.Add(message); - } - - private static void ValidateMaxTokens(int? maxTokens) + internal void AddAttribute(string key, string? value) { - if (maxTokens.HasValue && maxTokens < 1) + if (!string.IsNullOrEmpty(value)) { - throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); + this.Attributes.Add(key, value); } } @@ -1200,177 +187,6 @@ private static T RunRequest(Func request) } } - /// - /// Captures usage details, including token information. - /// - /// Instance of with token usage details. - private void LogUsage(ChatTokenUsage usage) - { - if (usage is null) - { - this.Logger.LogDebug("Token usage information unavailable."); - return; - } - - if (this.Logger.IsEnabled(LogLevel.Information)) - { - this.Logger.LogInformation( - "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", - usage.InputTokens, usage.OutputTokens, usage.TotalTokens); - } - - s_promptTokensCounter.Add(usage.InputTokens); - s_completionTokensCounter.Add(usage.OutputTokens); - s_totalTokensCounter.Add(usage.TotalTokens); - } - - /// - /// Processes the function result. - /// - /// The result of the function call. - /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. - /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, AzureOpenAIToolCallBehavior? toolCallBehavior) - { - if (functionResult is string stringResult) - { - return stringResult; - } - - // This is an optimization to use ChatMessageContent content directly - // without unnecessary serialization of the whole message content class. - if (functionResult is ChatMessageContent chatMessageContent) - { - return chatMessageContent.ToString(); - } - - // For polymorphic serialization of unknown in advance child classes of the KernelContent class, - // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. - // For more details about the polymorphic serialization, see the article at: - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 -#pragma warning disable CS0618 // Type or member is obsolete - return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); -#pragma warning restore CS0618 // Type or member is obsolete - } - - /// - /// Executes auto function invocation filters and/or function itself. - /// This method can be moved to when auto function invocation logic will be extracted to common place. - /// - private static async Task OnAutoFunctionInvocationAsync( - Kernel kernel, - AutoFunctionInvocationContext context, - Func functionCallCallback) - { - await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); - - return context; - } - - /// - /// This method will execute auto function invocation filters and function recursively. - /// If there are no registered filters, just function will be executed. - /// If there are registered filters, filter on position will be executed. - /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. - /// Function will be always executed as last step after all filters. - /// - private static async Task InvokeFilterOrFunctionAsync( - IList? autoFunctionInvocationFilters, - Func functionCallCallback, - AutoFunctionInvocationContext context, - int index = 0) - { - if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) - { - await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, - (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); - } - else - { - await functionCallCallback(context).ConfigureAwait(false); - } - } - - private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, AzureOpenAIPromptExecutionSettings executionSettings, int requestIndex) - { - if (executionSettings.ToolCallBehavior is null) - { - return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); - } - - if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - - return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); - } - - var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); - - bool autoInvoke = kernel is not null && - executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && - s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } - - return new ToolCallingConfig( - Tools: tools ?? [s_nonInvocableFunctionTool], - Choice: choice ?? ChatToolChoice.None, - AutoInvoke: autoInvoke); - } - - private static ChatResponseFormat? GetResponseFormat(AzureOpenAIPromptExecutionSettings executionSettings) - { - switch (executionSettings.ResponseFormat) - { - case ChatResponseFormat formatObject: - // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. - return formatObject; - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - } - break; - } - - return null; - } - private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) { return new GenericActionPipelinePolicy((message) => diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs index 9d771c4f7abb..bd06f49bfefa 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs @@ -20,7 +20,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAIChatCompletionService : IChatCompletionService, ITextGenerationService { /// Core implementation shared by Azure OpenAI clients. - private readonly AzureOpenAIClientCore _core; + private readonly ClientCore _core; /// /// Create an instance of the connector with API key auth. diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs index 31159da6f0a5..103f1bbcf3ca 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -20,7 +20,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; [Experimental("SKEXP0010")] public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService { - private readonly AzureOpenAIClientCore _core; + private readonly ClientCore _core; private readonly int? _dimensions; /// From edb74420811dc726a70a0f6eb21494e59bd91a49 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 3 Jul 2024 10:46:31 +0100 Subject: [PATCH 018/226] .Net: Tidying up AzureOpenAIChatCompletionService (#7073) ### Motivation, Context and Description This PR fixes a small issue that would occur if the LLM started calling tools that are not functions. Additionally, it renames the `openAIClient` parameter of the `AzureOpenAIChatCompletionService` class constructor to `azureOpenAIClient` to keep the name consistent with its type. It also renames the `AzureOpenAIChatMessageContent.GetOpenAIFunctionToolCalls` method to `GetFunctionToolCalls` because the old one is not relevant anymore. The last two changes are breaking changes and it will be decided in the scope of the https://github.com/microsoft/semantic-kernel/issues/7053 issue whether to keep them or roll them back. --- .../Core/AzureOpenAIChatMessageContentTests.cs | 4 ++-- .../Core/AzureOpenAIChatMessageContent.cs | 6 +++--- .../Services/AzureOpenAIChatCompletionService.cs | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs index 76e0b2064439..49832b221978 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs @@ -41,8 +41,8 @@ public void GetOpenAIFunctionToolCallsReturnsCorrectList() var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); // Act - var actualToolCalls1 = content1.GetOpenAIFunctionToolCalls(); - var actualToolCalls2 = content2.GetOpenAIFunctionToolCalls(); + var actualToolCalls1 = content1.GetFunctionToolCalls(); + var actualToolCalls2 = content2.GetFunctionToolCalls(); // Assert Assert.Equal(2, actualToolCalls1.Count); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs index ff7183cb0b12..8112d2c7dee4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -75,15 +75,15 @@ private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList /// Retrieve the resulting function from the chat result. /// /// The , or null if no function was returned by the model. - public IReadOnlyList GetOpenAIFunctionToolCalls() + public IReadOnlyList GetFunctionToolCalls() { List? functionToolCallList = null; foreach (var toolCall in this.ToolCalls) { - if (toolCall is ChatToolCall functionToolCall) + if (toolCall.Kind == ChatToolCallKind.Function) { - (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(functionToolCall)); + (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(toolCall)); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs index bd06f49bfefa..809c6fb21f6c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs @@ -69,16 +69,16 @@ public AzureOpenAIChatCompletionService( /// Creates a new client instance using the specified . /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . + /// Custom . /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. public AzureOpenAIChatCompletionService( string deploymentName, - AzureOpenAIClient openAIClient, + AzureOpenAIClient azureOpenAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } From d3cf959454b5f452ca8e75a5a2ee9b1e7316f6fc Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 3 Jul 2024 11:55:34 +0100 Subject: [PATCH 019/226] .Net: Remove exception utility duplicate (#7074) ### Motivation, Context and Description This PR removes the duplicate ClientResultExceptionExtensions extension class from the new AzureOpenAI project in favor of the existing extension class in the utils. It also includes the utils class in the SK solution, making it visible in the Visual Studio Explorer. --- dotnet/SK-dotnet.sln | 6 +++ .../ClientResultExceptionExtensions.cs | 39 ------------------- 2 files changed, 6 insertions(+), 39 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 6da6c33ec47a..7b2e556f616d 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -337,6 +337,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Policies", "Policies", "{73 src\InternalUtilities\openai\Policies\GeneratedActionPipelinePolicy.cs = src\InternalUtilities\openai\Policies\GeneratedActionPipelinePolicy.cs EndProjectSection EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\openai\Extensions\ClientResultExceptionExtensions.cs = src\InternalUtilities\openai\Extensions\ClientResultExceptionExtensions.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -944,6 +949,7 @@ Global {DB219924-208B-4CDD-8796-EE424689901E} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {7308EF7D-5F9A-47B2-A62F-0898603262A8} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} + {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs deleted file mode 100644 index fd282797e879..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ClientResultExceptionExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel; -using System.Net; -using Azure; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Provides extension methods for the class. -/// -internal static class ClientResultExceptionExtensions -{ - /// - /// Converts a to an . - /// - /// The original . - /// An instance. - public static HttpOperationException ToHttpOperationException(this ClientResultException exception) - { - const int NoResponseReceived = 0; - - string? responseContent = null; - - try - { - responseContent = exception.GetRawResponse()?.Content?.ToString(); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead. -#pragma warning restore CA1031 - - return new HttpOperationException( - exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status, - responseContent, - exception.Message, - exception); - } -} From 1f16875fef34c5c61fad02181d3cb86d896507a2 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 3 Jul 2024 14:48:28 +0100 Subject: [PATCH 020/226] .Net: Split service collection and kernel builder extension methods into separate classes. (#7078) ### Motivation, Context and Description This PR moves Azure-specific kernel builder extension methods from the `AzureOpenAIServiceCollectionExtensions` class to a newly introduced one - `AzureOpenAIKernelBuilderExtensions`, **as they are, with no functional changes** to follow the approach taken in SK - one file/class per type being extended. --- .../AzureOpenAIKernelBuilderExtensions.cs | 256 ++++++++++++++++++ .../AzureOpenAIServiceCollectionExtensions.cs | 219 +-------------- 2 files changed, 257 insertions(+), 218 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs new file mode 100644 index 000000000000..0a391e4693c0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -0,0 +1,256 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Azure; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Embeddings; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextGeneration; + +#pragma warning disable IDE0039 // Use local function + +namespace Microsoft.SemanticKernel; + +/// +/// Provides extension methods for to configure Azure OpenAI connectors. +/// +public static class AzureOpenAIKernelBuilderExtensions +{ + #region Chat Completion + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + new AzureKeyCredential(apiKey), + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + credentials, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the Azure OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + public static IKernelBuilder AddAzureOpenAIChatCompletion( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + + Func factory = (serviceProvider, _) => + new(deploymentName, azureOpenAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, factory); + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + #endregion + + #region Text Embedding + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credential, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + Verify.NotNull(builder); + Verify.NotNull(credential); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + endpoint, + credential, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + /// + /// Adds an Azure OpenAI text embeddings service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? serviceId = null, + string? modelId = null, + int? dimensions = null) + { + Verify.NotNull(builder); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextEmbeddingGenerationService( + deploymentName, + azureOpenAIClient ?? serviceProvider.GetRequiredService(), + modelId, + serviceProvider.GetService(), + dimensions)); + + return builder; + } + + #endregion + + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); + + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => + new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index e25eac02789b..0c719ed8b249 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -19,52 +19,12 @@ namespace Microsoft.SemanticKernel; /// -/// Provides extension methods for and related classes to configure Azure OpenAI connectors. +/// Provides extension methods for to configure Azure OpenAI connectors. /// public static class AzureOpenAIServiceCollectionExtensions { #region Chat Completion - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - { - AzureOpenAIClient client = CreateAzureOpenAIClient( - endpoint, - new AzureKeyCredential(apiKey), - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - /// /// Adds the Azure OpenAI chat completion service to the list. /// @@ -103,46 +63,6 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( return services; } - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - Func factory = (serviceProvider, _) => - { - AzureOpenAIClient client = CreateAzureOpenAIClient( - endpoint, - credentials, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - /// /// Adds the Azure OpenAI chat completion service to the list. /// @@ -181,34 +101,6 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( return services; } - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - AzureOpenAIClient? azureOpenAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - Func factory = (serviceProvider, _) => - new(deploymentName, azureOpenAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - /// /// Adds the Azure OpenAI chat completion service to the list. /// @@ -241,44 +133,6 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( #region Text Embedding - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - Verify.NotNull(builder); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - /// /// Adds an Azure OpenAI text embeddings service to the list. /// @@ -313,45 +167,6 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( dimensions)); } - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credential, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNull(credential); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - credential, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - /// /// Adds an Azure OpenAI text embeddings service to the list. /// @@ -387,38 +202,6 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( dimensions)); } - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - AzureOpenAIClient? azureOpenAIClient = null, - string? serviceId = null, - string? modelId = null, - int? dimensions = null) - { - Verify.NotNull(builder); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - azureOpenAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService(), - dimensions)); - - return builder; - } - /// /// Adds an Azure OpenAI text embeddings service to the list. /// From 47676ae6151ff26788c2e6f157c6a65b2be4805c Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 3 Jul 2024 15:58:01 +0100 Subject: [PATCH 021/226] .Net: Copy OpenAITextToImageService related code to AzureOpenAI project (#7077) ### Motivation, Context and Description This PR copies the `OpenAITextToImageService` class and related code, including unit tests, from the `Connectors.OpenAIV2` project to the `Connectors.AzureOpenAI` project. The copied classes have no functional changes; they have only been renamed to have the `Azure` prefix and placed into the corresponding `Microsoft.SemanticKernel.Connectors.AzureOpenAI` namespace. All the classes are temporarily excluded from the compilation process. This is done to simplify the code review of follow-up PR(s) that will include functional changes. A few small fixes, unrelated to the main purpose of the PR, were made to the XML documentation comments and logging in the `OpenAIAudioToTextService` and `OpenAITextToImageService` classes. --- .../Connectors.AzureOpenAI.UnitTests.csproj | 8 ++ .../AzureOpenAITextToImageServiceTests.cs | 107 ++++++++++++++++++ .../Connectors.AzureOpenAI.csproj | 10 ++ .../Core/ClientCore.TextToImage.cs | 44 +++++++ .../Services/AzureOpenAITextToImageService.cs | 66 +++++++++++ .../Services/OpenAIAudioToTextService.cs | 6 +- .../Services/OpenAITextToImageService.cs | 2 +- 7 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj index a0a695a6719c..056ac691dfa4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -30,6 +30,14 @@ + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs new file mode 100644 index 000000000000..b0d44113febb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Services; +using Moq; +using OpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextToImageServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextToImageServiceTests() + { + this._messageHandlerStub = new() + { + ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("./TestData/text-to-image-response.txt")) + } + }; + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Fact] + public void ConstructorWorksCorrectly() + { + // Arrange & Act + var sut = new AzureOpenAITextToImageServiceTests("model", "api-key", "organization"); + + // Assert + Assert.NotNull(sut); + Assert.Equal("organization", sut.Attributes[ClientCore.OrganizationKey]); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Fact] + public void OpenAIClientConstructorWorksCorrectly() + { + // Arrange + var sut = new AzureOpenAITextToImageServiceTests("model", new OpenAIClient("apikey")); + + // Assert + Assert.NotNull(sut); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + } + + [Theory] + [InlineData(256, 256, "dall-e-2")] + [InlineData(512, 512, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-2")] + [InlineData(1024, 1024, "dall-e-3")] + [InlineData(1024, 1792, "dall-e-3")] + [InlineData(1792, 1024, "dall-e-3")] + [InlineData(123, 321, "custom-model-1")] + [InlineData(179, 124, "custom-model-2")] + public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) + { + // Arrange + var sut = new AzureOpenAITextToImageServiceTests(modelId, "api-key", httpClient: this._httpClient); + Assert.Equal(modelId, sut.Attributes["ModelId"]); + + // Act + var result = await sut.GenerateImageAsync("description", width, height); + + // Assert + Assert.Equal("https://image-url/", result); + } + + [Fact] + public async Task GenerateImageDoesLogActionAsync() + { + // Assert + var modelId = "dall-e-2"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new AzureOpenAITextToImageServiceTests(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GenerateImageAsync("description", 256, 256); + + // Assert + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(AzureOpenAITextToImageServiceTests.GenerateImageAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..720cd1cf71f5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,10 +21,20 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs new file mode 100644 index 000000000000..b6490a058fb9 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Images; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Prompt to generate the image + /// Width of the image + /// Height of the image + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task GenerateImageAsync( + string prompt, + int width, + int height, + CancellationToken cancellationToken) + { + Verify.NotNullOrWhiteSpace(prompt); + + var size = new GeneratedImageSize(width, height); + + var imageOptions = new ImageGenerationOptions() + { + Size = size, + ResponseFormat = GeneratedImageFormat.Uri + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.ModelId).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + var generatedImage = response.Value; + + return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs new file mode 100644 index 000000000000..a48b3177ebee --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.TextToImage; +using OpenAI; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// OpenAI text to image service. +/// +[Experimental("SKEXP0010")] +public class AzureOpenAITextToImageService : ITextToImageService +{ + private readonly ClientCore _client; + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + /// Initializes a new instance of the class. + /// + /// The model to use for image generation. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Non-default endpoint for the OpenAI API. + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToImageService( + string modelId, + string? apiKey = null, + string? organizationId = null, + Uri? endpoint = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); + } + + /// + /// Initializes a new instance of the class. + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToImageService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + } + + /// + public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GenerateImageAsync(description, width, height, cancellationToken); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index a226d6c59040..cb37384845df 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -33,7 +33,7 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService public IReadOnlyDictionary Attributes => this._client.Attributes; /// - /// Creates an instance of the with API key auth. + /// Creates an instance of the with API key auth. /// /// Model name /// OpenAI API Key @@ -49,11 +49,11 @@ public OpenAIAudioToTextService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); } /// - /// Creates an instance of the with API key auth. + /// Creates an instance of the with API key auth. /// /// Model name /// Custom for HTTP requests. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 1a6038aa3f43..15ebcf049a93 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -64,7 +64,7 @@ public OpenAITextToImageService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { - this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToImageService))); } /// From 2abaec8682bb17dd8d11029f38b68d857770ffa5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 10:51:03 -0700 Subject: [PATCH 022/226] Checkpoint --- dotnet/samples/Concepts/Concepts.csproj | 13 +- .../GettingStartedWithAgents.csproj | 2 +- .../GettingStartedWithAgents/Step2_Plugins.cs | 4 +- .../Step6_DependencyInjection.cs | 6 +- .../Step8_OpenAIAssistant.cs | 4 +- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 3 +- .../OpenAI/Extensions/AuthorRoleExtensions.cs | 2 +- .../Extensions/KernelFunctionExtensions.cs | 2 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 192 ++++++++++-------- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 180 ++++++++-------- .../OpenAI/OpenAIAssistantConfiguration.cs | 8 +- .../OpenAI/OpenAIAssistantDefinition.cs | 4 +- .../Extensions/AuthorRoleExtensionsTests.cs | 2 +- .../KernelFunctionExtensionsTests.cs | 6 +- .../OpenAI/OpenAIAssistantAgentTests.cs | 21 +- .../OpenAIAssistantConfigurationTests.cs | 6 +- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 12 +- .../Agents/OpenAIAssistantAgentTests.cs | 2 +- .../samples/InternalUtilities/BaseTest.cs | 6 +- 19 files changed, 255 insertions(+), 220 deletions(-) diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 5f81653e6dff..dfcddf2920c5 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -46,8 +46,8 @@ - - + + @@ -103,4 +103,13 @@ Always + + + + + + + + + diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index ea4decbf86bb..b95bbd546d34 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -38,7 +38,7 @@ - + diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs index 708fab321f04..3ce8ade066e4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs @@ -3,7 +3,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; namespace GettingStarted; @@ -26,7 +26,7 @@ public async Task RunAsync() Instructions = HostInstructions, Name = HostName, Kernel = this.CreateKernelWithChatCompletion(), - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }, }; // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs index c759053dbe1c..6524f3ee39b2 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs @@ -38,9 +38,9 @@ public async Task RunAsync() if (this.UseOpenAIConfig) { - serviceContainer.AddOpenAIChatCompletion( - TestConfiguration.OpenAI.ChatModelId, - TestConfiguration.OpenAI.ApiKey); + //serviceContainer.AddOpenAIChatCompletion( %%% + // TestConfiguration.OpenAI.ChatModelId, + // TestConfiguration.OpenAI.ApiKey); } else { diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 32ce38da8b2f..f9e1b601e4c5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -14,6 +14,8 @@ namespace GettingStarted; /// public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) { + protected override bool ForceOpenAI => true; + private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; @@ -29,7 +31,7 @@ await OpenAIAssistantAgent.CreateAsync( { Instructions = HostInstructions, Name = HostName, - ModelId = this.Model, + Model = this.Model, }); // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 222ea5c5be88..628fdf2aa171 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -19,6 +19,7 @@ + @@ -32,7 +33,7 @@ - + diff --git a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs index cd4e80c3abf1..895482927515 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs index 9665fb680498..63eec124f0ae 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI.Assistants; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index ca016a5d97cb..78ff3bc470d9 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,17 +1,17 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; +using System.Net.Http; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Azure; -using Azure.AI.OpenAI.Assistants; -using Azure.Core; -using Azure.Core.Pipeline; +using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.OpenAI.Azure; using Microsoft.SemanticKernel.Http; +using OpenAI; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -21,13 +21,13 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; public sealed partial class OpenAIAssistantAgent : KernelAgent { private readonly Assistant _assistant; - private readonly AssistantsClient _client; + private readonly AssistantClient _client; private readonly OpenAIAssistantConfiguration _config; - /// - /// A list of previously uploaded file IDs to attach to the assistant. - /// - public IReadOnlyList FileIds => this._assistant.FileIds; + ///// + ///// A list of previously uploaded file IDs to attach to the assistant. + ///// + //public IReadOnlyList FileIds => this._assistant.FileIds; %%% /// /// A set of up to 16 key/value pairs that can be attached to an agent, used for @@ -67,11 +67,11 @@ public static async Task CreateAsync( Verify.NotNull(definition, nameof(definition)); // Create the client - AssistantsClient client = CreateClient(config); + AssistantClient client = CreateClient(config); // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); - Assistant model = await client.CreateAssistantAsync(assistantCreationOptions, cancellationToken).ConfigureAwait(false); + Assistant model = await client.CreateAssistantAsync(definition.Model, assistantCreationOptions, cancellationToken).ConfigureAwait(false); // Instantiate the agent return @@ -85,53 +85,31 @@ public static async Task CreateAsync( /// Retrieve a list of assistant definitions: . /// /// Configuration for accessing the Assistants API service, such as the api-key. - /// The maximum number of assistant definitions to retrieve - /// The identifier of the assistant beyond which to begin selection. /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( OpenAIAssistantConfiguration config, - int maxResults = 100, - string? lastId = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create the client - AssistantsClient client = CreateClient(config); - - // Retrieve the assistants - PageableList assistants; + AssistantClient client = CreateClient(config); - int resultCount = 0; - do + await foreach (Assistant assistant in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) { - assistants = await client.GetAssistantsAsync(limit: Math.Min(maxResults, 100), ListSortOrder.Descending, after: lastId, cancellationToken: cancellationToken).ConfigureAwait(false); - foreach (Assistant assistant in assistants) - { - if (resultCount >= maxResults) + yield return + new() { - break; - } - - resultCount++; - - yield return - new() - { - Id = assistant.Id, - Name = assistant.Name, - Description = assistant.Description, - Instructions = assistant.Instructions, - EnableCodeInterpreter = assistant.Tools.Any(t => t is CodeInterpreterToolDefinition), - EnableRetrieval = assistant.Tools.Any(t => t is RetrievalToolDefinition), - FileIds = assistant.FileIds, - Metadata = assistant.Metadata, - ModelId = assistant.Model, - }; - - lastId = assistant.Id; - } + Id = assistant.Id, + Name = assistant.Name, + Description = assistant.Description, + Instructions = assistant.Instructions, + EnableCodeInterpreter = assistant.Tools.Any(t => t is CodeInterpreterToolDefinition), + EnableFileSearch = assistant.Tools.Any(t => t is FileSearchToolDefinition), + //FileIds = assistant.FileIds, %%% + Metadata = assistant.Metadata, + Model = assistant.Model, + }; } - while (assistants.HasMore && resultCount < maxResults); } /// @@ -149,10 +127,10 @@ public static async Task RetrieveAsync( CancellationToken cancellationToken = default) { // Create the client - AssistantsClient client = CreateClient(config); + AssistantClient client = CreateClient(config); // Retrieve the assistant - Assistant model = await client.GetAssistantAsync(id, cancellationToken).ConfigureAwait(false); + Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // %%% CANCEL TOKEN // Instantiate the agent return @@ -182,12 +160,6 @@ protected override IEnumerable GetChannelKeys() // Distinguish between different Azure OpenAI endpoints or OpenAI services. yield return this._config.Endpoint ?? "openai"; - // Distinguish between different API versioning. - if (this._config.Version.HasValue) - { - yield return this._config.Version.ToString()!; - } - // Custom client receives dedicated channel. if (this._config.HttpClient is not null) { @@ -208,7 +180,7 @@ protected override async Task CreateChannelAsync(ILogger logger, C { logger.LogDebug("[{MethodName}] Creating assistant thread", nameof(CreateChannelAsync)); - AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + AssistantThread thread = await this._client.CreateThreadAsync(options: null, cancellationToken).ConfigureAwait(false); logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); @@ -219,7 +191,7 @@ protected override async Task CreateChannelAsync(ILogger logger, C /// Initializes a new instance of the class. /// private OpenAIAssistantAgent( - AssistantsClient client, + AssistantClient client, Assistant model, OpenAIAssistantConfiguration config) { @@ -233,64 +205,124 @@ private OpenAIAssistantAgent( this.Instructions = this._assistant.Instructions; } + private static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient) + { + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + }; + + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + } + + return options; + } + private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { AssistantCreationOptions assistantCreationOptions = - new(definition.ModelId) + new() { Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - Metadata = definition.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + // %%% ResponseFormat = + // %%% Temperature = + // %%% ToolResources + //assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []); %%% + // %%% NucleusSamplingFactor }; - assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []); + // %%% COPY METADATA + // Metadata = definition.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), if (definition.EnableCodeInterpreter) { assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); } - if (definition.EnableRetrieval) + if (definition.EnableFileSearch) { - assistantCreationOptions.Tools.Add(new RetrievalToolDefinition()); + assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); } return assistantCreationOptions; } - private static AssistantsClient CreateClient(OpenAIAssistantConfiguration config) + private static AssistantClient CreateClient(OpenAIAssistantConfiguration config) { - AssistantsClientOptions clientOptions = CreateClientOptions(config); + OpenAIClient client; // Inspect options - if (!string.IsNullOrWhiteSpace(config.Endpoint)) + if (!string.IsNullOrWhiteSpace(config.Endpoint)) // %%% INSUFFICENT (BOTH HAVE ENDPOINT OPTION) { // Create client configured for Azure OpenAI, if endpoint definition is present. - return new AssistantsClient(new Uri(config.Endpoint), new AzureKeyCredential(config.ApiKey), clientOptions); + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config.HttpClient); + client = new AzureOpenAIClient(new Uri(config.Endpoint), config.ApiKey, clientOptions); + } + else + { + // Otherwise, create client configured for OpenAI. + OpenAIClientOptions clientOptions = CreateClientOptions(config.HttpClient, config.Endpoint); + client = new OpenAIClient(config.ApiKey, clientOptions); + } + + return client.GetAssistantClient(); + } + + internal static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient) + { + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + }; + + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout } - // Otherwise, create client configured for OpenAI. - return new AssistantsClient(config.ApiKey, clientOptions); + return options; } - private static AssistantsClientOptions CreateClientOptions(OpenAIAssistantConfiguration config) + private static OpenAIClientOptions CreateClientOptions(HttpClient? httpClient, string? endpoint) { - AssistantsClientOptions options = - config.Version.HasValue ? - new(config.Version.Value) : - new(); + OpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = string.IsNullOrEmpty(endpoint) ? null : new Uri(endpoint) + }; - options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), HttpPipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); - if (config.HttpClient is not null) + if (httpClient is not null) { - options.Transport = new HttpClientTransport(config.HttpClient); - options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout } return options; } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + { + return new GenericActionPipelinePolicy((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 0d8b20b5b931..1c037ecab08c 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,16 +8,17 @@ using System.Threading; using System.Threading.Tasks; using Azure; -using Azure.AI.OpenAI.Assistants; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// A specialization for use with . /// -internal sealed class OpenAIAssistantChannel(AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) +internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) : AgentChannel { private const string FunctionDelimiter = "-"; @@ -35,7 +37,7 @@ internal sealed class OpenAIAssistantChannel(AssistantsClient client, string thr RunStatus.Cancelled, ]; - private readonly AssistantsClient _client = client; + private readonly AssistantClient _client = client; private readonly string _threadId = threadId; private readonly Dictionary _agentTools = []; private readonly Dictionary _agentNames = []; // Cache agent names by their identifier for GetHistoryAsync() @@ -50,11 +52,17 @@ protected override async Task ReceiveAsync(IReadOnlyList his continue; } + MessageCreationOptions options = + new() + { + //Role = message.Role.ToMessageRole(), // %%% BUG: ASSIGNABLE + }; + await this._client.CreateMessageAsync( this._threadId, - message.Role.ToMessageRole(), - message.Content, - cancellationToken: cancellationToken).ConfigureAwait(false); + [message.Content], // %%% + options, + cancellationToken).ConfigureAwait(false); } } @@ -81,15 +89,17 @@ protected override async IAsyncEnumerable InvokeAsync( this.Logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, this._threadId); - CreateRunOptions options = - new(agent.Id) + RunCreationOptions options = + new() { - OverrideInstructions = agent.Instructions, - OverrideTools = tools, + //InstructionsOverride = agent.Instructions, + //ParallelToolCallsEnabled = true, // %%% + //ResponseFormat = %%% + //ToolsOverride = tools, %%% }; // Create run - ThreadRun run = await this._client.CreateRunAsync(this._threadId, options, cancellationToken).ConfigureAwait(false); + ThreadRun run = await this._client.CreateRunAsync(this._threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); this.Logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); @@ -100,7 +110,7 @@ protected override async IAsyncEnumerable InvokeAsync( do { // Poll run and steps until actionable - PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); + await PollRunStatusAsync().ConfigureAwait(false); // Is in terminal state? if (s_terminalStatuses.Contains(run.Status)) @@ -108,13 +118,15 @@ protected override async IAsyncEnumerable InvokeAsync( throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } + RunStep[] steps = await this._client.GetRunStepsAsync(run).ToArrayAsync(cancellationToken).ConfigureAwait(false); + // Is tool action required? if (run.Status == RunStatus.RequiresAction) { this.Logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); // Execute functions in parallel and post results at once. - FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + FunctionCallContent[] activeFunctionSteps = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); if (activeFunctionSteps.Length > 0) { // Emit function-call content @@ -129,7 +141,7 @@ protected override async IAsyncEnumerable InvokeAsync( // Process tool output ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - await this._client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); + await this._client.SubmitToolOutputsToRunAsync(run, toolOutputs).ConfigureAwait(false); // %%% CANCEL TOKEN } if (this.Logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled @@ -149,24 +161,22 @@ protected override async IAsyncEnumerable InvokeAsync( int messageCount = 0; foreach (RunStep completedStep in completedStepsToProcess) { - if (completedStep.Type.Equals(RunStepType.ToolCalls)) + if (completedStep.Type == RunStepType.ToolCalls) { - RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; - - foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) + foreach (RunStepToolCall toolCall in completedStep.Details.ToolCalls) { ChatMessageContent? content = null; // Process code-interpreter content - if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) + if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter) { - content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput); } // Process function result content - else if (toolCall is RunStepFunctionToolCall toolFunction) + else if (toolCall.ToolKind == RunStepToolCallKind.Function) { - FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation - content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); + FunctionCallContent functionStep = functionSteps[toolCall.ToolCallId]; // Function step always captured on invocation + content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolCall.FunctionOutput); } if (content is not null) @@ -177,30 +187,28 @@ protected override async IAsyncEnumerable InvokeAsync( } } } - else if (completedStep.Type.Equals(RunStepType.MessageCreation)) + else if (completedStep.Type == RunStepType.MessageCreation) { - RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; - // Retrieve the message - ThreadMessage? message = await this.RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); + ThreadMessage? message = await this.RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false); if (message is not null) { AuthorRole role = new(message.Role.ToString()); - foreach (MessageContent itemContent in message.ContentItems) + foreach (MessageContent itemContent in message.Content) { ChatMessageContent? content = null; // Process text content - if (itemContent is MessageTextContent contentMessage) + if (!string.IsNullOrEmpty(itemContent.Text)) { - content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); + content = GenerateTextMessageContent(agent.GetName(), role, itemContent); } // Process image content - else if (itemContent is MessageImageFileContent contentImage) + else if (itemContent.ImageFileId != null) { - content = GenerateImageFileContent(agent.GetName(), role, contentImage); + content = GenerateImageFileContent(agent.GetName(), role, itemContent); } if (content is not null) @@ -226,7 +234,7 @@ protected override async IAsyncEnumerable InvokeAsync( this.Logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); // Local function to assist in run polling (participates in method closure). - async Task> PollRunStatusAsync() + async Task PollRunStatusAsync() { this.Logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); @@ -252,32 +260,30 @@ async Task> PollRunStatusAsync() while (s_pollingStatuses.Contains(run.Status)); this.Logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); - - return await this._client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); } // Local function to capture kernel function state for further processing (participates in method closure). IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) { - if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) + if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls) { - foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) + foreach (RunStepToolCall toolCall in step.Details.ToolCalls) { - var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); + var nameParts = FunctionName.Parse(toolCall.FunctionName, FunctionDelimiter); KernelArguments functionArguments = []; - if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) + if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments)) { - Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; + Dictionary arguments = JsonSerializer.Deserialize>(toolCall.FunctionArguments)!; foreach (var argumentKvp in arguments) { functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); } } - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); + var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - functionSteps.Add(toolCall.Id, content); + functionSteps.Add(toolCall.ToolCallId, content); yield return content; } @@ -288,90 +294,82 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R /// protected override async IAsyncEnumerable GetHistoryAsync([EnumeratorCancellation] CancellationToken cancellationToken) { - PageableList messages; - - string? lastId = null; - do + await foreach (ThreadMessage message in this._client.GetMessagesAsync(this._threadId, ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) { - messages = await this._client.GetMessagesAsync(this._threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false); - foreach (ThreadMessage message in messages) - { - AuthorRole role = new(message.Role.ToString()); + AuthorRole role = new(message.Role.ToString()); - string? assistantName = null; - if (!string.IsNullOrWhiteSpace(message.AssistantId) && - !this._agentNames.TryGetValue(message.AssistantId, out assistantName)) + string? assistantName = null; + if (!string.IsNullOrWhiteSpace(message.AssistantId) && + !this._agentNames.TryGetValue(message.AssistantId, out assistantName)) + { + Assistant assistant = await this._client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // %%% CANCEL TOKEN + if (!string.IsNullOrWhiteSpace(assistant.Name)) { - Assistant assistant = await this._client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(assistant.Name)) - { - this._agentNames.Add(assistant.Id, assistant.Name); - } + this._agentNames.Add(assistant.Id, assistant.Name); } + } - assistantName ??= message.AssistantId; + assistantName ??= message.AssistantId; - foreach (MessageContent item in message.ContentItems) - { - ChatMessageContent? content = null; - - if (item is MessageTextContent contentMessage) - { - content = GenerateTextMessageContent(assistantName, role, contentMessage); - } - else if (item is MessageImageFileContent contentImage) - { - content = GenerateImageFileContent(assistantName, role, contentImage); - } + foreach (MessageContent itemContent in message.Content) + { + ChatMessageContent? content = null; - if (content is not null) - { - yield return content; - } + if (!string.IsNullOrEmpty(itemContent.Text)) + { + content = GenerateTextMessageContent(assistantName, role, itemContent); + } + // Process image content + else if (itemContent.ImageFileId != null) + { + content = GenerateImageFileContent(assistantName, role, itemContent); } - lastId = message.Id; + if (content is not null) + { + yield return content; + } } } - while (messages.HasMore); } - private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) + private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation) { string? fileId = null; - if (annotation is MessageTextFileCitationAnnotation citationAnnotation) + + if (string.IsNullOrEmpty(annotation.OutputFileId)) { - fileId = citationAnnotation.FileId; + fileId = annotation.OutputFileId; } - else if (annotation is MessageTextFilePathAnnotation pathAnnotation) + else if (string.IsNullOrEmpty(annotation.InputFileId)) { - fileId = pathAnnotation.FileId; + fileId = annotation.InputFileId; } return new() { - Quote = annotation.Text, + Quote = annotation.TextToReplace, StartIndex = annotation.StartIndex, EndIndex = annotation.EndIndex, FileId = fileId, }; } - private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageImageFileContent contentImage) + private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageContent contentImage) { return new ChatMessageContent( role, [ - new FileReferenceContent(contentImage.FileId) + new FileReferenceContent(contentImage.ImageFileId) ]) { AuthorName = agentName, }; } - private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageTextContent contentMessage) + private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageContent contentMessage) { ChatMessageContent? messageContent = null; @@ -385,7 +383,7 @@ private static ChatMessageContent GenerateImageFileContent(string agentName, Aut AuthorName = agentName }; - foreach (MessageTextAnnotation annotation in contentMessage.Annotations) + foreach (TextAnnotation annotation in contentMessage.TextAnnotations) { messageContent.Items.Add(GenerateAnnotationContent(annotation)); } @@ -394,13 +392,13 @@ private static ChatMessageContent GenerateImageFileContent(string agentName, Aut return messageContent; } - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter) + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string code) { return new ChatMessageContent( AuthorRole.Tool, [ - new TextContent(contentCodeInterpreter.Input) + new TextContent(code) ]) { AuthorName = agentName, @@ -469,7 +467,7 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } - private async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) + private async Task RetrieveMessageAsync(string messageId, CancellationToken cancellationToken) { ThreadMessage? message = null; @@ -479,7 +477,7 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function { try { - message = await this._client.GetMessageAsync(this._threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); + message = await this._client.GetMessageAsync(this._threadId, messageId, cancellationToken).ConfigureAwait(false); } catch (RequestFailedException exception) { diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs index aa037266e7d5..acc094b86969 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.Net.Http; -using Azure.AI.OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -20,11 +19,6 @@ public sealed class OpenAIAssistantConfiguration /// public string? Endpoint { get; } - /// - /// An optional API version override. - /// - public AssistantsClientOptions.ServiceVersion? Version { get; init; } - /// /// Custom for HTTP requests. /// @@ -33,7 +27,7 @@ public sealed class OpenAIAssistantConfiguration /// /// Defineds polling behavior for Assistant API requests. /// - public PollingConfiguration Polling { get; } = new PollingConfiguration(); + public PollingConfiguration Polling { get; } = new(); /// /// Initializes a new instance of the class. diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 3699e07ee1ed..e3e8706abf47 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -11,7 +11,7 @@ public sealed class OpenAIAssistantDefinition /// /// Identifies the AI model (OpenAI) or deployment (AzureOAI) this agent targets. /// - public string? ModelId { get; init; } + public string? Model { get; init; } /// /// The description of the assistant. @@ -41,7 +41,7 @@ public sealed class OpenAIAssistantDefinition /// /// Set if retrieval is enabled. /// - public bool EnableRetrieval { get; init; } + public bool EnableFileSearch { get; init; } /// /// A list of previously uploaded file IDs to attach to the assistant. diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs index 0b0a0707e49a..997596796be1 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; using Xunit; using KernelExtensions = Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs index eeb8a4d3b9d1..2096012831ed 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.ComponentModel; -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI.Assistants; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions; @@ -26,11 +26,11 @@ public void VerifyKernelFunctionToFunctionTool() KernelFunction f2 = plugin[nameof(TestPlugin.TestFunction2)]; FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin", "-"); - Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.Name, StringComparison.Ordinal); + Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition1.Description); FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin", "-"); - Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.Name, StringComparison.Ordinal); + Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition2.Description); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 1d9a9ec9dfcf..45d9df826c7b 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -5,11 +5,11 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI; @@ -33,7 +33,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() OpenAIAssistantDefinition definition = new() { - ModelId = "testmodel", + Model = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); @@ -62,7 +62,7 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() OpenAIAssistantDefinition definition = new() { - ModelId = "testmodel", + Model = "testmodel", Name = "testname", Description = "testdescription", Instructions = "testinstructions", @@ -94,9 +94,9 @@ public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() OpenAIAssistantDefinition definition = new() { - ModelId = "testmodel", + Model = "testmodel", EnableCodeInterpreter = true, - EnableRetrieval = true, + EnableFileSearch = true, FileIds = ["#1", "#2"], Metadata = new Dictionary() { { "a", "1" } }, }; @@ -112,8 +112,8 @@ await OpenAIAssistantAgent.CreateAsync( Assert.NotNull(agent); Assert.Equal(2, agent.Tools.Count); Assert.True(agent.Tools.OfType().Any()); - Assert.True(agent.Tools.OfType().Any()); - Assert.NotEmpty(agent.FileIds); + //Assert.True(agent.Tools.OfType().Any()); %%% + //Assert.NotEmpty(agent.FileIds); %%% Assert.NotEmpty(agent.Metadata); } @@ -314,8 +314,7 @@ await OpenAIAssistantAgent.ListDefinitionsAsync( messages = await OpenAIAssistantAgent.ListDefinitionsAsync( - this.CreateTestConfiguration(), - maxResults: 4).ToArrayAsync(); + this.CreateTestConfiguration()).ToArrayAsync(); Assert.Equal(4, messages.Length); } @@ -370,7 +369,7 @@ private Task CreateAgentAsync() OpenAIAssistantDefinition definition = new() { - ModelId = "testmodel", + Model = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); @@ -387,7 +386,7 @@ private OpenAIAssistantConfiguration CreateTestConfiguration(bool targetAzure = return new(apiKey: "fakekey", endpoint: targetAzure ? "https://localhost" : null) { HttpClient = this._httpClient, - Version = useVersion ? AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview : null, + //Version = useVersion ? AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview : null, %%% }; } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs index 3708ab50ab97..d2849f5d19fa 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs @@ -23,7 +23,7 @@ public void VerifyOpenAIAssistantConfigurationInitialState() Assert.Equal("testkey", config.ApiKey); Assert.Null(config.Endpoint); Assert.Null(config.HttpClient); - Assert.Null(config.Version); + //Assert.Null(config.Version); %%% } /// @@ -38,13 +38,13 @@ public void VerifyOpenAIAssistantConfigurationAssignment() new(apiKey: "testkey", endpoint: "https://localhost") { HttpClient = client, - Version = AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, + //Version = AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, %%% }; Assert.Equal("testkey", config.ApiKey); Assert.Equal("https://localhost", config.Endpoint); Assert.NotNull(config.HttpClient); - Assert.Equal(AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, config.Version); + //Assert.Equal(AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, config.Version); %%% } /// diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index b17b61211c18..48977edc122a 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -20,13 +20,13 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Id); Assert.Null(definition.Name); - Assert.Null(definition.ModelId); + Assert.Null(definition.Model); Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); Assert.Null(definition.FileIds); Assert.False(definition.EnableCodeInterpreter); - Assert.False(definition.EnableRetrieval); + Assert.False(definition.EnableFileSearch); } /// @@ -40,23 +40,23 @@ public void VerifyOpenAIAssistantDefinitionAssignment() { Id = "testid", Name = "testname", - ModelId = "testmodel", + Model = "testmodel", Instructions = "testinstructions", Description = "testdescription", FileIds = ["id"], Metadata = new Dictionary() { { "a", "1" } }, EnableCodeInterpreter = true, - EnableRetrieval = true, + EnableFileSearch = true, }; Assert.Equal("testid", definition.Id); Assert.Equal("testname", definition.Name); - Assert.Equal("testmodel", definition.ModelId); + Assert.Equal("testmodel", definition.Model); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); Assert.Single(definition.Metadata); Assert.Single(definition.FileIds); Assert.True(definition.EnableCodeInterpreter); - Assert.True(definition.EnableRetrieval); + Assert.True(definition.EnableFileSearch); } } diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index 20d6dcad9146..d274ac7706d7 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -85,7 +85,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { Instructions = "Answer questions about the menu.", - ModelId = modelName, + Model = modelName, }); AgentGroupChat chat = new(); diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 8e65d7dcd88a..b9a76dd1b117 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -42,9 +42,9 @@ protected Kernel CreateKernelWithChatCompletion() if (this.UseOpenAIConfig) { - builder.AddOpenAIChatCompletion( - TestConfiguration.OpenAI.ChatModelId, - TestConfiguration.OpenAI.ApiKey); + //builder.AddOpenAIChatCompletion( %%% + // TestConfiguration.OpenAI.ChatModelId, + // TestConfiguration.OpenAI.ApiKey); } else { From 43d7ecbda1082ffa9c9640db86d41d06cde1d29d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 11:30:23 -0700 Subject: [PATCH 023/226] Add connector unit tests: Qdrant, Redis --- dotnet/SK-dotnet.sln | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 7b2e556f616d..861cb4f49a96 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -342,6 +342,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", src\InternalUtilities\openai\Extensions\ClientResultExceptionExtensions.cs = src\InternalUtilities\openai\Extensions\ClientResultExceptionExtensions.cs EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests", "src\Connectors\Connectors.Qdrant.UnitTests\Connectors.Qdrant.UnitTests.csproj", "{8642A03F-D840-4B2E-B092-478300000F83}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -835,6 +839,18 @@ Global {DB219924-208B-4CDD-8796-EE424689901E}.Publish|Any CPU.Build.0 = Debug|Any CPU {DB219924-208B-4CDD-8796-EE424689901E}.Release|Any CPU.ActiveCfg = Release|Any CPU {DB219924-208B-4CDD-8796-EE424689901E}.Release|Any CPU.Build.0 = Release|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Publish|Any CPU.Build.0 = Debug|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8642A03F-D840-4B2E-B092-478300000F83}.Release|Any CPU.Build.0 = Release|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.Build.0 = Debug|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -950,6 +966,8 @@ Global {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} {7308EF7D-5F9A-47B2-A62F-0898603262A8} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} + {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} + {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} From 08a426e12ee991efcd4c6728c77eaa4107197f98 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 11:55:16 -0700 Subject: [PATCH 024/226] Fix merge from parent --- .../Agents/OpenAI/AssistantThreadActions.cs | 182 +++++++++--------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 3 +- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 6 +- 3 files changed, 95 insertions(+), 96 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index 37649844a230..b5376f52d81d 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Collections.Generic; using System.Linq; using System.Net; @@ -7,9 +8,10 @@ using System.Threading; using System.Threading.Tasks; using Azure; -using Azure.AI.OpenAI.Assistants; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -49,7 +51,7 @@ internal static class AssistantThreadActions /// The message to add /// The to monitor for cancellation requests. The default is . /// if a system message is present, without taking any other action - public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) + public static async Task CreateMessageAsync(AssistantClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { if (!s_messageRoles.Contains(message.Role)) { @@ -61,11 +63,17 @@ public static async Task CreateMessageAsync(AssistantsClient client, string thre return; } + MessageCreationOptions options = + new() + { + //Role = message.Role.ToMessageRole(), // %%% BUG: ASSIGNABLE + }; + await client.CreateMessageAsync( threadId, - message.Role.ToMessageRole(), - message.Content, - cancellationToken: cancellationToken).ConfigureAwait(false); + [message.Content], // %%% + options, + cancellationToken).ConfigureAwait(false); } /// @@ -75,56 +83,47 @@ await client.CreateMessageAsync( /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public static async IAsyncEnumerable GetMessagesAsync(AssistantsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) + public static async IAsyncEnumerable GetMessagesAsync(AssistantClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) { Dictionary agentNames = []; // Cache agent names by their identifier - PageableList messages; - - string? lastId = null; - do + await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) { - messages = await client.GetMessagesAsync(threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false); - foreach (ThreadMessage message in messages) - { - AuthorRole role = new(message.Role.ToString()); + AuthorRole role = new(message.Role.ToString()); - string? assistantName = null; - if (!string.IsNullOrWhiteSpace(message.AssistantId) && - !agentNames.TryGetValue(message.AssistantId, out assistantName)) + string? assistantName = null; + if (!string.IsNullOrWhiteSpace(message.AssistantId) && + !agentNames.TryGetValue(message.AssistantId, out assistantName)) + { + Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // %%% CANCEL TOKEN + if (!string.IsNullOrWhiteSpace(assistant.Name)) { - Assistant assistant = await client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(assistant.Name)) - { - agentNames.Add(assistant.Id, assistant.Name); - } + agentNames.Add(assistant.Id, assistant.Name); } + } - assistantName ??= message.AssistantId; - - foreach (MessageContent item in message.ContentItems) - { - ChatMessageContent? content = null; + assistantName ??= message.AssistantId; - if (item is MessageTextContent contentMessage) - { - content = GenerateTextMessageContent(assistantName, role, contentMessage); - } - else if (item is MessageImageFileContent contentImage) - { - content = GenerateImageFileContent(assistantName, role, contentImage); - } + foreach (MessageContent itemContent in message.Content) + { + ChatMessageContent? content = null; - if (content is not null) - { - yield return content; - } + if (!string.IsNullOrEmpty(itemContent.Text)) + { + content = GenerateTextMessageContent(assistantName, role, itemContent); + } + // Process image content + else if (itemContent.ImageFileId != null) + { + content = GenerateImageFileContent(assistantName, role, itemContent); } - lastId = message.Id; + if (content is not null) + { + yield return content; + } } } - while (messages.HasMore); } /// @@ -139,7 +138,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// Asynchronous enumeration of messages. public static async IAsyncEnumerable InvokeAsync( OpenAIAssistantAgent agent, - AssistantsClient client, + AssistantClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration, ILogger logger, @@ -154,15 +153,17 @@ public static async IAsyncEnumerable InvokeAsync( logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); - CreateRunOptions options = - new(agent.Id) + RunCreationOptions options = + new() { - OverrideInstructions = agent.Instructions, - OverrideTools = tools, + //InstructionsOverride = agent.Instructions, + //ParallelToolCallsEnabled = true, // %%% + //ResponseFormat = %%% + //ToolsOverride = tools, %%% }; // Create run - ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false); + ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); @@ -173,7 +174,7 @@ public static async IAsyncEnumerable InvokeAsync( do { // Poll run and steps until actionable - PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); + await PollRunStatusAsync().ConfigureAwait(false); // Is in terminal state? if (s_terminalStatuses.Contains(run.Status)) @@ -181,13 +182,15 @@ public static async IAsyncEnumerable InvokeAsync( throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } + RunStep[] steps = await client.GetRunStepsAsync(run).ToArrayAsync(cancellationToken).ConfigureAwait(false); + // Is tool action required? if (run.Status == RunStatus.RequiresAction) { logger.LogDebug("[{MethodName}] Processing run steps: {RunId}", nameof(InvokeAsync), run.Id); // Execute functions in parallel and post results at once. - FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + FunctionCallContent[] activeFunctionSteps = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); if (activeFunctionSteps.Length > 0) { // Emit function-call content @@ -202,7 +205,7 @@ public static async IAsyncEnumerable InvokeAsync( // Process tool output ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); + await client.SubmitToolOutputsToRunAsync(run, toolOutputs).ConfigureAwait(false); // %%% CANCEL TOKEN } if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled @@ -222,24 +225,22 @@ public static async IAsyncEnumerable InvokeAsync( int messageCount = 0; foreach (RunStep completedStep in completedStepsToProcess) { - if (completedStep.Type.Equals(RunStepType.ToolCalls)) + if (completedStep.Type == RunStepType.ToolCalls) { - RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; - - foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) + foreach (RunStepToolCall toolCall in completedStep.Details.ToolCalls) { ChatMessageContent? content = null; // Process code-interpreter content - if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) + if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter) { - content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput); } // Process function result content - else if (toolCall is RunStepFunctionToolCall toolFunction) + else if (toolCall.ToolKind == RunStepToolCallKind.Function) { - FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation - content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); + FunctionCallContent functionStep = functionSteps[toolCall.ToolCallId]; // Function step always captured on invocation + content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolCall.FunctionOutput); } if (content is not null) @@ -250,30 +251,28 @@ public static async IAsyncEnumerable InvokeAsync( } } } - else if (completedStep.Type.Equals(RunStepType.MessageCreation)) + else if (completedStep.Type == RunStepType.MessageCreation) { - RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; - // Retrieve the message - ThreadMessage? message = await RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); + ThreadMessage? message = await RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false); if (message is not null) { AuthorRole role = new(message.Role.ToString()); - foreach (MessageContent itemContent in message.ContentItems) + foreach (MessageContent itemContent in message.Content) { ChatMessageContent? content = null; // Process text content - if (itemContent is MessageTextContent contentMessage) + if (!string.IsNullOrEmpty(itemContent.Text)) { - content = GenerateTextMessageContent(agent.GetName(), role, contentMessage); + content = GenerateTextMessageContent(agent.GetName(), role, itemContent); } // Process image content - else if (itemContent is MessageImageFileContent contentImage) + else if (itemContent.ImageFileId != null) { - content = GenerateImageFileContent(agent.GetName(), role, contentImage); + content = GenerateImageFileContent(agent.GetName(), role, itemContent); } if (content is not null) @@ -299,7 +298,7 @@ public static async IAsyncEnumerable InvokeAsync( logger.LogInformation("[{MethodName}] Completed run: {RunId}", nameof(InvokeAsync), run.Id); // Local function to assist in run polling (participates in method closure). - async Task> PollRunStatusAsync() + async Task PollRunStatusAsync() { logger.LogInformation("[{MethodName}] Polling run status: {RunId}", nameof(PollRunStatusAsync), run.Id); @@ -325,39 +324,37 @@ async Task> PollRunStatusAsync() while (s_pollingStatuses.Contains(run.Status)); logger.LogInformation("[{MethodName}] Run status is {RunStatus}: {RunId}", nameof(PollRunStatusAsync), run.Status, run.Id); - - return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); } // Local function to capture kernel function state for further processing (participates in method closure). IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) { - if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) + if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls) { - foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) + foreach (RunStepToolCall toolCall in step.Details.ToolCalls) { - var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); + var nameParts = FunctionName.Parse(toolCall.FunctionName, FunctionDelimiter); KernelArguments functionArguments = []; - if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) + if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments)) { - Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; + Dictionary arguments = JsonSerializer.Deserialize>(toolCall.FunctionArguments)!; foreach (var argumentKvp in arguments) { functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); } } - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); + var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - functionSteps.Add(toolCall.Id, content); + functionSteps.Add(toolCall.ToolCallId, content); yield return content; } } } - async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) + async Task RetrieveMessageAsync(string messageId, CancellationToken cancellationToken) { ThreadMessage? message = null; @@ -367,7 +364,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R { try { - message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); + message = await client.GetMessageAsync(threadId, messageId, cancellationToken).ConfigureAwait(false); } catch (RequestFailedException exception) { @@ -390,42 +387,43 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R } } - private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) + private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation) { string? fileId = null; - if (annotation is MessageTextFileCitationAnnotation citationAnnotation) + + if (string.IsNullOrEmpty(annotation.OutputFileId)) { - fileId = citationAnnotation.FileId; + fileId = annotation.OutputFileId; } - else if (annotation is MessageTextFilePathAnnotation pathAnnotation) + else if (string.IsNullOrEmpty(annotation.InputFileId)) { - fileId = pathAnnotation.FileId; + fileId = annotation.InputFileId; } return new() { - Quote = annotation.Text, + Quote = annotation.TextToReplace, StartIndex = annotation.StartIndex, EndIndex = annotation.EndIndex, FileId = fileId, }; } - private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageImageFileContent contentImage) + private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageContent contentImage) { return new ChatMessageContent( role, [ - new FileReferenceContent(contentImage.FileId) + new FileReferenceContent(contentImage.ImageFileId) ]) { AuthorName = agentName, }; } - private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageTextContent contentMessage) + private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageContent contentMessage) { ChatMessageContent? messageContent = null; @@ -439,7 +437,7 @@ private static ChatMessageContent GenerateImageFileContent(string agentName, Aut AuthorName = agentName }; - foreach (MessageTextAnnotation annotation in contentMessage.Annotations) + foreach (TextAnnotation annotation in contentMessage.TextAnnotations) { messageContent.Items.Add(GenerateAnnotationContent(annotation)); } @@ -448,13 +446,13 @@ private static ChatMessageContent GenerateImageFileContent(string agentName, Aut return messageContent; } - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter) + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string code) { return new ChatMessageContent( AuthorRole.Tool, [ - new TextContent(contentCodeInterpreter.Input) + new TextContent(code) ]) { AuthorName = agentName, diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 6df555f5b15e..35705dbafb82 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -147,7 +147,8 @@ public static async Task RetrieveAsync( /// The thread identifier public async Task CreateThreadAsync(CancellationToken cancellationToken = default) { - AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + ThreadCreationOptions options = new(); // %%% + AssistantThread thread = await this._client.CreateThreadAsync(options, cancellationToken).ConfigureAwait(false); return thread.Id; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index b84ef800ebd4..48faf44dab40 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -2,17 +2,17 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI.Assistants; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// A specialization for use with . /// -internal sealed class OpenAIAssistantChannel(AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) +internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) : AgentChannel { - private readonly AssistantsClient _client = client; + private readonly AssistantClient _client = client; private readonly string _threadId = threadId; /// From d7a31eb41cd5ca2b9858b0e147a8e0502eefaf22 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 11:57:46 -0700 Subject: [PATCH 025/226] Fix tool setup --- dotnet/src/Agents/OpenAI/AssistantThreadActions.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index b5376f52d81d..a2f134abd9a2 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -159,9 +159,14 @@ public static async IAsyncEnumerable InvokeAsync( //InstructionsOverride = agent.Instructions, //ParallelToolCallsEnabled = true, // %%% //ResponseFormat = %%% - //ToolsOverride = tools, %%% }; + foreach (ToolDefinition tool in tools) // %%% + { + options.ToolsOverride.Add(tool); + } + + // Create run ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); From efed79b1ee752c87cdae251384b28f89ec00e001 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 3 Jul 2024 12:34:30 -0700 Subject: [PATCH 026/226] Agent concepts and integration tests --- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 56 ++++++++ dotnet/samples/ConceptsV2/ConceptsV2.csproj | 12 +- .../Step8_OpenAIAssistant.cs | 2 +- .../Agents/OpenAI/AssistantThreadActions.cs | 1 - .../Agents/OpenAIAssistantAgentTests.cs | 120 ++++++++++++++++++ .../IntegrationTestsV2.csproj | 2 + .../src/IntegrationTestsV2/testsettings.json | 97 ++++++++++++++ .../samples/InternalUtilities/BaseTest.cs | 2 +- 8 files changed, 287 insertions(+), 5 deletions(-) create mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs create mode 100644 dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/testsettings.json diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs new file mode 100644 index 000000000000..cb110e8e0bad --- /dev/null +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Agents; + +/// +/// Demonstrate using code-interpreter on . +/// +public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) +{ + protected override bool ForceOpenAI => true; + + [Fact] + public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() + { + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: new(this.ApiKey, this.Endpoint), + new() + { + EnableCodeInterpreter = true, // Enable code-interpreter + Model = this.Model, + }); + + // Create a chat for agent interaction. + var chat = new AgentGroupChat(); + + // Respond to user input + try + { + await InvokeAgentAsync("Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); + } + finally + { + await agent.DeleteAsync(); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (var content in chat.InvokeAsync(agent)) + { + Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + } + } + } +} diff --git a/dotnet/samples/ConceptsV2/ConceptsV2.csproj b/dotnet/samples/ConceptsV2/ConceptsV2.csproj index a9fe41232166..e5185dd02bc1 100644 --- a/dotnet/samples/ConceptsV2/ConceptsV2.csproj +++ b/dotnet/samples/ConceptsV2/ConceptsV2.csproj @@ -1,4 +1,4 @@ - + Concepts @@ -41,12 +41,17 @@ + + + + + - + @@ -69,4 +74,7 @@ Always + + + diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index dac184ea2c9a..c0395c7ca26e 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -14,7 +14,7 @@ namespace GettingStarted; /// public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) { - protected override bool ForceOpenAI => true; + protected override bool ForceOpenAI => false; private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs index a2f134abd9a2..0d7153816b2f 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs @@ -166,7 +166,6 @@ public static async IAsyncEnumerable InvokeAsync( options.ToolsOverride.Add(tool); } - // Create run ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs new file mode 100644 index 000000000000..18628dbf66e1 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ComponentModel; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTests.Agents.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class OpenAIAssistantAgentTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + /// + /// Integration test for using function calling + /// and targeting Open AI services. + /// + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData("What is the special soup?", "Clam Chowder")] + public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) + { + var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + + await this.ExecuteAgentAsync( + new(openAIConfiguration.ApiKey), + openAIConfiguration.ModelId, + input, + expectedAnswerContains); + } + + /// + /// Integration test for using function calling + /// and targeting Azure OpenAI services. + /// + [Theory/*(Skip = "No supported endpoint configured.")*/] + [InlineData("What is the special soup?", "Clam Chowder")] + public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAnswerContains) + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + + await this.ExecuteAgentAsync( + new(azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.Endpoint), + azureOpenAIConfiguration.ChatDeploymentName!, + input, + expectedAnswerContains); + } + + private async Task ExecuteAgentAsync( + OpenAIAssistantConfiguration config, + string modelName, + string input, + string expected) + { + // Arrange + Kernel kernel = new(); + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel, + config, + new() + { + Instructions = "Answer questions about the menu.", + Model = modelName, + }); + + AgentGroupChat chat = new(); + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + // Act + StringBuilder builder = new(); + await foreach (var message in chat.InvokeAsync(agent)) + { + builder.Append(message.Content); + } + + // Assert + Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase); + } + + public sealed class MenuPlugin + { + [KernelFunction, Description("Provides a list of specials from the menu.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] + public string GetSpecials() + { + return @" +Special Soup: Clam Chowder +Special Salad: Cobb Salad +Special Drink: Chai Tea +"; + } + + [KernelFunction, Description("Provides the price of the requested menu item.")] + public string GetItemPrice( + [Description("The name of the menu item.")] + string menuItem) + { + return "$9.99"; + } + } +} diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj index 13bcc5ba0f44..fc22c692e42f 100644 --- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj +++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj @@ -42,6 +42,8 @@ + + diff --git a/dotnet/src/IntegrationTestsV2/testsettings.json b/dotnet/src/IntegrationTestsV2/testsettings.json new file mode 100644 index 000000000000..66df73f8b7a5 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/testsettings.json @@ -0,0 +1,97 @@ +{ + "OpenAI": { + "ServiceId": "gpt-3.5-turbo-instruct", + "ModelId": "gpt-3.5-turbo-instruct", + "ApiKey": "" + }, + "AzureOpenAI": { + "ServiceId": "azure-gpt-35-turbo-instruct", + "DeploymentName": "gpt-35-turbo-instruct", + "ChatDeploymentName": "gpt-4", + "Endpoint": "", + "ApiKey": "" + }, + "OpenAIEmbeddings": { + "ServiceId": "text-embedding-ada-002", + "ModelId": "text-embedding-ada-002", + "ApiKey": "" + }, + "AzureOpenAIEmbeddings": { + "ServiceId": "azure-text-embedding-ada-002", + "DeploymentName": "ada-002", + "Endpoint": "", + "ApiKey": "" + }, + "OpenAITextToAudio": { + "ServiceId": "tts-1", + "ModelId": "tts-1", + "ApiKey": "" + }, + "AzureOpenAITextToAudio": { + "ServiceId": "azure-tts", + "DeploymentName": "tts", + "Endpoint": "", + "ApiKey": "" + }, + "OpenAIAudioToText": { + "ServiceId": "whisper-1", + "ModelId": "whisper-1", + "ApiKey": "" + }, + "AzureOpenAIAudioToText": { + "ServiceId": "azure-whisper", + "DeploymentName": "whisper", + "Endpoint": "", + "ApiKey": "" + }, + "HuggingFace": { + "ApiKey": "" + }, + "GoogleAI": { + "EmbeddingModelId": "embedding-001", + "ApiKey": "", + "Gemini": { + "ModelId": "gemini-1.5-flash", + "VisionModelId": "gemini-1.5-flash" + } + }, + "VertexAI": { + "EmbeddingModelId": "textembedding-gecko@003", + "BearerKey": "", + "Location": "us-central1", + "ProjectId": "", + "Gemini": { + "ModelId": "gemini-1.5-flash", + "VisionModelId": "gemini-1.5-flash" + } + }, + "Bing": { + "ApiKey": "" + }, + "Postgres": { + "ConnectionString": "" + }, + "MongoDB": { + "ConnectionString": "", + "VectorSearchCollection": "dotnetMSKNearestTest.nearestSearch" + }, + "AzureCosmosDB": { + "ConnectionString": "" + }, + "SqlServer": { + "ConnectionString": "" + }, + "Planners": { + "AzureOpenAI": { + "ServiceId": "azure-gpt-35-turbo", + "DeploymentName": "gpt-35-turbo", + "Endpoint": "", + "ApiKey": "" + }, + "OpenAI": { + "ServiceId": "openai-gpt-4", + "ModelId": "gpt-4", + "ApiKey": "" + } + } +} \ No newline at end of file diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index b9a76dd1b117..671524865d33 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -42,7 +42,7 @@ protected Kernel CreateKernelWithChatCompletion() if (this.UseOpenAIConfig) { - //builder.AddOpenAIChatCompletion( %%% + //builder.AddOpenAIChatCompletion( // %%% // TestConfiguration.OpenAI.ChatModelId, // TestConfiguration.OpenAI.ApiKey); } From 48eb9c3f6a09547a0947dafa7323f2339c7695da Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 4 Jul 2024 10:53:06 +0100 Subject: [PATCH 027/226] .Net: Migrate AzureOpenAITextToImageService to Azure.AI.OpenAI v2 (#7093) ### Motivation, Context, and Description This PR migrates `AzureOpenAITextToImageService` to Azure.AI.OpenAI v2: 1. It updates the previously added `AzureOpenAITextToImageService` to use `AzureOpenAIClient`. 2. It replaces all constructors in the `AzureOpenAITextToImageService` with the relevant ones from the original `AzureOpenAITextToImageService`. 3. It adds the `serviceVersion` parameter to the `ClientCore.GetAzureOpenAIClientOptions` methods, allowing the specification of the service API version. 4. It updates XML documentation comments in a few classes to indicate their relevance to Azure OpenAI services. 5. It adds unit tests for the `AzureOpenAITextToImageService` class. --- .../Connectors.AzureOpenAI.UnitTests.csproj | 8 -- .../AzureOpenAITextToImageServiceTests.cs | 61 ++++------ .../TestData/text-to-image-response.txt | 9 ++ .../Connectors.AzureOpenAI.csproj | 10 -- .../Core/ClientCore.ChatCompletion.cs | 2 +- .../Core/ClientCore.Embeddings.cs | 2 +- .../Core/ClientCore.TextToImage.cs | 7 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 12 +- ...ureOpenAITextEmbeddingGenerationService.cs | 6 +- .../Services/AzureOpenAITextToImageService.cs | 108 +++++++++++++++--- 10 files changed, 136 insertions(+), 89 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-to-image-response.txt diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj index 056ac691dfa4..a0a695a6719c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Connectors.AzureOpenAI.UnitTests.csproj @@ -30,14 +30,6 @@ - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs index b0d44113febb..d384df3d627c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs @@ -3,17 +3,20 @@ using System; using System.IO; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Services; using Moq; -using OpenAI; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; /// -/// Unit tests for class. +/// Unit tests for class. /// public sealed class AzureOpenAITextToImageServiceTests : IDisposable { @@ -35,25 +38,21 @@ public AzureOpenAITextToImageServiceTests() } [Fact] - public void ConstructorWorksCorrectly() + public void ConstructorsAddRequiredMetadata() { - // Arrange & Act - var sut = new AzureOpenAITextToImageServiceTests("model", "api-key", "organization"); - - // Assert - Assert.NotNull(sut); - Assert.Equal("organization", sut.Attributes[ClientCore.OrganizationKey]); + // Case #1 + var sut = new AzureOpenAITextToImageService("deployment", "https://api-host/", "api-key", "model"); + Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); - } - [Fact] - public void OpenAIClientConstructorWorksCorrectly() - { - // Arrange - var sut = new AzureOpenAITextToImageServiceTests("model", new OpenAIClient("apikey")); + // Case #2 + sut = new AzureOpenAITextToImageService("deployment", "https://api-hostapi/", new Mock().Object, "model"); + Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); - // Assert - Assert.NotNull(sut); + // Case #3 + sut = new AzureOpenAITextToImageService("deployment", new AzureOpenAIClient(new Uri("https://api-host/"), "api-key"), "model"); + Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); } @@ -69,34 +68,20 @@ public void OpenAIClientConstructorWorksCorrectly() public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) { // Arrange - var sut = new AzureOpenAITextToImageServiceTests(modelId, "api-key", httpClient: this._httpClient); - Assert.Equal(modelId, sut.Attributes["ModelId"]); + var sut = new AzureOpenAITextToImageService("deployment", "https://api-host", "api-key", modelId, this._httpClient); // Act var result = await sut.GenerateImageAsync("description", width, height); // Assert Assert.Equal("https://image-url/", result); - } - [Fact] - public async Task GenerateImageDoesLogActionAsync() - { - // Assert - var modelId = "dall-e-2"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - // Arrange - var sut = new AzureOpenAITextToImageServiceTests(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GenerateImageAsync("description", 256, 256); - - // Assert - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(AzureOpenAITextToImageServiceTests.GenerateImageAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + var request = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent); // {"prompt":"description","model":"deployment","response_format":"url","size":"179x124"} + Assert.NotNull(request); + Assert.Equal("description", request["prompt"]?.ToString()); + Assert.Equal("deployment", request["model"]?.ToString()); + Assert.Equal("url", request["response_format"]?.ToString()); + Assert.Equal($"{width}x{height}", request["size"]?.ToString()); } public void Dispose() diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-to-image-response.txt b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-to-image-response.txt new file mode 100644 index 000000000000..1d6f2150b1d5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/TestData/text-to-image-response.txt @@ -0,0 +1,9 @@ +{ + "created": 1702575371, + "data": [ + { + "revised_prompt": "A photo capturing the diversity of the Earth's landscapes.", + "url": "https://image-url/" + } + ] +} \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 720cd1cf71f5..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,20 +21,10 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. - - - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index e118a4b440e9..14b8c6a38ae0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -23,7 +23,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. /// internal partial class ClientCore { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs index cc7f6ffdda04..6f1aaaf16efc 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. /// internal partial class ClientCore { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs index b6490a058fb9..46335d6289de 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs @@ -8,7 +8,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. /// internal partial class ClientCore { @@ -36,9 +36,8 @@ internal async Task GenerateImageAsync( ResponseFormat = GeneratedImageFormat.Uri }; - ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.ModelId).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); - var generatedImage = response.Value; + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.DeploymentOrModelName).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); - return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); + return response.Value.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index dc45fdaea59d..571d24d95c3b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -17,7 +17,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. /// internal partial class ClientCore { @@ -135,13 +135,13 @@ internal ClientCore( /// Gets options to use for an OpenAIClient /// Custom for HTTP requests. + /// Optional API version. /// An instance of . - internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient) + internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient, AzureOpenAIClientOptions.ServiceVersion? serviceVersion = null) { - AzureOpenAIClientOptions options = new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - }; + AzureOpenAIClientOptions options = serviceVersion is not null + ? new(serviceVersion.Value) { ApplicationId = HttpHeaderConstant.Values.UserAgent } + : new() { ApplicationId = HttpHeaderConstant.Values.UserAgent }; options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs index 103f1bbcf3ca..8908c9291220 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -79,18 +79,18 @@ public AzureOpenAITextEmbeddingGenerationService( /// Creates a new client. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. + /// Custom for HTTP requests. /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. public AzureOpenAITextEmbeddingGenerationService( string deploymentName, - AzureOpenAIClient openAIClient, + AzureOpenAIClient azureOpenAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs index a48b3177ebee..4b1ebe7aafa5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs @@ -6,14 +6,16 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextToImage; -using OpenAI; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// -/// OpenAI text to image service. +/// Azure OpenAI text to image service. /// [Experimental("SKEXP0010")] public class AzureOpenAITextToImageService : ITextToImageService @@ -26,41 +28,111 @@ public class AzureOpenAITextToImageService : ITextToImageService /// /// Initializes a new instance of the class. /// - /// The model to use for image generation. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// Non-default endpoint for the OpenAI API. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. + /// Azure OpenAI service API version, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart public AzureOpenAITextToImageService( - string modelId, - string? apiKey = null, - string? organizationId = null, - Uri? endpoint = null, + string deploymentName, + string endpoint, + string apiKey, + string? modelId, HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) + ILoggerFactory? loggerFactory = null, + string? apiVersion = null) { - this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); + Verify.NotNullOrWhiteSpace(apiKey); + + var connectorEndpoint = !string.IsNullOrWhiteSpace(endpoint) ? endpoint! : httpClient?.BaseAddress?.AbsoluteUri; + if (connectorEndpoint is null) + { + throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); + } + + var options = ClientCore.GetAzureOpenAIClientOptions( + httpClient, + AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // DALL-E 3 is supported in the latest API releases - https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#image-generation + + var azureOpenAIClient = new AzureOpenAIClient(new Uri(connectorEndpoint), apiKey, options); + + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(this.GetType())); + + if (modelId is not null) + { + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } } /// /// Initializes a new instance of the class. /// - /// Model name - /// Custom for HTTP requests. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. + /// Azure OpenAI service API version, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart public AzureOpenAITextToImageService( - string modelId, - OpenAIClient openAIClient, + string deploymentName, + string endpoint, + TokenCredential credential, + string? modelId, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null, + string? apiVersion = null) + { + Verify.NotNull(credential); + + var connectorEndpoint = !string.IsNullOrWhiteSpace(endpoint) ? endpoint! : httpClient?.BaseAddress?.AbsoluteUri; + if (connectorEndpoint is null) + { + throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); + } + + var options = ClientCore.GetAzureOpenAIClientOptions( + httpClient, + AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // DALL-E 3 is supported in the latest API releases - https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#image-generation + + var azureOpenAIClient = new AzureOpenAIClient(new Uri(connectorEndpoint), credential, options); + + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(this.GetType())); + + if (modelId is not null) + { + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + } + + /// + /// Initializes a new instance of the class. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToImageService( + string deploymentName, + AzureOpenAIClient azureOpenAIClient, + string? modelId, ILoggerFactory? loggerFactory = null) { - this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); + Verify.NotNull(azureOpenAIClient); + + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(this.GetType())); + + if (modelId is not null) + { + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } } /// public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) { - this._client.LogActionDetails(); return this._client.GenerateImageAsync(description, width, height, cancellationToken); } } From caed23a0245d6f19098df3d5c10cc3b0105245fe Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 4 Jul 2024 11:51:05 +0100 Subject: [PATCH 028/226] .Net: OpenAI V2 - Migrated FileService - Phase 05 (#7076) ### Motivation and Context - Added FileService OpenAI V2 Implementation - Updated Extensions and UT accordingly - Updated ClientCore to be used with FileService and allow non-required `modelId`. --- .../Core/ClientCoreTests.cs | 9 - .../KernelBuilderExtensionsTests.cs | 12 + .../OpenAIFileUploadExecutionSettingsTests.cs | 24 ++ .../ServiceCollectionExtensionsTests.cs | 13 + .../Services/OpenAIAudioToTextServiceTests.cs | 12 + .../Services/OpenAIFileServiceTests.cs | 319 ++++++++++++++++++ ...enAITextEmbeddingGenerationServiceTests.cs | 13 + .../Services/OpenAITextToAudioServiceTests.cs | 12 + .../Services/OpenAITextToImageServiceTests.cs | 12 + .../Connectors.OpenAIV2.csproj | 4 +- .../Core/ClientCore.File.cs | 124 +++++++ .../Connectors.OpenAIV2/Core/ClientCore.cs | 32 +- .../OpenAIKernelBuilderExtensions.cs | 34 ++ .../OpenAIServiceCollectionExtensions.cs | 32 ++ .../Models/OpenAIFilePurpose.cs | 22 ++ .../Models/OpenAIFileReference.cs | 38 +++ .../Services/OpenAIAudioToTextService.cs | 2 + .../Services/OpenAIFileService.cs | 128 +++++++ .../OpenAITextEmbbedingGenerationService.cs | 2 + .../Services/OpenAITextToAudioService.cs | 2 + .../Services/OpenAITextToImageService.cs | 2 + .../OpenAIFileUploadExecutionSettings.cs | 35 ++ 22 files changed, 860 insertions(+), 23 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs index f162e1d7334c..b6783adc4823 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs @@ -222,15 +222,6 @@ public void ItAddOrNotOrganizationIdAttributeWhenProvided() Assert.False(clientCoreWithoutOrgId.Attributes.ContainsKey(ClientCore.OrganizationKey)); } - [Fact] - public void ItThrowsIfModelIdIsNotProvided() - { - // Act & Assert - Assert.Throws(() => new ClientCore(" ", "apikey")); - Assert.Throws(() => new ClientCore("", "apikey")); - Assert.Throws(() => new ClientCore(null!)); - } - [Fact] public void ItThrowsWhenNotUsingCustomEndpointAndApiKeyIsNotProvided() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs index bfa71f7e5ab3..6068dbe558da 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextToAudio; @@ -132,4 +133,15 @@ public void ItCanAddAudioToTextServiceWithOpenAIClient() // Assert Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } + + [Fact] + public void ItCanAddFileService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddOpenAIFiles("key").Build() + .GetRequiredService(); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs new file mode 100644 index 000000000000..8e4ffa622ca8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; + +public class OpenAIFileUploadExecutionSettingsTests +{ + [Fact] + public void ItCanCreateOpenAIFileUploadExecutionSettings() + { + // Arrange + var fileName = "file.txt"; + var purpose = OpenAIFilePurpose.FineTune; + + // Act + var settings = new OpenAIFileUploadExecutionSettings(fileName, purpose); + + // Assert + Assert.Equal(fileName, settings.FileName); + Assert.Equal(purpose, settings.Purpose); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 79c8024bb93f..19c030b820fb 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextToAudio; @@ -133,4 +134,16 @@ public void ItCanAddAudioToTextServiceWithOpenAIClient() // Assert Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } + + [Fact] + public void ItCanAddFileService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddOpenAIFiles("key") + .BuildServiceProvider() + .GetRequiredService(); + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs index 9648670d3de5..5627803bfab1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -45,6 +45,18 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) Assert.Equal("model-id", service.Attributes["ModelId"]); } + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new OpenAIAudioToTextService(" ", "apikey")); + Assert.Throws(() => new OpenAIAudioToTextService(" ", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAIAudioToTextService("", "apikey")); + Assert.Throws(() => new OpenAIAudioToTextService("", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAIAudioToTextService(null!, "apikey")); + Assert.Throws(() => new OpenAIAudioToTextService(null!, openAIClient: new("apikey"))); + } + [Theory] [InlineData(true)] [InlineData(false)] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs new file mode 100644 index 000000000000..85ac2f2bf8d4 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs @@ -0,0 +1,319 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIFileServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public OpenAIFileServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWorksCorrectlyForOpenAI(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAIFileService("api-key", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIFileService("api-key"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWorksCorrectlyForAzure(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAIFileService(new Uri("http://localhost"), "api-key", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIFileService(new Uri("http://localhost"), "api-key"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DeleteFileWorksCorrectlyAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "test.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); + + this._messageHandlerStub.ResponseToReturn = response; + + // Act & Assert + await service.DeleteFileAsync("file-id"); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task DeleteFileFailsAsExpectedAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateFailedResponse(); + + this._messageHandlerStub.ResponseToReturn = response; + + // Act & Assert + await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetFileWorksCorrectlyAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "file.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); + + this._messageHandlerStub.ResponseToReturn = response; + + // Act & Assert + var file = await service.GetFileAsync("file-id"); + Assert.NotNull(file); + Assert.NotEqual(string.Empty, file.Id); + Assert.NotEqual(string.Empty, file.FileName); + Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); + Assert.NotEqual(0, file.SizeInBytes); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetFileFailsAsExpectedAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateFailedResponse(); + this._messageHandlerStub.ResponseToReturn = response; + + // Act & Assert + await Assert.ThrowsAsync(() => service.GetFileAsync("file-id")); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetFilesWorksCorrectlyAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateSuccessResponse( + """ + { + "data": [ + { + "id": "123", + "filename": "file1.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + }, + { + "id": "456", + "filename": "file2.txt", + "purpose": "assistants", + "bytes": 999, + "created_at": 1677610606 + } + ] + } + """); + + this._messageHandlerStub.ResponseToReturn = response; + + // Act & Assert + var files = (await service.GetFilesAsync()).ToArray(); + Assert.NotNull(files); + Assert.NotEmpty(files); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetFilesFailsAsExpectedAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateFailedResponse(); + + this._messageHandlerStub.ResponseToReturn = response; + + await Assert.ThrowsAsync(() => service.GetFilesAsync()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task GetFileContentWorksCorrectlyAsync(bool isCustomEndpoint) + { + // Arrange + var data = BinaryData.FromString("Hello AI!"); + var service = this.CreateFileService(isCustomEndpoint); + this._messageHandlerStub.ResponseToReturn = + new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new ByteArrayContent(data.ToArray()) + }; + + // Act & Assert + var content = await service.GetFileContentAsync("file-id"); + var result = content.Data!.Value; + Assert.Equal(data.ToArray(), result.ToArray()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task UploadContentWorksCorrectlyAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "test.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); + + this._messageHandlerStub.ResponseToReturn = response; + + var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); + + await using var stream = new MemoryStream(); + await using (var writer = new StreamWriter(stream, leaveOpen: true)) + { + await writer.WriteLineAsync("test"); + await writer.FlushAsync(); + } + + stream.Position = 0; + + var content = new BinaryContent(stream.ToArray(), "text/plain"); + + // Act & Assert + var file = await service.UploadContentAsync(content, settings); + Assert.NotNull(file); + Assert.NotEqual(string.Empty, file.Id); + Assert.NotEqual(string.Empty, file.FileName); + Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); + Assert.NotEqual(0, file.SizeInBytes); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task UploadContentFailsAsExpectedAsync(bool isCustomEndpoint) + { + // Arrange + var service = this.CreateFileService(isCustomEndpoint); + using var response = this.CreateFailedResponse(); + + this._messageHandlerStub.ResponseToReturn = response; + + var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); + + await using var stream = new MemoryStream(); + await using (var writer = new StreamWriter(stream, leaveOpen: true)) + { + await writer.WriteLineAsync("test"); + await writer.FlushAsync(); + } + + stream.Position = 0; + + var content = new BinaryContent(stream.ToArray(), "text/plain"); + + // Act & Assert + await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings)); + } + + private OpenAIFileService CreateFileService(bool isCustomEndpoint = false) + { + return + isCustomEndpoint ? + new OpenAIFileService(new Uri("http://localhost"), "api-key", httpClient: this._httpClient) : + new OpenAIFileService("api-key", "organization", this._httpClient); + } + + private HttpResponseMessage CreateSuccessResponse(string payload) + { + return + new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = + new StringContent( + payload, + Encoding.UTF8, + "application/json") + }; + } + + private HttpResponseMessage CreateFailedResponse(string? payload = null) + { + return + new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) + { + Content = + string.IsNullOrEmpty(payload) ? + null : + new StringContent( + payload, + Encoding.UTF8, + "application/json") + }; + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs index 5fb36efc0349..0181d15d8449 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.ClientModel; using System.IO; using System.Net; @@ -35,6 +36,18 @@ public void ItCanBeInstantiatedAndPropertiesSetAsExpected() Assert.Equal("model", sutWithOpenAIClient.Attributes[AIServiceExtensions.ModelIdKey]); } + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new OpenAITextEmbeddingGenerationService(" ", "apikey")); + Assert.Throws(() => new OpenAITextEmbeddingGenerationService(" ", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextEmbeddingGenerationService("", "apikey")); + Assert.Throws(() => new OpenAITextEmbeddingGenerationService("", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextEmbeddingGenerationService(null!, "apikey")); + Assert.Throws(() => new OpenAITextEmbeddingGenerationService(null!, openAIClient: new("apikey"))); + } + [Fact] public async Task ItGetEmbeddingsAsyncReturnsEmptyWhenProvidedDataIsEmpty() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs index e8fdb7b46b1e..9c7de44d8a83 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs @@ -45,6 +45,18 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) Assert.Equal("model-id", service.Attributes["ModelId"]); } + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new OpenAITextToAudioService(" ", "apikey")); + Assert.Throws(() => new OpenAITextToAudioService(" ", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextToAudioService("", "apikey")); + Assert.Throws(() => new OpenAITextToAudioService("", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextToAudioService(null!, "apikey")); + Assert.Throws(() => new OpenAITextToAudioService(null!, openAIClient: new("apikey"))); + } + [Theory] [MemberData(nameof(ExecutionSettings))] public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs index f449059e8ab5..c31c1f275dbc 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -47,6 +47,18 @@ public void ConstructorWorksCorrectly() Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); } + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new OpenAITextToImageService(" ", "apikey")); + Assert.Throws(() => new OpenAITextToImageService(" ", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextToImageService("", "apikey")); + Assert.Throws(() => new OpenAITextToImageService("", openAIClient: new("apikey"))); + Assert.Throws(() => new OpenAITextToImageService(null!, "apikey")); + Assert.Throws(() => new OpenAITextToImageService(null!, openAIClient: new("apikey"))); + } + [Fact] public void OpenAIClientConstructorWorksCorrectly() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index 22f364461818..bab4ac2c2e15 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -17,8 +17,8 @@ - Semantic Kernel - OpenAI and Azure OpenAI connectors - Semantic Kernel connectors for OpenAI and Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + Semantic Kernel - OpenAI connector + Semantic Kernel connectors for OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs new file mode 100644 index 000000000000..41a9f470c4b0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 05 +- Ignoring the specific Purposes not implemented by current FileService. +*/ + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Files; + +using OAIFilePurpose = OpenAI.Files.OpenAIFilePurpose; +using SKFilePurpose = Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Uploads a file to OpenAI. + /// + /// File name + /// File content + /// Purpose of the file + /// Cancellation token + /// Uploaded file information + internal async Task UploadFileAsync( + string fileName, + Stream fileContent, + SKFilePurpose purpose, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().UploadFileAsync(fileContent, fileName, ConvertToOpenAIFilePurpose(purpose), cancellationToken)).ConfigureAwait(false); + return ConvertToFileReference(response.Value); + } + + /// + /// Delete a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + internal async Task DeleteFileAsync( + string fileId, + CancellationToken cancellationToken) + { + await RunRequestAsync(() => this.Client.GetFileClient().DeleteFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + } + + /// + /// Retrieve metadata for a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The metadata associated with the specified file identifier. + internal async Task GetFileAsync( + string fileId, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + return ConvertToFileReference(response.Value); + } + + /// + /// Retrieve metadata for all previously uploaded files. + /// + /// The to monitor for cancellation requests. The default is . + /// The metadata of all uploaded files. + internal async Task> GetFilesAsync(CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFilesAsync(cancellationToken: cancellationToken)).ConfigureAwait(false); + return response.Value.Select(ConvertToFileReference); + } + + /// + /// Retrieve the file content from a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The file content as + /// + /// Files uploaded with do not support content retrieval. + /// + internal async Task GetFileContentAsync( + string fileId, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().DownloadFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + return response.Value.ToArray(); + } + + private static OpenAIFileReference ConvertToFileReference(OpenAIFileInfo fileInfo) + => new() + { + Id = fileInfo.Id, + CreatedTimestamp = fileInfo.CreatedAt.DateTime, + FileName = fileInfo.Filename, + SizeInBytes = (int)(fileInfo.SizeInBytes ?? 0), + Purpose = ConvertToFilePurpose(fileInfo.Purpose), + }; + + private static FileUploadPurpose ConvertToOpenAIFilePurpose(SKFilePurpose purpose) + { + if (purpose == SKFilePurpose.Assistants) { return FileUploadPurpose.Assistants; } + if (purpose == SKFilePurpose.FineTune) { return FileUploadPurpose.FineTune; } + + throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); + } + + private static SKFilePurpose ConvertToFilePurpose(OAIFilePurpose purpose) + { + if (purpose == OAIFilePurpose.Assistants) { return SKFilePurpose.Assistants; } + if (purpose == OAIFilePurpose.FineTune) { return SKFilePurpose.FineTune; } + + throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs index 355000887f51..695f23579ad1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -9,6 +9,10 @@ All logic from original ClientCore and OpenAIClientCore were preserved. - Moved AddAttributes usage to the constructor, avoiding the need verify and adding it in the services. - Added ModelId attribute to the OpenAIClient constructor. - Added WhiteSpace instead of empty string for ApiKey to avoid exception from OpenAI Client on custom endpoints added an issue in OpenAI SDK repo. https://github.com/openai/openai-dotnet/issues/90 + +Phase 05: +- Model Id became not be required to support services like: File Service. + */ using System; @@ -65,7 +69,7 @@ internal partial class ClientCore internal ILogger Logger { get; init; } /// - /// OpenAI / Azure OpenAI Client + /// OpenAI Client /// internal OpenAIClient Client { get; } @@ -84,19 +88,20 @@ internal partial class ClientCore /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. internal ClientCore( - string modelId, + string? modelId = null, string? apiKey = null, string? organizationId = null, Uri? endpoint = null, HttpClient? httpClient = null, ILogger? logger = null) { - Verify.NotNullOrWhiteSpace(modelId); + if (!string.IsNullOrWhiteSpace(modelId)) + { + this.ModelId = modelId!; + this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } this.Logger = logger ?? NullLogger.Instance; - this.ModelId = modelId; - - this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); // Accepts the endpoint if provided, otherwise uses the default OpenAI endpoint. this.Endpoint = endpoint ?? httpClient?.BaseAddress; @@ -129,22 +134,25 @@ internal ClientCore( /// Note: instances created this way might not have the default diagnostics settings, /// it's up to the caller to configure the client. /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// OpenAI model Id /// Custom . /// The to use for logging. If null, no logging will be performed. internal ClientCore( - string modelId, + string? modelId, OpenAIClient openAIClient, ILogger? logger = null) { - Verify.NotNullOrWhiteSpace(modelId); + // Model Id may not be required when other services. i.e: File Service. + if (modelId is not null) + { + this.ModelId = modelId; + this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + Verify.NotNull(openAIClient); this.Logger = logger ?? NullLogger.Instance; - this.ModelId = modelId; this.Client = openAIClient; - - this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index ce4a4d9866e0..37ac7d384647 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -287,4 +287,38 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => } #endregion + + #region Files + + /// + /// Add the OpenAI file service to the list + /// + /// The instance to augment. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAIFiles( + this IKernelBuilder builder, + string apiKey, + string? orgId = null, + string? serviceId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(apiKey); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAIFileService( + apiKey, + orgId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService())); + + return builder; + } + + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index 769634c1cea7..c1c2fe7dd2f7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -266,4 +266,36 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => return services; } #endregion + + #region Files + + /// + /// Add the OpenAI file service to the list + /// + /// The instance to augment. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAIFiles( + this IServiceCollection services, + string apiKey, + string? orgId = null, + string? serviceId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(apiKey); + + services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new OpenAIFileService( + apiKey, + orgId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService())); + + return services; + } + + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs new file mode 100644 index 000000000000..a01b2d08fa8d --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Defines the purpose associated with the uploaded file. +/// +[Experimental("SKEXP0010")] +public enum OpenAIFilePurpose +{ + /// + /// File to be used by assistants for model processing. + /// + Assistants, + + /// + /// File to be used by fine-tuning jobs. + /// + FineTune, +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs new file mode 100644 index 000000000000..371be0d93a33 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// References an uploaded file by id. +/// +[Experimental("SKEXP0010")] +public sealed class OpenAIFileReference +{ + /// + /// The file identifier. + /// + public string Id { get; set; } = string.Empty; + + /// + /// The timestamp the file was uploaded.s + /// + public DateTime CreatedTimestamp { get; set; } + + /// + /// The name of the file.s + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Describes the associated purpose of the file. + /// + public OpenAIFilePurpose Purpose { get; set; } + + /// + /// The file size, in bytes. + /// + public int SizeInBytes { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index cb37384845df..9084ab1782c3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -49,6 +49,7 @@ public OpenAIAudioToTextService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); } @@ -63,6 +64,7 @@ public OpenAIAudioToTextService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs new file mode 100644 index 000000000000..8b50df3f3639 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// File service access for OpenAI: https://api.openai.com/v1/files +/// +[Experimental("SKEXP0010")] +public sealed class OpenAIFileService +{ + /// + /// OpenAI client for HTTP operations. + /// + private readonly ClientCore _client; + + /// + /// Create an instance of the OpenAI chat completion connector + /// + /// Non-default endpoint for the OpenAI API. + /// API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIFileService( + Uri endpoint, + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + + this._client = new(null, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + } + + /// + /// Create an instance of the OpenAI chat completion connector + /// + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIFileService( + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + + this._client = new(null, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + } + + /// + /// Remove a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + public Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + + return this._client.DeleteFileAsync(id, cancellationToken); + } + + /// + /// Retrieve the file content from a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The file content as + /// + /// Files uploaded with do not support content retrieval. + /// + public async Task GetFileContentAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + var bytes = await this._client.GetFileContentAsync(id, cancellationToken).ConfigureAwait(false); + + // The mime type of the downloaded file is not provided by the OpenAI API. + return new(bytes, null); + } + + /// + /// Retrieve metadata for a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The metadata associated with the specified file identifier. + public Task GetFileAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + return this._client.GetFileAsync(id, cancellationToken); + } + + /// + /// Retrieve metadata for all previously uploaded files. + /// + /// The to monitor for cancellation requests. The default is . + /// The metadata of all uploaded files. + public async Task> GetFilesAsync(CancellationToken cancellationToken = default) + => await this._client.GetFilesAsync(cancellationToken).ConfigureAwait(false); + + /// + /// Upload a file. + /// + /// The file content as + /// The upload settings + /// The to monitor for cancellation requests. The default is . + /// The file metadata. + public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default) + { + Verify.NotNull(settings, nameof(settings)); + Verify.NotNull(fileContent.Data, nameof(fileContent.Data)); + + using var memoryStream = new MemoryStream(fileContent.Data.Value.ToArray()); + return await this._client.UploadFileAsync(settings.FileName, memoryStream, settings.Purpose, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index ea607b2565b3..39837bde1bc4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -44,6 +44,7 @@ public OpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new( modelId: modelId, apiKey: apiKey, @@ -68,6 +69,7 @@ public OpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); this._dimensions = dimensions; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs index 87346eefb1b5..2032d8fd2c12 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -49,6 +49,7 @@ public OpenAITextToAudioService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); } @@ -63,6 +64,7 @@ public OpenAITextToAudioService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 15ebcf049a93..e152c608922f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -50,6 +50,7 @@ public OpenAITextToImageService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); } @@ -64,6 +65,7 @@ public OpenAITextToImageService( OpenAIClient openAIClient, ILoggerFactory? loggerFactory = null) { + Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToImageService))); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs new file mode 100644 index 000000000000..3b49c1850df0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Execution settings associated with Open AI file upload . +/// +[Experimental("SKEXP0010")] +public sealed class OpenAIFileUploadExecutionSettings +{ + /// + /// Initializes a new instance of the class. + /// + /// The file name + /// The file purpose + public OpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose) + { + Verify.NotNull(fileName, nameof(fileName)); + + this.FileName = fileName; + this.Purpose = purpose; + } + + /// + /// The file name. + /// + public string FileName { get; } + + /// + /// The file purpose. + /// + public OpenAIFilePurpose Purpose { get; } +} From 965fe63a25ef61d9582b718202ba215152bb2080 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 4 Jul 2024 16:02:59 +0100 Subject: [PATCH 029/226] .Net: Copy AzureOpenAITextToAudioService related code to AzureOpenAI project (#7099) ### Motivation, Context and Description This PR copies the existing `AzureOpenAITextToAudioService` class from the `Connectors.OpenAI` project and `ClientCore.TextToAudio` class from `Connectors.OpenAIV2` to the `Connectors.AzureOpenAI` project. The copied classes have no functional changes; they have only been renamed to include the Azure prefix and placed into the corresponding Microsoft.SemanticKernel.Connectors.AzureOpenAI namespace. All the classes are temporarily excluded from the compilation process. This is done to simplify the code review of follow-up PR(s) that will include functional changes. --- .../Connectors.AzureOpenAI.csproj | 10 +++ .../Core/ClientCore.TextToAudio.cs | 67 +++++++++++++++++++ .../Services/AzureOpenAITextToAudioService.cs | 63 +++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..73bba3cb28f6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,10 +21,20 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs new file mode 100644 index 000000000000..d11b5ce81a26 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Audio; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Prompt to generate the image + /// Text to Audio execution settings for the prompt + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task> GetAudioContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + CancellationToken cancellationToken) + { + Verify.NotNullOrWhiteSpace(prompt); + + OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings?.ResponseFormat); + SpeechGenerationOptions options = new() + { + ResponseFormat = responseFormat, + Speed = audioExecutionSettings?.Speed, + }; + + ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); + + return [new AudioContent(response.Value.ToArray(), mimeType)]; + } + + private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice) + => voice?.ToUpperInvariant() switch + { + "ALLOY" => GeneratedSpeechVoice.Alloy, + "ECHO" => GeneratedSpeechVoice.Echo, + "FABLE" => GeneratedSpeechVoice.Fable, + "ONYX" => GeneratedSpeechVoice.Onyx, + "NOVA" => GeneratedSpeechVoice.Nova, + "SHIMMER" => GeneratedSpeechVoice.Shimmer, + _ => throw new NotSupportedException($"The voice '{voice}' is not supported."), + }; + + private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) + => format?.ToUpperInvariant() switch + { + "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"), + "MP3" => (GeneratedSpeechFormat.Mp3, "audio/mpeg"), + "OPUS" => (GeneratedSpeechFormat.Opus, "audio/opus"), + "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"), + "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"), + "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), + _ => throw new NotSupportedException($"The format '{format}' is not supported.") + }; +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs new file mode 100644 index 000000000000..5dd70f6fd38f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextToAudio; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI text-to-audio service. +/// +[Experimental("SKEXP0001")] +public sealed class AzureOpenAITextToAudioService : ITextToAudioService +{ + /// + /// Azure OpenAI text-to-audio client for HTTP operations. + /// + private readonly AzureOpenAITextToAudioClient _client; + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + /// Gets the key used to store the deployment name in the dictionary. + /// + public static string DeploymentNameKey => "DeploymentName"; + + /// + /// Creates an instance of the connector with API key auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAITextToAudioService( + string deploymentName, + string endpoint, + string apiKey, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._client = new(deploymentName, endpoint, apiKey, modelId, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextToAudioService))); + + this._client.AddAttribute(DeploymentNameKey, deploymentName); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + public Task> GetAudioContentsAsync( + string text, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); +} From 6d7434ffe43369d69887a0123134e9ca9dbbdb7a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 5 Jul 2024 13:01:04 +0100 Subject: [PATCH 030/226] .Net: Migrate AzureOpenAITextToImageService to Azure.AI.OpenAI SDK v2 (#7097) ### Motivation, Context, and Description This PR adds service collection and kernel builder extension methods that register the newly added `AzureOpenAITextToImageService`. The method signatures remain the same as the current extension methods, except for those that had an `OpenAIClient` parameter, whose name has been changed from `openAIClient` to `azureOpenAIClient` and whose type has been changed to `AzureOpenAIClient`. The breaking change is tracked in this issue: https://github.com/microsoft/semantic-kernel/issues/7053. Additionally, this PR adds unit tests for the extension methods and integration tests for the service. --- ...eOpenAIServiceCollectionExtensionsTests.cs | 35 ++++++ ...enAIServiceKernelBuilderExtensionsTests.cs | 57 +++++++-- .../AzureOpenAIKernelBuilderExtensions.cs | 113 ++++++++++++++++++ .../AzureOpenAIServiceCollectionExtensions.cs | 104 ++++++++++++++++ .../AzureOpenAITextToImageTests.cs | 41 +++++++ 5 files changed, 339 insertions(+), 11 deletions(-) create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs index ca4899258b21..70c2bfbe385a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToImage; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -88,6 +89,40 @@ public void ServiceCollectionAddAzureOpenAITextEmbeddingGenerationAddsValidServi #endregion + #region Text to image + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void ServiceCollectionExtensionsAddAzureOpenAITextToImageService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", credentials), + InitializationType.ClientInline => builder.Services.AddAzureOpenAITextToImage("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.Services.AddAzureOpenAITextToImage("deployment-name"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.True(service is AzureOpenAITextToImageService); + } + + #endregion + public enum InitializationType { ApiKey, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs index 8c5515516ca5..f5b6f5516d22 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToImage; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -22,8 +23,8 @@ public sealed class AzureOpenAIServiceKernelBuilderExtensionsTests [Theory] [InlineData(InitializationType.ApiKey)] [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) { // Arrange @@ -38,8 +39,8 @@ public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(Initializa { InitializationType.ApiKey => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), InitializationType.TokenCredential => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAIChatCompletion("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAIChatCompletion("deployment-name"), + InitializationType.ClientInline => builder.AddAzureOpenAIChatCompletion("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.AddAzureOpenAIChatCompletion("deployment-name"), _ => builder }; @@ -58,8 +59,8 @@ public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(Initializa [Theory] [InlineData(InitializationType.ApiKey)] [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) { // Arrange @@ -74,8 +75,8 @@ public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(I { InitializationType.ApiKey => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), InitializationType.TokenCredential => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), + InitializationType.ClientInline => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), _ => builder }; @@ -88,12 +89,46 @@ public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(I #endregion + #region Text to image + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void KernelBuilderExtensionsAddAzureOpenAITextToImageService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("http://localhost"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", credentials), + InitializationType.ClientInline => builder.AddAzureOpenAITextToImage("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.AddAzureOpenAITextToImage("deployment-name"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.True(service is AzureOpenAITextToImageService); + } + + #endregion + public enum InitializationType { ApiKey, TokenCredential, - OpenAIClientInline, - OpenAIClientInServiceProvider, - OpenAIClientEndpoint, + ClientInline, + ClientInServiceProvider, + ClientEndpoint, } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs index 0a391e4693c0..ed82a788079e 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToImage; #pragma warning disable IDE0039 // Use local function @@ -248,6 +249,118 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( #endregion + #region Images + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Azure OpenAI API version + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextToImage( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? modelId = null, + string? serviceId = null, + string? apiVersion = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + endpoint, + credentials, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService(), + apiVersion)); + + return builder; + } + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Azure OpenAI API version + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextToImage( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? modelId = null, + string? serviceId = null, + string? apiVersion = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService(), + apiVersion)); + + return builder; + } + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAITextToImage( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? azureOpenAIClient = null, + string? modelId = null, + string? serviceId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + azureOpenAIClient ?? serviceProvider.GetRequiredService(), + modelId, + serviceProvider.GetService())); + + return builder; + } + + #endregion + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index 0c719ed8b249..b3995680bb16 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToImage; #pragma warning disable IDE0039 // Use local function @@ -234,6 +235,109 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( #endregion + #region Images + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Azure OpenAI API version + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextToImage( + this IServiceCollection services, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? modelId = null, + string? serviceId = null, + string? apiVersion = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + endpoint, + credentials, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService(), + apiVersion)); + } + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Maximum number of attempts to retrieve the text to image operation result. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextToImage( + this IServiceCollection services, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + int maxRetryCount = 5) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService())); + } + + /// + /// Add the Azure OpenAI text-to-image service to the list. + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextToImage( + this IServiceCollection services, + string deploymentName, + AzureOpenAIClient? openAIClient = null, + string? modelId = null, + string? serviceId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToImageService( + deploymentName, + openAIClient ?? serviceProvider.GetRequiredService(), + modelId, + serviceProvider.GetService())); + } + + #endregion + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs new file mode 100644 index 000000000000..08e2599fd51e --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextToImage; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +public sealed class AzureOpenAITextToImageTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Fact] + public async Task ItCanReturnImageUrlAsync() + { + // Arrange + AzureOpenAIConfiguration? configuration = this._configuration.GetSection("AzureOpenAITextToImage").Get(); + Assert.NotNull(configuration); + + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAITextToImage(configuration.DeploymentName, configuration.Endpoint, configuration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + // Act + var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", 1024, 1024); + + // Assert + Assert.NotNull(result); + Assert.StartsWith("https://", result); + } +} From 5eefea7e0f9d8199e9eeac331f2d88894263ec1f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 5 Jul 2024 14:35:50 +0100 Subject: [PATCH 031/226] .Net: Clean-up (#7107) ### Motivation and Context A few cosmetic improvements were identified while migrating {Azure}OpenAI services to Azure.AI.OpenAI SDK v2. This PR implements those improvements. --- .../0046-kernel-content-graduation.md | 6 +++--- ...AzureOpenAIKernelBuilderExtensionsTests.cs} | 4 ++-- .../AzureOpenAIKernelBuilderExtensions.cs | 18 +++++++++--------- .../AzureOpenAIServiceCollectionExtensions.cs | 18 +++++++++--------- .../AzureOpenAIChatCompletionService.cs | 7 +++---- ...zureOpenAITextEmbeddingGenerationService.cs | 6 +++--- .../Services/AzureOpenAITextToAudioService.cs | 2 +- .../OpenAIKernelBuilderExtensions.cs | 18 +++++++++--------- .../OpenAIServiceCollectionExtensions.cs | 18 +++++++++--------- .../Services/OpenAIAudioToTextService.cs | 4 ++-- .../Services/OpenAIFileService.cs | 4 ++-- .../OpenAITextEmbbedingGenerationService.cs | 4 ++-- .../Services/OpenAITextToAudioService.cs | 4 ++-- 13 files changed, 56 insertions(+), 57 deletions(-) rename dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/{AzureOpenAIServiceKernelBuilderExtensionsTests.cs => AzureOpenAIKernelBuilderExtensionsTests.cs} (97%) diff --git a/docs/decisions/0046-kernel-content-graduation.md b/docs/decisions/0046-kernel-content-graduation.md index 43518ddfa2d3..368c59bd7621 100644 --- a/docs/decisions/0046-kernel-content-graduation.md +++ b/docs/decisions/0046-kernel-content-graduation.md @@ -85,7 +85,7 @@ Pros: - With no deferred content we have simpler API and a single responsibility for contents. - Can be written and read in both `Data` or `DataUri` formats. - Can have a `Uri` reference property, which is common for specialized contexts. -- Fully serializeable. +- Fully serializable. - Data Uri parameters support (serialization included). - Data Uri and Base64 validation checks - Data Uri and Data can be dynamically generated @@ -197,7 +197,7 @@ Pros: - Can be used as a `BinaryContent` type - Can be written and read in both `Data` or `DataUri` formats. - Can have a `Uri` dedicated for referenced location. -- Fully serializeable. +- Fully serializable. - Data Uri parameters support (serialization included). - Data Uri and Base64 validation checks - Can be retrieved @@ -254,7 +254,7 @@ Pros: - Can be used as a `BinaryContent` type - Can be written and read in both `Data` or `DataUri` formats. - Can have a `Uri` dedicated for referenced location. -- Fully serializeable. +- Fully serializable. - Data Uri parameters support (serialization included). - Data Uri and Base64 validation checks - Can be retrieved diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs similarity index 97% rename from dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs index f5b6f5516d22..7d6e09dddbb1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs @@ -14,9 +14,9 @@ namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; /// -/// Unit tests for the kernel builder extensions in the class. +/// Unit tests for the kernel builder extensions in the class. /// -public sealed class AzureOpenAIServiceKernelBuilderExtensionsTests +public sealed class AzureOpenAIKernelBuilderExtensionsTests { #region Chat completion diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs index ed82a788079e..9bb6b2f18f5d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -27,7 +27,7 @@ public static class AzureOpenAIKernelBuilderExtensions #region Chat Completion /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -67,7 +67,7 @@ public static IKernelBuilder AddAzureOpenAIChatCompletion( } /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -107,7 +107,7 @@ public static IKernelBuilder AddAzureOpenAIChatCompletion( } /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -139,7 +139,7 @@ public static IKernelBuilder AddAzureOpenAIChatCompletion( #region Text Embedding /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -177,7 +177,7 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( } /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -216,7 +216,7 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( } /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -252,7 +252,7 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( #region Images /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -290,7 +290,7 @@ public static IKernelBuilder AddAzureOpenAITextToImage( } /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -330,7 +330,7 @@ public static IKernelBuilder AddAzureOpenAITextToImage( } /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index b3995680bb16..bfd3e4f65fbe 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -27,7 +27,7 @@ public static class AzureOpenAIServiceCollectionExtensions #region Chat Completion /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -65,7 +65,7 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( } /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -103,7 +103,7 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( } /// - /// Adds the Azure OpenAI chat completion service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -135,7 +135,7 @@ public static IServiceCollection AddAzureOpenAIChatCompletion( #region Text Embedding /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -169,7 +169,7 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( } /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -204,7 +204,7 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( } /// - /// Adds an Azure OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -238,7 +238,7 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( #region Images /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -274,7 +274,7 @@ public static IServiceCollection AddAzureOpenAITextToImage( } /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource @@ -309,7 +309,7 @@ public static IServiceCollection AddAzureOpenAITextToImage( } /// - /// Add the Azure OpenAI text-to-image service to the list. + /// Adds the to the . /// /// The instance to augment. /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs index 809c6fb21f6c..61aad2714bfd 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs @@ -10,7 +10,6 @@ using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextGeneration; -using OpenAI; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -23,7 +22,7 @@ public sealed class AzureOpenAIChatCompletionService : IChatCompletionService, I private readonly ClientCore _core; /// - /// Create an instance of the connector with API key auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -45,7 +44,7 @@ public AzureOpenAIChatCompletionService( } /// - /// Create an instance of the connector with AAD auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -66,7 +65,7 @@ public AzureOpenAIChatCompletionService( } /// - /// Creates a new client instance using the specified . + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs index 8908c9291220..d332174845cf 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -24,7 +24,7 @@ public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGe private readonly int? _dimensions; /// - /// Creates a new client instance using API Key auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -50,7 +50,7 @@ public AzureOpenAITextEmbeddingGenerationService( } /// - /// Creates a new client instance supporting AAD auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -76,7 +76,7 @@ public AzureOpenAITextEmbeddingGenerationService( } /// - /// Creates a new client. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom for HTTP requests. diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs index 5dd70f6fd38f..62e081aa72c4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs @@ -31,7 +31,7 @@ public sealed class AzureOpenAITextToAudioService : ITextToAudioService public static string DeploymentNameKey => "DeploymentName"; /// - /// Creates an instance of the connector with API key auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index 37ac7d384647..795f75a5d977 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class OpenAIKernelBuilderExtensions { #region Text Embedding /// - /// Adds the OpenAI text embeddings service to the list. + /// Adds to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -64,7 +64,7 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( } /// - /// Adds the OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -95,7 +95,7 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( #region Text to Image /// - /// Add the OpenAI text-to-image service to the list + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -121,7 +121,7 @@ public static IKernelBuilder AddOpenAITextToImage( } /// - /// Add the OpenAI text-to-image service to the list + /// Adds the to the . /// /// The instance to augment. /// The model to use for image generation. @@ -159,7 +159,7 @@ public static IKernelBuilder AddOpenAITextToImage( #region Text to Audio /// - /// Adds the OpenAI text-to-audio service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -194,7 +194,7 @@ public static IKernelBuilder AddOpenAITextToAudio( } /// - /// Add the OpenAI text-to-audio service to the list + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -224,7 +224,7 @@ public static IKernelBuilder AddOpenAITextToAudio( #region Audio-to-Text /// - /// Adds the OpenAI audio-to-text service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -260,7 +260,7 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => } /// - /// Adds the OpenAI audio-to-text service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model id @@ -291,7 +291,7 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => #region Files /// - /// Add the OpenAI file service to the list + /// Adds the to the . /// /// The instance to augment. /// OpenAI API key, see https://platform.openai.com/account/api-keys diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index c1c2fe7dd2f7..eff0b551876e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -29,7 +29,7 @@ public static class OpenAIServiceCollectionExtensions { #region Text Embedding /// - /// Adds the OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -63,7 +63,7 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration( } /// - /// Adds the OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// The OpenAI model id. @@ -91,7 +91,7 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceC #region Text to Image /// - /// Add the OpenAI text-to-image service to the list + /// Adds the to the . /// /// The instance to augment. /// The model to use for image generation. @@ -121,7 +121,7 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se } /// - /// Adds the OpenAI text embeddings service to the list. + /// Adds the to the . /// /// The instance to augment. /// The OpenAI model id. @@ -149,7 +149,7 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se #region Text to Audio /// - /// Adds the OpenAI text-to-audio service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -180,7 +180,7 @@ public static IServiceCollection AddOpenAITextToAudio( } /// - /// Adds the OpenAI text-to-audio service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -208,7 +208,7 @@ public static IServiceCollection AddOpenAITextToAudio( #region Audio-to-Text /// - /// Adds the OpenAI audio-to-text service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models @@ -242,7 +242,7 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => } /// - /// Adds the OpenAI audio-to-text service to the list. + /// Adds the to the . /// /// The instance to augment. /// OpenAI model id @@ -270,7 +270,7 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => #region Files /// - /// Add the OpenAI file service to the list + /// Adds the to the . /// /// The instance to augment. /// OpenAI API key, see https://platform.openai.com/account/api-keys diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index 9084ab1782c3..eb409cb24851 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -33,7 +33,7 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService public IReadOnlyDictionary Attributes => this._client.Attributes; /// - /// Creates an instance of the with API key auth. + /// Initializes a new instance of the class. /// /// Model name /// OpenAI API Key @@ -54,7 +54,7 @@ public OpenAIAudioToTextService( } /// - /// Creates an instance of the with API key auth. + /// Initializes a new instance of the class. /// /// Model name /// Custom for HTTP requests. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs index 8b50df3f3639..4185f1237b15 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs @@ -23,7 +23,7 @@ public sealed class OpenAIFileService private readonly ClientCore _client; /// - /// Create an instance of the OpenAI chat completion connector + /// Initializes a new instance of the class. /// /// Non-default endpoint for the OpenAI API. /// API Key @@ -43,7 +43,7 @@ public OpenAIFileService( } /// - /// Create an instance of the OpenAI chat completion connector + /// Initializes a new instance of the class. /// /// OpenAI API Key /// OpenAI Organization Id (usually optional) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index 39837bde1bc4..dbb5ec08f135 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -26,7 +26,7 @@ public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerat private readonly int? _dimensions; /// - /// Create an instance of + /// Initializes a new instance of the class. /// /// Model name /// OpenAI API Key @@ -57,7 +57,7 @@ public OpenAITextEmbeddingGenerationService( } /// - /// Create an instance of the OpenAI text embedding connector + /// Initializes a new instance of the class. /// /// Model name /// Custom for HTTP requests. diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs index 2032d8fd2c12..49ca77d74c6d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -33,7 +33,7 @@ public sealed class OpenAITextToAudioService : ITextToAudioService public IReadOnlyDictionary Attributes => this._client.Attributes; /// - /// Creates an instance of the with API key auth. + /// Initializes a new instance of the class. /// /// Model name /// OpenAI API Key @@ -54,7 +54,7 @@ public OpenAITextToAudioService( } /// - /// Creates an instance of the with API key auth. + /// Initializes a new instance of the class. /// /// Model name /// Custom for HTTP requests. From ba1df519480dca83e1d6b1c7cf67e744570cf906 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 5 Jul 2024 18:19:32 +0100 Subject: [PATCH 032/226] .Net: Prepare AzureOpenAIAudioToTextService for migration to the Azure.AI.OpenAI SDK V2 (#7112) ### Motivation, Context, and Description This PR copies existing AzureOpenAIAudioToTextService-related classes to the new Connectors.AzureOpenAI project as they are, with only the namespaces changed. The classes are temporarily excluded from the compilation process and will be included again in a follow-up PR. This is done to simplify the review process of the next PR. --- .../Connectors.AzureOpenAI.csproj | 4 + .../Core/ClientCore.AudioToText.cs | 89 ++++++++++++++++++ .../Services/AzureOpenAIAudioToTextService.cs | 94 +++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 73bba3cb28f6..4ee2a67b24e2 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -22,7 +22,9 @@ + + @@ -31,7 +33,9 @@ + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs new file mode 100644 index 000000000000..59c1173bd780 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Audio; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Generates an image with the provided configuration. + /// + /// Input audio to generate the text + /// Audio-to-text execution settings for the prompt + /// The to monitor for cancellation requests. The default is . + /// Url of the generated image + internal async Task> GetTextFromAudioContentsAsync( + AudioContent input, + PromptExecutionSettings? executionSettings, + CancellationToken cancellationToken) + { + if (!input.CanRead) + { + throw new ArgumentException("The input audio content is not readable.", nameof(input)); + } + + OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; + AudioTranscriptionOptions? audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); + + Verify.ValidFilename(audioExecutionSettings?.Filename); + + using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); + + AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; + + return [new(responseData.Text, this.ModelId, metadata: GetResponseMetadata(responseData))]; + } + + /// + /// Converts to type. + /// + /// Instance of . + /// Instance of . + private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) + => new() + { + Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), + Language = executionSettings.Language, + Prompt = executionSettings.Prompt, + Temperature = executionSettings.Temperature + }; + + private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) + { + AudioTimestampGranularities result = AudioTimestampGranularities.Default; + + if (granularities is not null) + { + foreach (var granularity in granularities) + { + var openAIGranularity = granularity switch + { + OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, + OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, + _ => AudioTimestampGranularities.Default + }; + + result |= openAIGranularity; + } + } + + return result; + } + + private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) + => new(3) + { + [nameof(audioTranscription.Language)] = audioTranscription.Language, + [nameof(audioTranscription.Duration)] = audioTranscription.Duration, + [nameof(audioTranscription.Segments)] = audioTranscription.Segments + }; +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs new file mode 100644 index 000000000000..313402adce99 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Services; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Azure OpenAI audio-to-text service. +/// +[Experimental("SKEXP0001")] +public sealed class AzureOpenAIAudioToTextService : IAudioToTextService +{ + /// Core implementation shared by Azure OpenAI services. + private readonly AzureOpenAIClientCore _core; + + /// + public IReadOnlyDictionary Attributes => this._core.Attributes; + + /// + /// Creates an instance of the with API key auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIAudioToTextService( + string deploymentName, + string endpoint, + string apiKey, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + /// Creates an instance of the with AAD auth. + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIAudioToTextService( + string deploymentName, + string endpoint, + TokenCredential credentials, + string? modelId = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + /// Creates an instance of the using the specified . + /// + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Custom . + /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIAudioToTextService( + string deploymentName, + OpenAIClient openAIClient, + string? modelId = null, + ILoggerFactory? loggerFactory = null) + { + this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + } + + /// + public Task> GetTextContentsAsync( + AudioContent content, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken); +} From 348cc9b9349487a125f6675e520a30b09944fa0f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 5 Jul 2024 10:40:27 -0700 Subject: [PATCH 033/226] Checkpoint --- .../AddHeaderRequestPolicy.cs | 2 +- .../{ => Internal}/AssistantThreadActions.cs | 13 +- .../OpenAI/Internal/OpenAIClientFactory.cs | 109 +++++++++ .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 215 +++++------------- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 4 +- .../OpenAI/OpenAIAssistantConfiguration.cs | 85 ------- .../OpenAI/OpenAIAssistantDefinition.cs | 13 +- .../src/Agents/OpenAI/OpenAIConfiguration.cs | 96 ++++++++ dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 90 ++++++++ .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 144 ++++++++++++ .../Agents/OpenAI/RunPollingConfiguration.cs | 40 ++++ .../Azure/AddHeaderRequestPolicyTests.cs | 2 +- .../src/Http/HttpHeaderConstant.cs | 3 + 13 files changed, 550 insertions(+), 266 deletions(-) rename dotnet/src/Agents/OpenAI/{Azure => Internal}/AddHeaderRequestPolicy.cs (88%) rename dotnet/src/Agents/OpenAI/{ => Internal}/AssistantThreadActions.cs (96%) create mode 100644 dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs delete mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs create mode 100644 dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs diff --git a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs similarity index 88% rename from dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs rename to dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs index 084e533fe757..1fb0698ffa77 100644 --- a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs @@ -2,7 +2,7 @@ using Azure.Core; using Azure.Core.Pipeline; -namespace Microsoft.SemanticKernel.Agents.OpenAI.Azure; +namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Helper class to inject headers into Azure SDK HTTP pipeline diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs similarity index 96% rename from dotnet/src/Agents/OpenAI/AssistantThreadActions.cs rename to dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 0d7153816b2f..72358eb56eec 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -132,7 +132,6 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// The assistant agent to interact with the thread. /// The assistant client /// The thread identifier - /// Config to utilize when polling for run state. /// The logger to utilize (might be agent or channel scoped) /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. @@ -140,7 +139,6 @@ public static async IAsyncEnumerable InvokeAsync( OpenAIAssistantAgent agent, AssistantClient client, string threadId, - OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -149,7 +147,7 @@ public static async IAsyncEnumerable InvokeAsync( throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); } - ToolDefinition[]? tools = [.. agent.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; + ToolDefinition[]? tools = [.. agent.Definition.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); @@ -161,10 +159,7 @@ public static async IAsyncEnumerable InvokeAsync( //ResponseFormat = %%% }; - foreach (ToolDefinition tool in tools) // %%% - { - options.ToolsOverride.Add(tool); - } + options.ToolsOverride.AddRange(tools); // Create run ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); @@ -311,7 +306,7 @@ async Task PollRunStatusAsync() do { // Reduce polling frequency after a couple attempts - await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + await Task.Delay(count >= 2 ? agent.Polling.RunPollingInterval : agent.Polling.RunPollingBackoff, cancellationToken).ConfigureAwait(false); ++count; #pragma warning disable CA1031 // Do not catch general exception types @@ -380,7 +375,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R if (retry) { - await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(agent.Polling.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); } ++count; diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs new file mode 100644 index 000000000000..bce45b415321 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; +using System.Net.Http; +using System.Threading; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.Http; +using OpenAI; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +internal static class OpenAIClientFactory +{ + /// + /// Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key. + /// + private const string SingleSpaceKey = " "; + + /// + /// %%% + /// + /// + /// + public static OpenAIClient CreateClient(OpenAIConfiguration config) + { + OpenAIClient client; + + // Inspect options + switch (config.Type) + { + case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAIKey: + { + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); + client = new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); + break; + } + case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAICredential: + { + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); + client = new AzureOpenAIClient(config.Endpoint, config.Credentials!, clientOptions); + break; + } + case OpenAIConfiguration.OpenAIConfigurationType.OpenAI: + { + OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(config); + client = new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); + break; + } + default: + throw new KernelException($"Unsupported configuration type: {config.Type}"); + } + + return client; + } + + private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIConfiguration config) + { + AzureOpenAIClientOptions options = + new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = config.Endpoint, + }; + + ConfigureClientOptions(config.HttpClient, options); + + return options; + } + + private static OpenAIClientOptions CreateOpenAIClientOptions(OpenAIConfiguration config) + { + OpenAIClientOptions options = + new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = config.Endpoint ?? config.HttpClient?.BaseAddress, + }; + + if (!string.IsNullOrWhiteSpace(config.OrganizationId)) + { + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.OpenAIOrganizationId, config.OrganizationId!), PipelinePosition.PerCall); + } + + ConfigureClientOptions(config.HttpClient, options); + + return options; + } + + private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options) + { + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout + } + } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + => + new((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 35705dbafb82..fca25fcc0dc4 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,15 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.ClientModel.Primitives; using System.Collections.Generic; using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Http; using OpenAI; using OpenAI.Assistants; @@ -18,28 +12,15 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// A specialization based on Open AI Assistant / GPT. /// -public sealed partial class OpenAIAssistantAgent : KernelAgent +public sealed class OpenAIAssistantAgent : KernelAgent { - private readonly Assistant _assistant; private readonly AssistantClient _client; - private readonly OpenAIAssistantConfiguration _config; - - ///// - ///// A list of previously uploaded file IDs to attach to the assistant. - ///// - //public IReadOnlyList FileIds => this._assistant.FileIds; %%% + private readonly string[] _channelKeys; /// - /// A set of up to 16 key/value pairs that can be attached to an agent, used for - /// storing additional information about that object in a structured format.Keys - /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// %%% /// - public IReadOnlyDictionary Metadata => this._assistant.Metadata; - - /// - /// Expose predefined tools. - /// - internal IReadOnlyList Tools => this._assistant.Tools; + public Assistant Definition { get; } /// /// Set when the assistant has been deleted via . @@ -47,6 +28,11 @@ public sealed partial class OpenAIAssistantAgent : KernelAgent /// public bool IsDeleted { get; private set; } + /// + /// %%% + /// + public RunPollingConfiguration Polling { get; } = new(); + /// /// Define a new . /// @@ -57,7 +43,7 @@ public sealed partial class OpenAIAssistantAgent : KernelAgent /// An instance public static async Task CreateAsync( Kernel kernel, - OpenAIAssistantConfiguration config, + OpenAIConfiguration config, OpenAIAssistantDefinition definition, CancellationToken cancellationToken = default) { @@ -75,7 +61,7 @@ public static async Task CreateAsync( // Instantiate the agent return - new OpenAIAssistantAgent(client, model, config) + new OpenAIAssistantAgent(client, model, DefineChannelKeys(config)) { Kernel = kernel, }; @@ -87,29 +73,14 @@ public static async Task CreateAsync( /// Configuration for accessing the Assistants API service, such as the api-key. /// The to monitor for cancellation requests. The default is . /// An list of objects. - public static async IAsyncEnumerable ListDefinitionsAsync( - OpenAIAssistantConfiguration config, - [EnumeratorCancellation] CancellationToken cancellationToken = default) + public static IAsyncEnumerable ListDefinitionsAsync( + OpenAIConfiguration config, + CancellationToken cancellationToken = default) { // Create the client AssistantClient client = CreateClient(config); - await foreach (Assistant assistant in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) - { - yield return - new() - { - Id = assistant.Id, - Name = assistant.Name, - Description = assistant.Description, - Instructions = assistant.Instructions, - EnableCodeInterpreter = assistant.Tools.Any(t => t is CodeInterpreterToolDefinition), - EnableFileSearch = assistant.Tools.Any(t => t is FileSearchToolDefinition), - //FileIds = assistant.FileIds, %%% - Metadata = assistant.Metadata, - Model = assistant.Model, - }; - } + return client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken); } /// @@ -122,7 +93,7 @@ public static async IAsyncEnumerable ListDefinitionsA /// An instance public static async Task RetrieveAsync( Kernel kernel, - OpenAIAssistantConfiguration config, + OpenAIConfiguration config, string id, CancellationToken cancellationToken = default) { @@ -134,7 +105,7 @@ public static async Task RetrieveAsync( // Instantiate the agent return - new OpenAIAssistantAgent(client, model, config) + new OpenAIAssistantAgent(client, model, DefineChannelKeys(config)) { Kernel = kernel, }; @@ -225,32 +196,11 @@ public IAsyncEnumerable InvokeAsync( { this.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(this, this._client, threadId, this.Logger, cancellationToken); } /// - protected override IEnumerable GetChannelKeys() - { - // Distinguish from other channel types. - yield return typeof(AgentChannel).FullName!; - - // Distinguish between different Azure OpenAI endpoints or OpenAI services. - yield return this._config.Endpoint ?? "openai"; - - // Custom client receives dedicated channel. - if (this._config.HttpClient is not null) - { - if (this._config.HttpClient.BaseAddress is not null) - { - yield return this._config.HttpClient.BaseAddress.AbsoluteUri; - } - - foreach (string header in this._config.HttpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) - { - yield return header; - } - } - } + protected override IEnumerable GetChannelKeys() => this._channelKeys; /// protected override async Task CreateChannelAsync(CancellationToken cancellationToken) @@ -262,7 +212,7 @@ protected override async Task CreateChannelAsync(CancellationToken this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); return - new OpenAIAssistantChannel(this._client, thread.Id, this._config.Polling) + new OpenAIAssistantChannel(this._client, thread.Id) { Logger = this.LoggerFactory.CreateLogger() }; @@ -282,35 +232,16 @@ internal void ThrowIfDeleted() private OpenAIAssistantAgent( AssistantClient client, Assistant model, - OpenAIAssistantConfiguration config) + IEnumerable channelKeys) { - this._assistant = model; + this.Definition = model; this._client = client; - this._config = config; - - this.Description = this._assistant.Description; - this.Id = this._assistant.Id; - this.Name = this._assistant.Name; - this.Instructions = this._assistant.Instructions; - } - - private static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? httpClient) - { - AzureOpenAIClientOptions options = new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - }; - - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); - - if (httpClient is not null) - { - options.Transport = new HttpClientPipelineTransport(httpClient); - options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout - } + this._channelKeys = channelKeys.ToArray(); - return options; + this.Description = model.Description; + this.Id = model.Id; + this.Name = model.Name; + this.Instructions = model.Instructions; } private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) @@ -323,95 +254,57 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss Name = definition.Name, // %%% ResponseFormat = // %%% Temperature = - // %%% ToolResources - //assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []); %%% - // %%% NucleusSamplingFactor + // %%% NucleusSamplingFactor = }; - // %%% COPY METADATA - // Metadata = definition.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + if (definition.Metadata != null) + { + foreach (KeyValuePair item in definition.Metadata) + { + assistantCreationOptions.Metadata[item.Key] = item.Value; + } + } if (definition.EnableCodeInterpreter) { assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); } - if (definition.EnableFileSearch) + if (!string.IsNullOrWhiteSpace(definition.VectorStoreId)) { assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); + assistantCreationOptions.ToolResources.FileSearch.VectorStoreIds.Add(definition.VectorStoreId); } return assistantCreationOptions; } - private static AssistantClient CreateClient(OpenAIAssistantConfiguration config) + private static AssistantClient CreateClient(OpenAIConfiguration config) { - OpenAIClient client; - - // Inspect options - if (!string.IsNullOrWhiteSpace(config.Endpoint)) // %%% INSUFFICENT (BOTH HAVE ENDPOINT OPTION) - { - // Create client configured for Azure OpenAI, if endpoint definition is present. - AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config.HttpClient); - client = new AzureOpenAIClient(new Uri(config.Endpoint), config.ApiKey, clientOptions); - } - else - { - // Otherwise, create client configured for OpenAI. - OpenAIClientOptions clientOptions = CreateClientOptions(config.HttpClient, config.Endpoint); - client = new OpenAIClient(config.ApiKey, clientOptions); - } - - return client.GetAssistantClient(); - } - - internal static AzureOpenAIClientOptions CreateAzureClientOptions(HttpClient? httpClient) - { - AzureOpenAIClientOptions options = new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - }; - - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); - - if (httpClient is not null) - { - options.Transport = new HttpClientPipelineTransport(httpClient); - options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout - } - - return options; + OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); + return openAIClient.GetAssistantClient(); } - private static OpenAIClientOptions CreateClientOptions(HttpClient? httpClient, string? endpoint) + private static IEnumerable DefineChannelKeys(OpenAIConfiguration config) { - OpenAIClientOptions options = new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = string.IsNullOrEmpty(endpoint) ? null : new Uri(endpoint) - }; + // Distinguish from other channel types. + yield return typeof(AgentChannel).FullName!; - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + // Distinguish between different Azure OpenAI endpoints or OpenAI services. + yield return config.Endpoint != null ? config.Endpoint.ToString() : "openai"; - if (httpClient is not null) + // Custom client receives dedicated channel. + if (config.HttpClient is not null) { - options.Transport = new HttpClientPipelineTransport(httpClient); - options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. - options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout - } - - return options; - } + if (config.HttpClient.BaseAddress is not null) + { + yield return config.HttpClient.BaseAddress.AbsoluteUri; + } - private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) - { - return new GenericActionPipelinePolicy((message) => - { - if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + foreach (string header in config.HttpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) { - message.Request.Headers.Set(headerName, headerValue); + yield return header; } - }); + } } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 48faf44dab40..19bbf4eb3294 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// A specialization for use with . /// -internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) +internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId) : AgentChannel { private readonly AssistantClient _client = client; @@ -31,7 +31,7 @@ protected override IAsyncEnumerable InvokeAsync( { agent.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, this.Logger, cancellationToken); } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs deleted file mode 100644 index acc094b86969..000000000000 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Configuration to target an OpenAI Assistant API. -/// -public sealed class OpenAIAssistantConfiguration -{ - /// - /// The Assistants API Key. - /// - public string ApiKey { get; } - - /// - /// An optional endpoint if targeting Azure OpenAI Assistants API. - /// - public string? Endpoint { get; } - - /// - /// Custom for HTTP requests. - /// - public HttpClient? HttpClient { get; init; } - - /// - /// Defineds polling behavior for Assistant API requests. - /// - public PollingConfiguration Polling { get; } = new(); - - /// - /// Initializes a new instance of the class. - /// - /// The Assistants API Key - /// An optional endpoint if targeting Azure OpenAI Assistants API - public OpenAIAssistantConfiguration(string apiKey, string? endpoint = null) - { - Verify.NotNullOrWhiteSpace(apiKey); - if (!string.IsNullOrWhiteSpace(endpoint)) - { - // Only verify `endpoint` when provided (AzureOAI vs OpenAI) - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - } - - this.ApiKey = apiKey; - this.Endpoint = endpoint; - } - - /// - /// Configuration and defaults associated with polling behavior for Assistant API requests. - /// - public sealed class PollingConfiguration - { - /// - /// The default polling interval when monitoring thread-run status. - /// - public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500); - - /// - /// The default back-off interval when monitoring thread-run status. - /// - public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1); - - /// - /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. - /// - public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500); - - /// - /// The polling interval when monitoring thread-run status. - /// - public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval; - - /// - /// The back-off interval when monitoring thread-run status. - /// - public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff; - - /// - /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. - /// - public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay; - } -} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index e3e8706abf47..47fd333d348b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -9,7 +9,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; public sealed class OpenAIAssistantDefinition { /// - /// Identifies the AI model (OpenAI) or deployment (AzureOAI) this agent targets. + /// Identifies the AI model targeted by the agent. /// public string? Model { get; init; } @@ -39,14 +39,13 @@ public sealed class OpenAIAssistantDefinition public bool EnableCodeInterpreter { get; init; } /// - /// Set if retrieval is enabled. + /// Enables file-serach if specified. /// - public bool EnableFileSearch { get; init; } + public string? VectorStoreId { get; init; } - /// - /// A list of previously uploaded file IDs to attach to the assistant. - /// - public IEnumerable? FileIds { get; init; } + // %%% ResponseFormat + // %%% Temperature + // %%% NucleusSamplingFactor /// /// A set of up to 16 key/value pairs that can be attached to an agent, used for diff --git a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs new file mode 100644 index 000000000000..a0c606beca06 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using Azure.Core; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Configuration for OpenAI services. +/// +public sealed class OpenAIConfiguration +{ + internal enum OpenAIConfigurationType + { + AzureOpenAIKey, + AzureOpenAICredential, + OpenAI, + } + + /// + /// %%% + /// + /// + /// + /// + /// + public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, string apiKey, HttpClient? httpClient = null) => + // %%% VERIFY + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.AzureOpenAIKey, + }; + + /// + /// %%% + /// + /// + /// + /// + /// + public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, TokenCredential credentials, HttpClient? httpClient = null) => + // %%% VERIFY + new() + { + Credentials = credentials, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.AzureOpenAICredential, + }; + + /// + /// %%% + /// + /// + /// + /// + /// + public static OpenAIConfiguration ForOpenAI(Uri endpoint, string apiKey, HttpClient? httpClient = null) => + // %%% VERIFY + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.OpenAI, + }; + + /// + /// %%% + /// + /// + /// + /// + /// + /// + public static OpenAIConfiguration ForOpenAI(Uri endpoint, string apiKey, string organizationId, HttpClient? httpClient = null) => + // %%% VERIFY + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + OrganizationId = organizationId, + Type = OpenAIConfigurationType.OpenAI, + }; + + internal string? ApiKey { get; init; } + internal TokenCredential? Credentials { get; init; } + internal Uri? Endpoint { get; init; } + internal HttpClient? HttpClient { get; init; } + internal string? OrganizationId { get; init; } + internal OpenAIConfigurationType Type { get; init; } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs new file mode 100644 index 000000000000..94d59cb88e7a --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using OpenAI; +using OpenAI.VectorStores; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// %%% +/// +public sealed class OpenAIVectorStore +{ + private readonly VectorStoreClient _client; + + /// + /// %%% + /// + public string VectorStoreId { get; } + + /// + /// %%% + /// + /// + /// + /// + public static IAsyncEnumerable GetVectorStoresAsync(OpenAIConfiguration config, CancellationToken cancellationToken = default) + { + OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); + VectorStoreClient client = openAIClient.GetVectorStoreClient(); + + return client.GetVectorStoresAsync(ListOrder.NewestFirst, cancellationToken); + } + + /// + /// %%% + /// + /// + /// + public OpenAIVectorStore(string vectorStoreId, OpenAIConfiguration config) + { + OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); + this._client = openAIClient.GetVectorStoreClient(); + + this.VectorStoreId = vectorStoreId; + } + + // %%% BATCH JOBS ??? + + /// + /// %%% + /// + /// + /// + /// + public async Task AddFileAsync(string fileId, CancellationToken cancellationToken = default) => + await this._client.AddFileToVectorStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); + + /// + /// %%% + /// + /// + /// + public async Task DeleteAsync(CancellationToken cancellationToken = default) => + await this._client.DeleteVectorStoreAsync(this.VectorStoreId, cancellationToken).ConfigureAwait(false); + + /// + /// %%% + /// + /// + /// + public async IAsyncEnumerable GetFilesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + await foreach (VectorStoreFileAssociation file in this._client.GetFileAssociationsAsync(this.VectorStoreId, ListOrder.NewestFirst, filter: null, cancellationToken).ConfigureAwait(false)) // %%% FILTER + { + yield return file.FileId; + } + } + + /// + /// %%% + /// + /// + /// + /// + public async Task RemoveFileAsync(string fileId, CancellationToken cancellationToken = default) => + await this._client.RemoveFileFromStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs new file mode 100644 index 000000000000..555f3adfb7f3 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs @@ -0,0 +1,144 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using OpenAI; +using OpenAI.VectorStores; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// %%% +/// +public sealed class OpenAIVectorStoreBuilder(OpenAIConfiguration config) +{ + private string? _name; + private FileChunkingStrategy? _chunkingStrategy; + private VectorStoreExpirationPolicy? _expirationPolicy; + private List? _fileIds; + private Dictionary? _metadata; + + /// + /// %%% + /// + /// + public OpenAIVectorStoreBuilder AddFile(string fileId) + { + this._fileIds ??= []; + this._fileIds.Add(fileId); + + return this; + } + + /// + /// %%% + /// + /// + public OpenAIVectorStoreBuilder AddFile(string[] fileIds) + { + this._fileIds ??= []; + this._fileIds.AddRange(fileIds); + + return this; + } + + /// + /// %%% + /// + /// + /// + public OpenAIVectorStoreBuilder WithChunkingStrategy(int maxTokensPerChunk, int overlappingTokenCount) + { + this._chunkingStrategy = FileChunkingStrategy.CreateStaticStrategy(maxTokensPerChunk, overlappingTokenCount); + + return this; + } + + /// + /// %%% + /// + /// + public OpenAIVectorStoreBuilder WithExpiration(TimeSpan duration) + { + this._expirationPolicy = new VectorStoreExpirationPolicy(VectorStoreExpirationAnchor.LastActiveAt, duration.Days); + + return this; + } + + /// + /// %%% + /// + /// + /// + /// + public OpenAIVectorStoreBuilder WithMetadata(string key, string value) + { + this._metadata ??= []; + + this._metadata[key] = value; + + return this; + } + + /// + /// %%% + /// + /// + /// + public OpenAIVectorStoreBuilder WithMetadata(IDictionary metadata) + { + this._metadata ??= []; + + foreach (KeyValuePair item in this._metadata) + { + this._metadata[item.Key] = item.Value; + } + + return this; + } + + /// + /// %%% + /// + /// + /// + public OpenAIVectorStoreBuilder WithName(string name) + { + this._name = name; + + return this; + } + + /// + /// %%% + /// + /// + /// + public async Task CreateAsync(CancellationToken cancellationToken) + { + OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); + VectorStoreClient client = openAIClient.GetVectorStoreClient(); + + VectorStoreCreationOptions options = + new() + { + FileIds = this._fileIds, + ChunkingStrategy = this._chunkingStrategy, + ExpirationPolicy = this._expirationPolicy, + Name = this._name, + }; + + if (this._metadata != null) + { + foreach (KeyValuePair item in this._metadata) + { + options.Metadata.Add(item.Key, item.Value); + } + } + + VectorStore store = await client.CreateVectorStoreAsync(options, cancellationToken).ConfigureAwait(false); + + return store; + } +} diff --git a/dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs b/dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs new file mode 100644 index 000000000000..e534128a4e49 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Configuration and defaults associated with polling behavior for Assistant API run processing. +/// +public sealed class RunPollingConfiguration +{ + /// + /// The default polling interval when monitoring thread-run status. + /// + public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500); + + /// + /// The default back-off interval when monitoring thread-run status. + /// + public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1); + + /// + /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. + /// + public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500); + + /// + /// The polling interval when monitoring thread-run status. + /// + public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval; + + /// + /// The back-off interval when monitoring thread-run status. + /// + public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff; + + /// + /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. + /// + public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay; +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs index b1e4d397eded..3c2945ad0fb9 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs @@ -2,7 +2,7 @@ using System.Linq; using Azure.Core; using Azure.Core.Pipeline; -using Microsoft.SemanticKernel.Agents.OpenAI.Azure; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Azure; diff --git a/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs b/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs index db45523ee3bd..a0d0dea0b50a 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs @@ -13,6 +13,9 @@ public static class Names { /// HTTP header name to use to include the Semantic Kernel package version in all HTTP requests issued by Semantic Kernel. public static string SemanticKernelVersion => "Semantic-Kernel-Version"; + + /// HTTP header name to use to include the Open AI organization identifier. + public static string OpenAIOrganizationId => "OpenAI-Organization"; } public static class Values From b458a741c0d03adb63d2689a548830acc52a0af1 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 5 Jul 2024 18:51:59 +0100 Subject: [PATCH 034/226] .Net: Migrate AzureOpenAITextToAudioService to Azure.AI.OpenAI SDK v2 (#7102) ### Motivation and Context This PR migrates AzureOpenAITextToAudioService to Azure.AI.OpenAI SDK v2. ### Description 1. The new `AzureOpenAITextToAudioExecutionSettings` class is added to represent prompt execution settings for `AzureOpenAITextToAudioService`. Both `AzureOpenAITextToAudioService` classes that SK has today use the same prompt execution settings class - [OpenAITextToAudioExecutionSettings](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioExecutionSettings.cs). This is a breaking change that is tracked in the issue - https://github.com/microsoft/semantic-kernel/issues/7053, and it will be decided later whether to proceed with the change or roll it back. 2. The `ClientCore.TextToAudio.cs` class is refactored to use the new `AzureOpenAITextToAudioExecutionSettings` class. 3. The `ClientCore.TextToAudio.cs` class is refactored to decide which model id to use - the one from the prompt execution setting, the one supplied when registering the connector, or to use the deployment name if no model id is provided. This is done for backward compatibility with the existing `AzureOpenAITextToAudioService`. https://github.com/microsoft/semantic-kernel/issues/7104 4. Service collection and kernel builder extension methods are added to register the service in the DI container. 5. Unit and integration tests are added as well. --- ...AzureOpenAIKernelBuilderExtensionsTests.cs | 20 ++ ...eOpenAIServiceCollectionExtensionsTests.cs | 20 ++ .../AzureOpenAITextToAudioServiceTests.cs | 214 ++++++++++++++++++ .../Connectors.AzureOpenAI.csproj | 4 - .../Core/ClientCore.TextToAudio.cs | 22 +- .../AzureOpenAIKernelBuilderExtensions.cs | 42 ++++ .../AzureOpenAIServiceCollectionExtensions.cs | 41 ++++ .../Services/AzureOpenAITextToAudioService.cs | 26 ++- ...AzureOpenAITextToAudioExecutionSettings.cs | 130 +++++++++++ .../AzureOpenAITextToAudioTests.cs | 44 ++++ 10 files changed, 550 insertions(+), 13 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs index 7d6e09dddbb1..bfeebb320ff1 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -89,6 +90,25 @@ public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(I #endregion + #region Text to audio + + [Fact] + public void KernelBuilderAddAzureOpenAITextToAudioAddsValidService() + { + // Arrange + var sut = Kernel.CreateBuilder(); + + // Act + var service = sut.AddAzureOpenAITextToAudio("deployment-name", "https://endpoint", "api-key") + .Build() + .GetRequiredService(); + + // Assert + Assert.IsType(service); + } + + #endregion + #region Text to image [Theory] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs index 70c2bfbe385a..969241f3f23c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -9,6 +9,7 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; @@ -89,6 +90,25 @@ public void ServiceCollectionAddAzureOpenAITextEmbeddingGenerationAddsValidServi #endregion + #region Text to audio + + [Fact] + public void ServiceCollectionAddAzureOpenAITextToAudioAddsValidService() + { + // Arrange + var sut = new ServiceCollection(); + + // Act + var service = sut.AddAzureOpenAITextToAudio("deployment-name", "https://endpoint", "api-key") + .BuildServiceProvider() + .GetRequiredService(); + + // Assert + Assert.IsType(service); + } + + #endregion + #region Text to image [Theory] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs new file mode 100644 index 000000000000..b1f69110bf21 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Moq; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextToAudioServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextToAudioServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorsAddRequiredMetadata(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.Equal("model-id", service.Attributes["ModelId"]); + Assert.Equal("deployment-name", service.Attributes["DeploymentName"]); + } + + [Fact] + public void ItThrowsIfModelIdIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new AzureOpenAITextToAudioService(null!, "https://endpoint", "api-key")); + Assert.Throws(() => new AzureOpenAITextToAudioService("", "https://endpoint", "api-key")); + Assert.Throws(() => new AzureOpenAITextToAudioService(" ", "https://endpoint", "api-key")); + } + + [Fact] + public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync() + { + // Arrange + var settingsWithInvalidVoice = new AzureOpenAITextToAudioExecutionSettings(""); + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + await using var stream = new MemoryStream(new byte[] { 0x00, 0x00, 0xFF, 0x7F }); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + await Assert.ThrowsAsync(() => service.GetAudioContentsAsync("Some text", settingsWithInvalidVoice)); + } + + [Fact] + public async Task GetAudioContentByDefaultWorksCorrectlyAsync() + { + // Arrange + var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova")); + + // Assert + var audioData = result[0].Data!.Value; + Assert.False(audioData.IsEmpty); + Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); + } + + [Theory] + [InlineData("echo", "wav")] + [InlineData("fable", "opus")] + [InlineData("onyx", "flac")] + [InlineData("nova", "aac")] + [InlineData("shimmer", "pcm")] + public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string format) + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings(voice) { ResponseFormat = format }); + + // Assert + var requestBody = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent!); + Assert.NotNull(requestBody); + Assert.Equal(voice, requestBody["voice"]?.ToString()); + Assert.Equal(format, requestBody["response_format"]?.ToString()); + + var audioData = result[0].Data!.Value; + Assert.False(audioData.IsEmpty); + Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); + } + + [Fact] + public async Task GetAudioContentThrowsWhenVoiceIsNotSupportedAsync() + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("voice"))); + } + + [Fact] + public async Task GetAudioContentThrowsWhenFormatIsNotSupportedAsync() + { + // Arrange + byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" })); + } + + [Theory] + [InlineData(true, "http://local-endpoint")] + [InlineData(false, "https://endpoint")] + public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAddress, string expectedBaseAddress) + { + // Arrange + var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; + + if (useHttpClientBaseAddress) + { + this._httpClient.BaseAddress = new Uri("http://local-endpoint/path"); + } + + var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint/path", "api-key", "model-id", this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova")); + + // Assert + Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); + } + + [Theory] + [InlineData("model-1", "model-2", "deployment", "model-2")] + [InlineData("model-1", null, "deployment", "model-1")] + [InlineData(null, "model-2", "deployment", "model-2")] + [InlineData(null, null, "deployment", "deployment")] + public async Task GetAudioContentPrioritizesModelIdOverDeploymentNameAsync(string? modelInSettings, string? modelInConstructor, string deploymentName, string expectedModel) + { + // Arrange + var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; + + var service = new AzureOpenAITextToAudioService(deploymentName, "https://endpoint", "api-key", modelInConstructor, this._httpClient); + await using var stream = new MemoryStream(expectedByteArray); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act + var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova") { ModelId = modelInSettings }); + + // Assert + var requestBody = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent!); + Assert.Equal(expectedModel, requestBody?["model"]?.ToString()); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 4ee2a67b24e2..0fe5ad9344b3 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -23,9 +23,7 @@ - - @@ -34,9 +32,7 @@ - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs index d11b5ce81a26..4351b15607bd 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs @@ -19,24 +19,30 @@ internal partial class ClientCore /// /// Prompt to generate the image /// Text to Audio execution settings for the prompt + /// Azure OpenAI model id /// The to monitor for cancellation requests. The default is . /// Url of the generated image internal async Task> GetAudioContentsAsync( string prompt, PromptExecutionSettings? executionSettings, + string? modelId, CancellationToken cancellationToken) { Verify.NotNullOrWhiteSpace(prompt); - OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings?.ResponseFormat); + AzureOpenAITextToAudioExecutionSettings audioExecutionSettings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + + var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings.ResponseFormat); + SpeechGenerationOptions options = new() { ResponseFormat = responseFormat, - Speed = audioExecutionSettings?.Speed, + Speed = audioExecutionSettings.Speed, }; - ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); + var deploymentOrModel = this.GetModelId(audioExecutionSettings, modelId); + + ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(deploymentOrModel).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); return [new AudioContent(response.Value.ToArray(), mimeType)]; } @@ -64,4 +70,12 @@ private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeec "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), _ => throw new NotSupportedException($"The format '{format}' is not supported.") }; + + private string GetModelId(AzureOpenAITextToAudioExecutionSettings executionSettings, string? modelId) + { + return + !string.IsNullOrWhiteSpace(modelId) ? modelId! : + !string.IsNullOrWhiteSpace(executionSettings.ModelId) ? executionSettings.ModelId! : + this.DeploymentOrModelName; + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs index 9bb6b2f18f5d..1d995745bdde 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; #pragma warning disable IDE0039 // Use local function @@ -249,6 +250,47 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( #endregion + #region Text-to-Audio + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0001")] + public static IKernelBuilder AddAzureOpenAITextToAudio( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToAudioService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService())); + + return builder; + } + + #endregion + #region Images /// diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index bfd3e4f65fbe..4df5711603ab 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -13,6 +13,7 @@ using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; using Microsoft.SemanticKernel.TextGeneration; +using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; #pragma warning disable IDE0039 // Use local function @@ -235,6 +236,46 @@ public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( #endregion + #region Text-to-Audio + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAITextToAudio( + this IServiceCollection services, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => + new AzureOpenAITextToAudioService( + deploymentName, + endpoint, + apiKey, + modelId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService())); + } + + #endregion + #region Images /// diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs index 62e081aa72c4..b688f61263b9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextToAudio; @@ -18,9 +20,14 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAITextToAudioService : ITextToAudioService { /// - /// Azure OpenAI text-to-audio client for HTTP operations. + /// Azure OpenAI text-to-audio client. /// - private readonly AzureOpenAITextToAudioClient _client; + private readonly ClientCore _client; + + /// + /// Azure OpenAI model id. + /// + private readonly string? _modelId; /// public IReadOnlyDictionary Attributes => this._client.Attributes; @@ -47,10 +54,19 @@ public AzureOpenAITextToAudioService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._client = new(deploymentName, endpoint, apiKey, modelId, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextToAudioService))); + var url = !string.IsNullOrWhiteSpace(httpClient?.BaseAddress?.AbsoluteUri) ? httpClient!.BaseAddress!.AbsoluteUri : endpoint; + + var options = ClientCore.GetAzureOpenAIClientOptions( + httpClient, + AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#text-to-speech + + var azureOpenAIClient = new AzureOpenAIClient(new Uri(url), apiKey, options); + + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextToAudioService))); - this._client.AddAttribute(DeploymentNameKey, deploymentName); this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + + this._modelId = modelId; } /// @@ -59,5 +75,5 @@ public Task> GetAudioContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); + => this._client.GetAudioContentsAsync(text, executionSettings, this._modelId, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs new file mode 100644 index 000000000000..1552d56f26ce --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Execution settings for Azure OpenAI text-to-audio request. +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAITextToAudioExecutionSettings : PromptExecutionSettings +{ + /// + /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. + /// + [JsonPropertyName("voice")] + public string Voice + { + get => this._voice; + + set + { + this.ThrowIfFrozen(); + this._voice = value; + } + } + + /// + /// The format to audio in. Supported formats are mp3, opus, aac, and flac. + /// + [JsonPropertyName("response_format")] + public string ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. + /// + [JsonPropertyName("speed")] + public float Speed + { + get => this._speed; + + set + { + this.ThrowIfFrozen(); + this._speed = value; + } + } + + /// + /// Creates an instance of class with default voice - "alloy". + /// + public AzureOpenAITextToAudioExecutionSettings() + : this(DefaultVoice) + { + } + + /// + /// Creates an instance of class. + /// + /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. + public AzureOpenAITextToAudioExecutionSettings(string voice) + { + this._voice = voice; + } + + /// + public override PromptExecutionSettings Clone() + { + return new AzureOpenAITextToAudioExecutionSettings(this.Voice) + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Speed = this.Speed, + ResponseFormat = this.ResponseFormat + }; + } + + /// + /// Converts to derived type. + /// + /// Instance of . + /// Instance of . + public static AzureOpenAITextToAudioExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) + { + if (executionSettings is null) + { + return new AzureOpenAITextToAudioExecutionSettings(); + } + + if (executionSettings is AzureOpenAITextToAudioExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var azureOpenAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + + if (azureOpenAIExecutionSettings is not null) + { + return azureOpenAIExecutionSettings; + } + + throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAITextToAudioExecutionSettings)}", nameof(executionSettings)); + } + + #region private ================================================================================ + + private const string DefaultVoice = "alloy"; + + private float _speed = 1.0f; + private string _responseFormat = "mp3"; + private string _voice; + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs new file mode 100644 index 000000000000..372364ff21ed --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.TextToAudio; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +public sealed class AzureOpenAITextToAudioTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Fact] + public async Task AzureOpenAITextToAudioTestAsync() + { + // Arrange + AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAITextToAudio").Get(); + Assert.NotNull(azureOpenAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAITextToAudio( + azureOpenAIConfiguration.DeploymentName, + azureOpenAIConfiguration.Endpoint, + azureOpenAIConfiguration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + // Act + var result = await service.GetAudioContentAsync("The sun rises in the east and sets in the west."); + + // Assert + var audioData = result.Data!.Value; + Assert.False(audioData.IsEmpty); + } +} From 9da6154babec19a45645e86cb937ba712ac52d1a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 5 Jul 2024 11:50:43 -0700 Subject: [PATCH 035/226] Checkpoint --- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 4 +- .../Step8_OpenAIAssistant.cs | 2 +- .../Extensions/KernelFunctionExtensions.cs | 7 +- .../OpenAI/Internal/AssistantThreadActions.cs | 10 +-- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 65 +++++++++++++++---- .../src/Agents/OpenAI/OpenAIConfiguration.cs | 16 ++--- .../Azure/AddHeaderRequestPolicyTests.cs | 2 +- .../KernelFunctionExtensionsTests.cs | 4 +- .../OpenAI/OpenAIAssistantAgentTests.cs | 36 +++++----- .../OpenAIAssistantConfigurationTests.cs | 27 ++------ .../OpenAI/OpenAIAssistantDefinitionTests.cs | 9 +-- 11 files changed, 96 insertions(+), 86 deletions(-) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs index cb110e8e0bad..83282d2eee82 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -11,7 +11,7 @@ namespace Agents; /// public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) { - protected override bool ForceOpenAI => true; + protected override bool ForceOpenAI => false; [Fact] public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() @@ -20,7 +20,7 @@ public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)), // %%% MODE new() { EnableCodeInterpreter = true, // Enable code-interpreter diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index c0395c7ca26e..f282a1ee4c88 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -26,7 +26,7 @@ public async Task UseSingleOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)), // %%% MODES new() { Instructions = HostInstructions, diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs index 63eec124f0ae..97a439729ff3 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs @@ -13,9 +13,8 @@ internal static class KernelFunctionExtensions /// /// The source function /// The plugin name - /// The delimiter character /// An OpenAI tool definition - public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName, string delimiter) + public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName) { var metadata = function.Metadata; if (metadata.Parameters.Count > 0) @@ -47,10 +46,10 @@ public static FunctionToolDefinition ToToolDefinition(this KernelFunction functi required, }; - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description, BinaryData.FromObjectAsJson(spec)); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description, BinaryData.FromObjectAsJson(spec)); } - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description); } private static string ConvertType(Type? type) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 72358eb56eec..50d7d9d3b351 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System.ClientModel; using System.Collections.Generic; using System.Linq; using System.Net; @@ -20,9 +19,6 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// internal static class AssistantThreadActions { - /*AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration*/ - private const string FunctionDelimiter = "-"; - private static readonly HashSet s_messageRoles = [ AuthorRole.User, @@ -147,8 +143,6 @@ public static async IAsyncEnumerable InvokeAsync( throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); } - ToolDefinition[]? tools = [.. agent.Definition.Tools, .. agent.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; - logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); RunCreationOptions options = @@ -159,7 +153,7 @@ public static async IAsyncEnumerable InvokeAsync( //ResponseFormat = %%% }; - options.ToolsOverride.AddRange(tools); + options.ToolsOverride.AddRange(agent.Tools); // Create run ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); @@ -332,7 +326,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R { foreach (RunStepToolCall toolCall in step.Details.ToolCalls) { - var nameParts = FunctionName.Parse(toolCall.FunctionName, FunctionDelimiter); + var nameParts = FunctionName.Parse(toolCall.FunctionName); KernelArguments functionArguments = []; if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments)) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index fca25fcc0dc4..129f99642465 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -14,13 +15,14 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// public sealed class OpenAIAssistantAgent : KernelAgent { + private readonly Assistant _assistant; private readonly AssistantClient _client; private readonly string[] _channelKeys; /// /// %%% /// - public Assistant Definition { get; } + public OpenAIAssistantDefinition Definition { get; private init; } /// /// Set when the assistant has been deleted via . @@ -33,6 +35,11 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// public RunPollingConfiguration Polling { get; } = new(); + /// + /// Expose predefined tools merged with available kernel functions. + /// + internal IReadOnlyList Tools => [.. this._assistant.Tools, .. this.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))]; + /// /// Define a new . /// @@ -73,14 +80,18 @@ public static async Task CreateAsync( /// Configuration for accessing the Assistants API service, such as the api-key. /// The to monitor for cancellation requests. The default is . /// An list of objects. - public static IAsyncEnumerable ListDefinitionsAsync( + public static async IAsyncEnumerable ListDefinitionsAsync( OpenAIConfiguration config, - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create the client AssistantClient client = CreateClient(config); - return client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken); + // Query and enumerate assistant definitions + await foreach (Assistant model in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) + { + yield return CreateAssistantDefinition(model); + } } /// @@ -234,24 +245,57 @@ private OpenAIAssistantAgent( Assistant model, IEnumerable channelKeys) { - this.Definition = model; + this._assistant = model; this._client = client; this._channelKeys = channelKeys.ToArray(); - this.Description = model.Description; - this.Id = model.Id; - this.Name = model.Name; - this.Instructions = model.Instructions; + this.Definition = CreateAssistantDefinition(model); + + this.Description = this._assistant.Description; + this.Id = this._assistant.Id; + this.Name = this._assistant.Name; + this.Instructions = this._assistant.Instructions; } + private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model) + => + new() + { + Id = model.Id, + Name = model.Name, + Description = model.Description, + Instructions = model.Instructions, + EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), + VectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(), + Metadata = model.Metadata, + Model = model.Model, + }; + private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { + bool enableFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); + + ToolResources? toolResources = null; + + if (enableFileSearch) + { + toolResources = + new ToolResources() + { + FileSearch = new FileSearchToolResources() + { + VectorStoreIds = [definition.VectorStoreId!], + } + }; + } + AssistantCreationOptions assistantCreationOptions = new() { Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, + ToolResources = toolResources, // %%% ResponseFormat = // %%% Temperature = // %%% NucleusSamplingFactor = @@ -270,10 +314,9 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); } - if (!string.IsNullOrWhiteSpace(definition.VectorStoreId)) + if (enableFileSearch) { assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); - assistantCreationOptions.ToolResources.FileSearch.VectorStoreIds.Add(definition.VectorStoreId); } return assistantCreationOptions; diff --git a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs index a0c606beca06..26bdefff975d 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs @@ -20,11 +20,11 @@ internal enum OpenAIConfigurationType /// /// %%% /// - /// /// + /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, string apiKey, HttpClient? httpClient = null) => + public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) => // %%% VERIFY new() { @@ -37,11 +37,11 @@ public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, string apiKey, Ht /// /// %%% /// - /// /// + /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, TokenCredential credentials, HttpClient? httpClient = null) => + public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credentials, Uri endpoint, HttpClient? httpClient = null) => // %%% VERIFY new() { @@ -54,11 +54,11 @@ public static OpenAIConfiguration ForAzureOpenAI(Uri endpoint, TokenCredential c /// /// %%% /// - /// /// + /// /// /// - public static OpenAIConfiguration ForOpenAI(Uri endpoint, string apiKey, HttpClient? httpClient = null) => + public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) => // %%% VERIFY new() { @@ -71,12 +71,12 @@ public static OpenAIConfiguration ForOpenAI(Uri endpoint, string apiKey, HttpCli /// /// %%% /// - /// /// /// + /// /// /// - public static OpenAIConfiguration ForOpenAI(Uri endpoint, string apiKey, string organizationId, HttpClient? httpClient = null) => + public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) => // %%% VERIFY new() { diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs index 3c2945ad0fb9..0a4076055fae 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs @@ -2,7 +2,7 @@ using System.Linq; using Azure.Core; using Azure.Core.Pipeline; -using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.Agents.OpenAI; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Azure; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs index 2096012831ed..6d690e909457 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs @@ -25,11 +25,11 @@ public void VerifyKernelFunctionToFunctionTool() KernelFunction f1 = plugin[nameof(TestPlugin.TestFunction1)]; KernelFunction f2 = plugin[nameof(TestPlugin.TestFunction2)]; - FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin", "-"); + FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin"); Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition1.Description); - FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin", "-"); + FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin"); Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition2.Description); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 45d9df826c7b..a196d74dd74c 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -41,7 +41,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( this._emptyKernel, - this.CreateTestConfiguration(targetAzure: true, useVersion: true), + this.CreateTestConfiguration(targetAzure: true), definition); Assert.NotNull(agent); @@ -96,8 +96,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() { Model = "testmodel", EnableCodeInterpreter = true, - EnableFileSearch = true, - FileIds = ["#1", "#2"], + VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, }; @@ -112,9 +111,10 @@ await OpenAIAssistantAgent.CreateAsync( Assert.NotNull(agent); Assert.Equal(2, agent.Tools.Count); Assert.True(agent.Tools.OfType().Any()); - //Assert.True(agent.Tools.OfType().Any()); %%% - //Assert.NotEmpty(agent.FileIds); %%% - Assert.NotEmpty(agent.Metadata); + Assert.True(agent.Tools.OfType().Any()); + Assert.Equal("#vs", agent.Definition.VectorStoreId); + Assert.NotNull(agent.Definition.Metadata); + Assert.NotEmpty(agent.Definition.Metadata); } /// @@ -381,14 +381,10 @@ private Task CreateAgentAsync() definition); } - private OpenAIAssistantConfiguration CreateTestConfiguration(bool targetAzure = false, bool useVersion = false) - { - return new(apiKey: "fakekey", endpoint: targetAzure ? "https://localhost" : null) - { - HttpClient = this._httpClient, - //Version = useVersion ? AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview : null, %%% - }; - } + private OpenAIConfiguration CreateTestConfiguration(bool targetAzure = false) + => targetAzure ? + OpenAIConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); private void SetupResponse(HttpStatusCode statusCode, string content) { @@ -469,10 +465,14 @@ private static class ResponseContent "type": "code_interpreter" }, { - "type": "retrieval" + "type": "file_search" } ], - "file_ids": ["#1", "#2"], + "tool_resources": { + "file_search": { + "vector_store_ids": ["#vs"] + } + }, "metadata": {"a": "1"} } """; @@ -747,7 +747,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} }, { @@ -759,7 +758,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} }, { @@ -771,7 +769,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": null, "tools": [], - "file_ids": [], "metadata": {} } ], @@ -795,7 +792,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} } ], diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs index d2849f5d19fa..67672c17cd4e 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.Net.Http; -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel.Agents.OpenAI; using Xunit; @@ -18,12 +17,11 @@ public class OpenAIAssistantConfigurationTests [Fact] public void VerifyOpenAIAssistantConfigurationInitialState() { - OpenAIAssistantConfiguration config = new(apiKey: "testkey"); + OpenAIConfiguration config = OpenAIConfiguration.ForOpenAI(apiKey: "testkey"); Assert.Equal("testkey", config.ApiKey); Assert.Null(config.Endpoint); Assert.Null(config.HttpClient); - //Assert.Null(config.Version); %%% } /// @@ -34,28 +32,11 @@ public void VerifyOpenAIAssistantConfigurationAssignment() { using HttpClient client = new(); - OpenAIAssistantConfiguration config = - new(apiKey: "testkey", endpoint: "https://localhost") - { - HttpClient = client, - //Version = AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, %%% - }; + OpenAIConfiguration config = OpenAIConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); Assert.Equal("testkey", config.ApiKey); - Assert.Equal("https://localhost", config.Endpoint); + Assert.NotNull(config.Endpoint); + Assert.Equal("https://localhost/", config.Endpoint.ToString()); Assert.NotNull(config.HttpClient); - //Assert.Equal(AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, config.Version); %%% - } - - /// - /// Verify secure endpoint. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationThrows() - { - using HttpClient client = new(); - - Assert.Throws( - () => new OpenAIAssistantConfiguration(apiKey: "testkey", endpoint: "http://localhost")); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 48977edc122a..9c19372188ff 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -24,9 +24,8 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); - Assert.Null(definition.FileIds); + Assert.Null(definition.VectorStoreId); Assert.False(definition.EnableCodeInterpreter); - Assert.False(definition.EnableFileSearch); } /// @@ -43,10 +42,9 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Model = "testmodel", Instructions = "testinstructions", Description = "testdescription", - FileIds = ["id"], + VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, EnableCodeInterpreter = true, - EnableFileSearch = true, }; Assert.Equal("testid", definition.Id); @@ -54,9 +52,8 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("testmodel", definition.Model); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); + Assert.Equal("#vs", definition.VectorStoreId); Assert.Single(definition.Metadata); - Assert.Single(definition.FileIds); Assert.True(definition.EnableCodeInterpreter); - Assert.True(definition.EnableFileSearch); } } From 9b506cb31d50f7124d821b263441cf4d7ad8f61e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 5 Jul 2024 11:55:41 -0700 Subject: [PATCH 036/226] Integration test update --- .../Agents/OpenAIAssistantAgentTests.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs index 18628dbf66e1..6c4a85104d54 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs @@ -11,6 +11,9 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; +using OpenAIConfiguration = Microsoft.SemanticKernel.Agents.OpenAI.OpenAIConfiguration; +using OpenAISettings = SemanticKernel.IntegrationTests.TestSettings.OpenAIConfiguration; + namespace SemanticKernel.IntegrationTests.Agents.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. @@ -32,12 +35,12 @@ public sealed class OpenAIAssistantAgentTests [InlineData("What is the special soup?", "Clam Chowder")] public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) { - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); + OpenAISettings openAISettings = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(openAISettings); await this.ExecuteAgentAsync( - new(openAIConfiguration.ApiKey), - openAIConfiguration.ModelId, + OpenAIConfiguration.ForOpenAI(openAISettings.ApiKey), + openAISettings.ModelId, input, expectedAnswerContains); } @@ -54,14 +57,14 @@ public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAn Assert.NotNull(azureOpenAIConfiguration); await this.ExecuteAgentAsync( - new(azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.Endpoint), + OpenAIConfiguration.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), azureOpenAIConfiguration.ChatDeploymentName!, input, expectedAnswerContains); } private async Task ExecuteAgentAsync( - OpenAIAssistantConfiguration config, + OpenAIConfiguration config, string modelName, string input, string expected) From f240d2712e23f1311b23f203f8ff6a9f11d34b66 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 5 Jul 2024 16:44:51 -0700 Subject: [PATCH 037/226] Check point w/ more samples --- .../Agents/OpenAIAssistant_ChartMaker.cs | 91 +++ .../Agents/OpenAIAssistant_CodeInterpreter.cs | 10 +- .../OpenAIAssistant_FileManipulation.cs | 103 +++ .../Agents/OpenAIAssistant_FileSearch.cs | 94 +++ dotnet/samples/ConceptsV2/ConceptsV2.csproj | 3 - dotnet/samples/ConceptsV2/Resources/sales.csv | 701 ++++++++++++++++++ .../ConceptsV2/Resources/travelinfo.txt | 217 ++++++ .../Step8_OpenAIAssistant.cs | 2 +- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 1 + .../OpenAI/Internal/AssistantThreadActions.cs | 63 +- .../OpenAI/Internal/OpenAIClientFactory.cs | 30 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 21 +- .../OpenAI/OpenAIAssistantDefinition.cs | 21 +- .../src/Agents/OpenAI/OpenAIConfiguration.cs | 15 +- .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 2 +- .../OpenAI/OpenAIAssistantAgentTests.cs | 8 +- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 6 +- ...onTests.cs => OpenAIConfigurationTests.cs} | 6 +- .../Agents/OpenAIAssistantAgentTests.cs | 2 +- .../Agents/OpenAIAssistantAgentTests.cs | 2 +- 20 files changed, 1332 insertions(+), 66 deletions(-) create mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs create mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs create mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs create mode 100644 dotnet/samples/ConceptsV2/Resources/sales.csv create mode 100644 dotnet/samples/ConceptsV2/Resources/travelinfo.txt rename dotnet/src/Agents/UnitTests/OpenAI/{OpenAIAssistantConfigurationTests.cs => OpenAIConfigurationTests.cs} (91%) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs new file mode 100644 index 000000000000..70b4fe3d99fe --- /dev/null +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs @@ -0,0 +1,91 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Agents; + +/// +/// Demonstrate using code-interpreter with to +/// produce image content displays the requested charts. +/// +public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Target Open AI services. + /// + protected override bool ForceOpenAI => true; + + private const string AgentName = "ChartMaker"; + private const string AgentInstructions = "Create charts as requested without explanation."; + + [Fact] + public async Task GenerateChartWithOpenAIAssistantAgentAsync() + { + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: GetOpenAIConfiguration(), + new() + { + Instructions = AgentInstructions, + Name = AgentName, + EnableCodeInterpreter = true, + ModelName = this.Model, + }); + + // Create a chat for agent interaction. + var chat = new AgentGroupChat(); + + // Respond to user input + try + { + await InvokeAgentAsync( + """ + Display this data using a bar-chart: + + Banding Brown Pink Yellow Sum + X00000 339 433 126 898 + X00300 48 421 222 691 + X12345 16 395 352 763 + Others 23 373 156 552 + Sum 426 1622 856 2904 + """); + + await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); // %%% WHY NOT ??? + } + finally + { + await agent.DeleteAsync(); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (var message in chat.InvokeAsync(agent)) + { + if (!string.IsNullOrWhiteSpace(message.Content)) + { + Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); + } + + foreach (var fileReference in message.Items.OfType()) + { + Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); + } + } + } + } + + private OpenAIConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIConfiguration.ForOpenAI(this.ApiKey) : + OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); +} diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs index 83282d2eee82..5a5314935eca 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -20,11 +20,11 @@ public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)), // %%% MODE + config: GetOpenAIConfiguration(), new() { EnableCodeInterpreter = true, // Enable code-interpreter - Model = this.Model, + ModelName = this.Model, }); // Create a chat for agent interaction. @@ -53,4 +53,10 @@ async Task InvokeAgentAsync(string input) } } } + + private OpenAIConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIConfiguration.ForOpenAI(this.ApiKey) : + OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs new file mode 100644 index 000000000000..1124f385709d --- /dev/null +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; +using OpenAI.Files; +using Resources; + +namespace Agents; + +/// +/// Demonstrate using code-interpreter to manipulate and generate csv files with . +/// +public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Target OpenAI services. + /// + protected override bool ForceOpenAI => true; + + [Fact] + public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() + { + OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); + FileClient fileClient = rootClient.GetFileClient(); + + await using Stream fileStream = EmbeddedResource.ReadStream("sales.csv")!; + OpenAIFileInfo fileInfo = + await fileClient.UploadFileAsync( + fileStream, + "sales.csv", + FileUploadPurpose.Assistants); + + //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); %%% + //OpenAIFileReference uploadFile = + // await fileService.UploadContentAsync( + // new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), + // new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); + + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: GetOpenAIConfiguration(), + new() + { + EnableCodeInterpreter = true, // Enable code-interpreter + ModelName = this.Model, + }); + + // Create a chat for agent interaction. + AgentGroupChat chat = new(); + + // Respond to user input + try + { + await InvokeAgentAsync("Which segment had the most sales?"); + await InvokeAgentAsync("List the top 5 countries that generated the most profit."); + await InvokeAgentAsync("Create a tab delimited file report of profit by each country per month."); + } + finally + { + await agent.DeleteAsync(); + //await fileService.DeleteFileAsync(uploadFile.Id); %%% + await fileClient.DeleteFileAsync(fileInfo.Id); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + chat.AddChatMessage( + new(AuthorRole.User, content: null) + { + Items = [new TextContent(input), new FileReferenceContent(fileInfo.Id)] + }); + + Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + { + Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); + + foreach (AnnotationContent annotation in message.Items.OfType()) + { + Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); + BinaryData fileData = await fileClient.DownloadFileAsync(annotation.FileId!); + Console.WriteLine(Encoding.Default.GetString(fileData.ToArray())); + //BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); %%% + //byte[] byteContent = fileContent.Data?.ToArray() ?? []; + //Console.WriteLine(Encoding.Default.GetString(byteContent)); + } + } + } + } + + private OpenAIConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIConfiguration.ForOpenAI(this.ApiKey) : + OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); +} diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs new file mode 100644 index 000000000000..e73ad5aa9378 --- /dev/null +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs @@ -0,0 +1,94 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; +using OpenAI; +using Resources; +using OpenAI.VectorStores; + +namespace Agents; + +/// +/// Demonstrate using retrieval on . +/// +public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Retrieval tool not supported on Azure OpenAI. + /// + protected override bool ForceOpenAI => true; + + [Fact] + public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() + { + OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); + FileClient fileClient = rootClient.GetFileClient(); + + Stream fileStream = EmbeddedResource.ReadStream("travelinfo.txt")!; // %%% USING + OpenAIFileInfo fileInfo = + await fileClient.UploadFileAsync( + fileStream, + "travelinfo.txt", + FileUploadPurpose.Assistants); + + VectorStore vectorStore = + await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) + .AddFile(fileInfo.Id) + .CreateAsync(); + + OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); + + //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); %%% + //OpenAIFileReference uploadFile = + // await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), + // new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); + + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: GetOpenAIConfiguration(), + new() + { + ModelName = this.Model, + VectorStoreId = vectorStore.Id, // %%% + }); + + // Create a chat for agent interaction. + var chat = new AgentGroupChat(); + + // Respond to user input + try + { + await InvokeAgentAsync("Where did sam go?"); + await InvokeAgentAsync("When does the flight leave Seattle?"); + await InvokeAgentAsync("What is the hotel contact info at the destination?"); + } + finally + { + await agent.DeleteAsync(); + await openAIStore.DeleteAsync(); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + + await foreach (var content in chat.InvokeAsync(agent)) + { + Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + } + } + } + + private OpenAIConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIConfiguration.ForOpenAI(this.ApiKey) : + OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); +} diff --git a/dotnet/samples/ConceptsV2/ConceptsV2.csproj b/dotnet/samples/ConceptsV2/ConceptsV2.csproj index e5185dd02bc1..8eee3868170b 100644 --- a/dotnet/samples/ConceptsV2/ConceptsV2.csproj +++ b/dotnet/samples/ConceptsV2/ConceptsV2.csproj @@ -74,7 +74,4 @@ Always - - - diff --git a/dotnet/samples/ConceptsV2/Resources/sales.csv b/dotnet/samples/ConceptsV2/Resources/sales.csv new file mode 100644 index 000000000000..4a355d11bf83 --- /dev/null +++ b/dotnet/samples/ConceptsV2/Resources/sales.csv @@ -0,0 +1,701 @@ +Segment,Country,Product,Units Sold,Sale Price,Gross Sales,Discounts,Sales,COGS,Profit,Date,Month Number,Month Name,Year +Government,Canada,Carretera,1618.5,20.00,32370.00,0.00,32370.00,16185.00,16185.00,1/1/2014,1,January,2014 +Government,Germany,Carretera,1321,20.00,26420.00,0.00,26420.00,13210.00,13210.00,1/1/2014,1,January,2014 +Midmarket,France,Carretera,2178,15.00,32670.00,0.00,32670.00,21780.00,10890.00,6/1/2014,6,June,2014 +Midmarket,Germany,Carretera,888,15.00,13320.00,0.00,13320.00,8880.00,4440.00,6/1/2014,6,June,2014 +Midmarket,Mexico,Carretera,2470,15.00,37050.00,0.00,37050.00,24700.00,12350.00,6/1/2014,6,June,2014 +Government,Germany,Carretera,1513,350.00,529550.00,0.00,529550.00,393380.00,136170.00,12/1/2014,12,December,2014 +Midmarket,Germany,Montana,921,15.00,13815.00,0.00,13815.00,9210.00,4605.00,3/1/2014,3,March,2014 +Channel Partners,Canada,Montana,2518,12.00,30216.00,0.00,30216.00,7554.00,22662.00,6/1/2014,6,June,2014 +Government,France,Montana,1899,20.00,37980.00,0.00,37980.00,18990.00,18990.00,6/1/2014,6,June,2014 +Channel Partners,Germany,Montana,1545,12.00,18540.00,0.00,18540.00,4635.00,13905.00,6/1/2014,6,June,2014 +Midmarket,Mexico,Montana,2470,15.00,37050.00,0.00,37050.00,24700.00,12350.00,6/1/2014,6,June,2014 +Enterprise,Canada,Montana,2665.5,125.00,333187.50,0.00,333187.50,319860.00,13327.50,7/1/2014,7,July,2014 +Small Business,Mexico,Montana,958,300.00,287400.00,0.00,287400.00,239500.00,47900.00,8/1/2014,8,August,2014 +Government,Germany,Montana,2146,7.00,15022.00,0.00,15022.00,10730.00,4292.00,9/1/2014,9,September,2014 +Enterprise,Canada,Montana,345,125.00,43125.00,0.00,43125.00,41400.00,1725.00,10/1/2013,10,October,2013 +Midmarket,United States of America,Montana,615,15.00,9225.00,0.00,9225.00,6150.00,3075.00,12/1/2014,12,December,2014 +Government,Canada,Paseo,292,20.00,5840.00,0.00,5840.00,2920.00,2920.00,2/1/2014,2,February,2014 +Midmarket,Mexico,Paseo,974,15.00,14610.00,0.00,14610.00,9740.00,4870.00,2/1/2014,2,February,2014 +Channel Partners,Canada,Paseo,2518,12.00,30216.00,0.00,30216.00,7554.00,22662.00,6/1/2014,6,June,2014 +Government,Germany,Paseo,1006,350.00,352100.00,0.00,352100.00,261560.00,90540.00,6/1/2014,6,June,2014 +Channel Partners,Germany,Paseo,367,12.00,4404.00,0.00,4404.00,1101.00,3303.00,7/1/2014,7,July,2014 +Government,Mexico,Paseo,883,7.00,6181.00,0.00,6181.00,4415.00,1766.00,8/1/2014,8,August,2014 +Midmarket,France,Paseo,549,15.00,8235.00,0.00,8235.00,5490.00,2745.00,9/1/2013,9,September,2013 +Small Business,Mexico,Paseo,788,300.00,236400.00,0.00,236400.00,197000.00,39400.00,9/1/2013,9,September,2013 +Midmarket,Mexico,Paseo,2472,15.00,37080.00,0.00,37080.00,24720.00,12360.00,9/1/2014,9,September,2014 +Government,United States of America,Paseo,1143,7.00,8001.00,0.00,8001.00,5715.00,2286.00,10/1/2014,10,October,2014 +Government,Canada,Paseo,1725,350.00,603750.00,0.00,603750.00,448500.00,155250.00,11/1/2013,11,November,2013 +Channel Partners,United States of America,Paseo,912,12.00,10944.00,0.00,10944.00,2736.00,8208.00,11/1/2013,11,November,2013 +Midmarket,Canada,Paseo,2152,15.00,32280.00,0.00,32280.00,21520.00,10760.00,12/1/2013,12,December,2013 +Government,Canada,Paseo,1817,20.00,36340.00,0.00,36340.00,18170.00,18170.00,12/1/2014,12,December,2014 +Government,Germany,Paseo,1513,350.00,529550.00,0.00,529550.00,393380.00,136170.00,12/1/2014,12,December,2014 +Government,Mexico,Velo,1493,7.00,10451.00,0.00,10451.00,7465.00,2986.00,1/1/2014,1,January,2014 +Enterprise,France,Velo,1804,125.00,225500.00,0.00,225500.00,216480.00,9020.00,2/1/2014,2,February,2014 +Channel Partners,Germany,Velo,2161,12.00,25932.00,0.00,25932.00,6483.00,19449.00,3/1/2014,3,March,2014 +Government,Germany,Velo,1006,350.00,352100.00,0.00,352100.00,261560.00,90540.00,6/1/2014,6,June,2014 +Channel Partners,Germany,Velo,1545,12.00,18540.00,0.00,18540.00,4635.00,13905.00,6/1/2014,6,June,2014 +Enterprise,United States of America,Velo,2821,125.00,352625.00,0.00,352625.00,338520.00,14105.00,8/1/2014,8,August,2014 +Enterprise,Canada,Velo,345,125.00,43125.00,0.00,43125.00,41400.00,1725.00,10/1/2013,10,October,2013 +Small Business,Canada,VTT,2001,300.00,600300.00,0.00,600300.00,500250.00,100050.00,2/1/2014,2,February,2014 +Channel Partners,Germany,VTT,2838,12.00,34056.00,0.00,34056.00,8514.00,25542.00,4/1/2014,4,April,2014 +Midmarket,France,VTT,2178,15.00,32670.00,0.00,32670.00,21780.00,10890.00,6/1/2014,6,June,2014 +Midmarket,Germany,VTT,888,15.00,13320.00,0.00,13320.00,8880.00,4440.00,6/1/2014,6,June,2014 +Government,France,VTT,1527,350.00,534450.00,0.00,534450.00,397020.00,137430.00,9/1/2013,9,September,2013 +Small Business,France,VTT,2151,300.00,645300.00,0.00,645300.00,537750.00,107550.00,9/1/2014,9,September,2014 +Government,Canada,VTT,1817,20.00,36340.00,0.00,36340.00,18170.00,18170.00,12/1/2014,12,December,2014 +Government,France,Amarilla,2750,350.00,962500.00,0.00,962500.00,715000.00,247500.00,2/1/2014,2,February,2014 +Channel Partners,United States of America,Amarilla,1953,12.00,23436.00,0.00,23436.00,5859.00,17577.00,4/1/2014,4,April,2014 +Enterprise,Germany,Amarilla,4219.5,125.00,527437.50,0.00,527437.50,506340.00,21097.50,4/1/2014,4,April,2014 +Government,France,Amarilla,1899,20.00,37980.00,0.00,37980.00,18990.00,18990.00,6/1/2014,6,June,2014 +Government,Germany,Amarilla,1686,7.00,11802.00,0.00,11802.00,8430.00,3372.00,7/1/2014,7,July,2014 +Channel Partners,United States of America,Amarilla,2141,12.00,25692.00,0.00,25692.00,6423.00,19269.00,8/1/2014,8,August,2014 +Government,United States of America,Amarilla,1143,7.00,8001.00,0.00,8001.00,5715.00,2286.00,10/1/2014,10,October,2014 +Midmarket,United States of America,Amarilla,615,15.00,9225.00,0.00,9225.00,6150.00,3075.00,12/1/2014,12,December,2014 +Government,France,Paseo,3945,7.00,27615.00,276.15,27338.85,19725.00,7613.85,1/1/2014,1,January,2014 +Midmarket,France,Paseo,2296,15.00,34440.00,344.40,34095.60,22960.00,11135.60,2/1/2014,2,February,2014 +Government,France,Paseo,1030,7.00,7210.00,72.10,7137.90,5150.00,1987.90,5/1/2014,5,May,2014 +Government,France,Velo,639,7.00,4473.00,44.73,4428.27,3195.00,1233.27,11/1/2014,11,November,2014 +Government,Canada,VTT,1326,7.00,9282.00,92.82,9189.18,6630.00,2559.18,3/1/2014,3,March,2014 +Channel Partners,United States of America,Carretera,1858,12.00,22296.00,222.96,22073.04,5574.00,16499.04,2/1/2014,2,February,2014 +Government,Mexico,Carretera,1210,350.00,423500.00,4235.00,419265.00,314600.00,104665.00,3/1/2014,3,March,2014 +Government,United States of America,Carretera,2529,7.00,17703.00,177.03,17525.97,12645.00,4880.97,7/1/2014,7,July,2014 +Channel Partners,Canada,Carretera,1445,12.00,17340.00,173.40,17166.60,4335.00,12831.60,9/1/2014,9,September,2014 +Enterprise,United States of America,Carretera,330,125.00,41250.00,412.50,40837.50,39600.00,1237.50,9/1/2013,9,September,2013 +Channel Partners,France,Carretera,2671,12.00,32052.00,320.52,31731.48,8013.00,23718.48,9/1/2014,9,September,2014 +Channel Partners,Germany,Carretera,766,12.00,9192.00,91.92,9100.08,2298.00,6802.08,10/1/2013,10,October,2013 +Small Business,Mexico,Carretera,494,300.00,148200.00,1482.00,146718.00,123500.00,23218.00,10/1/2013,10,October,2013 +Government,Mexico,Carretera,1397,350.00,488950.00,4889.50,484060.50,363220.00,120840.50,10/1/2014,10,October,2014 +Government,France,Carretera,2155,350.00,754250.00,7542.50,746707.50,560300.00,186407.50,12/1/2014,12,December,2014 +Midmarket,Mexico,Montana,2214,15.00,33210.00,332.10,32877.90,22140.00,10737.90,3/1/2014,3,March,2014 +Small Business,United States of America,Montana,2301,300.00,690300.00,6903.00,683397.00,575250.00,108147.00,4/1/2014,4,April,2014 +Government,France,Montana,1375.5,20.00,27510.00,275.10,27234.90,13755.00,13479.90,7/1/2014,7,July,2014 +Government,Canada,Montana,1830,7.00,12810.00,128.10,12681.90,9150.00,3531.90,8/1/2014,8,August,2014 +Small Business,United States of America,Montana,2498,300.00,749400.00,7494.00,741906.00,624500.00,117406.00,9/1/2013,9,September,2013 +Enterprise,United States of America,Montana,663,125.00,82875.00,828.75,82046.25,79560.00,2486.25,10/1/2013,10,October,2013 +Midmarket,United States of America,Paseo,1514,15.00,22710.00,227.10,22482.90,15140.00,7342.90,2/1/2014,2,February,2014 +Government,United States of America,Paseo,4492.5,7.00,31447.50,314.48,31133.03,22462.50,8670.53,4/1/2014,4,April,2014 +Enterprise,United States of America,Paseo,727,125.00,90875.00,908.75,89966.25,87240.00,2726.25,6/1/2014,6,June,2014 +Enterprise,France,Paseo,787,125.00,98375.00,983.75,97391.25,94440.00,2951.25,6/1/2014,6,June,2014 +Enterprise,Mexico,Paseo,1823,125.00,227875.00,2278.75,225596.25,218760.00,6836.25,7/1/2014,7,July,2014 +Midmarket,Germany,Paseo,747,15.00,11205.00,112.05,11092.95,7470.00,3622.95,9/1/2014,9,September,2014 +Channel Partners,Germany,Paseo,766,12.00,9192.00,91.92,9100.08,2298.00,6802.08,10/1/2013,10,October,2013 +Small Business,United States of America,Paseo,2905,300.00,871500.00,8715.00,862785.00,726250.00,136535.00,11/1/2014,11,November,2014 +Government,France,Paseo,2155,350.00,754250.00,7542.50,746707.50,560300.00,186407.50,12/1/2014,12,December,2014 +Government,France,Velo,3864,20.00,77280.00,772.80,76507.20,38640.00,37867.20,4/1/2014,4,April,2014 +Government,Mexico,Velo,362,7.00,2534.00,25.34,2508.66,1810.00,698.66,5/1/2014,5,May,2014 +Enterprise,Canada,Velo,923,125.00,115375.00,1153.75,114221.25,110760.00,3461.25,8/1/2014,8,August,2014 +Enterprise,United States of America,Velo,663,125.00,82875.00,828.75,82046.25,79560.00,2486.25,10/1/2013,10,October,2013 +Government,Canada,Velo,2092,7.00,14644.00,146.44,14497.56,10460.00,4037.56,11/1/2013,11,November,2013 +Government,Germany,VTT,263,7.00,1841.00,18.41,1822.59,1315.00,507.59,3/1/2014,3,March,2014 +Government,Canada,VTT,943.5,350.00,330225.00,3302.25,326922.75,245310.00,81612.75,4/1/2014,4,April,2014 +Enterprise,United States of America,VTT,727,125.00,90875.00,908.75,89966.25,87240.00,2726.25,6/1/2014,6,June,2014 +Enterprise,France,VTT,787,125.00,98375.00,983.75,97391.25,94440.00,2951.25,6/1/2014,6,June,2014 +Small Business,Germany,VTT,986,300.00,295800.00,2958.00,292842.00,246500.00,46342.00,9/1/2014,9,September,2014 +Small Business,Mexico,VTT,494,300.00,148200.00,1482.00,146718.00,123500.00,23218.00,10/1/2013,10,October,2013 +Government,Mexico,VTT,1397,350.00,488950.00,4889.50,484060.50,363220.00,120840.50,10/1/2014,10,October,2014 +Enterprise,France,VTT,1744,125.00,218000.00,2180.00,215820.00,209280.00,6540.00,11/1/2014,11,November,2014 +Channel Partners,United States of America,Amarilla,1989,12.00,23868.00,238.68,23629.32,5967.00,17662.32,9/1/2013,9,September,2013 +Midmarket,France,Amarilla,321,15.00,4815.00,48.15,4766.85,3210.00,1556.85,11/1/2013,11,November,2013 +Enterprise,Canada,Carretera,742.5,125.00,92812.50,1856.25,90956.25,89100.00,1856.25,4/1/2014,4,April,2014 +Channel Partners,Canada,Carretera,1295,12.00,15540.00,310.80,15229.20,3885.00,11344.20,10/1/2014,10,October,2014 +Small Business,Germany,Carretera,214,300.00,64200.00,1284.00,62916.00,53500.00,9416.00,10/1/2013,10,October,2013 +Government,France,Carretera,2145,7.00,15015.00,300.30,14714.70,10725.00,3989.70,11/1/2013,11,November,2013 +Government,Canada,Carretera,2852,350.00,998200.00,19964.00,978236.00,741520.00,236716.00,12/1/2014,12,December,2014 +Channel Partners,United States of America,Montana,1142,12.00,13704.00,274.08,13429.92,3426.00,10003.92,6/1/2014,6,June,2014 +Government,United States of America,Montana,1566,20.00,31320.00,626.40,30693.60,15660.00,15033.60,10/1/2014,10,October,2014 +Channel Partners,Mexico,Montana,690,12.00,8280.00,165.60,8114.40,2070.00,6044.40,11/1/2014,11,November,2014 +Enterprise,Mexico,Montana,1660,125.00,207500.00,4150.00,203350.00,199200.00,4150.00,11/1/2013,11,November,2013 +Midmarket,Canada,Paseo,2363,15.00,35445.00,708.90,34736.10,23630.00,11106.10,2/1/2014,2,February,2014 +Small Business,France,Paseo,918,300.00,275400.00,5508.00,269892.00,229500.00,40392.00,5/1/2014,5,May,2014 +Small Business,Germany,Paseo,1728,300.00,518400.00,10368.00,508032.00,432000.00,76032.00,5/1/2014,5,May,2014 +Channel Partners,United States of America,Paseo,1142,12.00,13704.00,274.08,13429.92,3426.00,10003.92,6/1/2014,6,June,2014 +Enterprise,Mexico,Paseo,662,125.00,82750.00,1655.00,81095.00,79440.00,1655.00,6/1/2014,6,June,2014 +Channel Partners,Canada,Paseo,1295,12.00,15540.00,310.80,15229.20,3885.00,11344.20,10/1/2014,10,October,2014 +Enterprise,Germany,Paseo,809,125.00,101125.00,2022.50,99102.50,97080.00,2022.50,10/1/2013,10,October,2013 +Enterprise,Mexico,Paseo,2145,125.00,268125.00,5362.50,262762.50,257400.00,5362.50,10/1/2013,10,October,2013 +Channel Partners,France,Paseo,1785,12.00,21420.00,428.40,20991.60,5355.00,15636.60,11/1/2013,11,November,2013 +Small Business,Canada,Paseo,1916,300.00,574800.00,11496.00,563304.00,479000.00,84304.00,12/1/2014,12,December,2014 +Government,Canada,Paseo,2852,350.00,998200.00,19964.00,978236.00,741520.00,236716.00,12/1/2014,12,December,2014 +Enterprise,Canada,Paseo,2729,125.00,341125.00,6822.50,334302.50,327480.00,6822.50,12/1/2014,12,December,2014 +Midmarket,United States of America,Paseo,1925,15.00,28875.00,577.50,28297.50,19250.00,9047.50,12/1/2013,12,December,2013 +Government,United States of America,Paseo,2013,7.00,14091.00,281.82,13809.18,10065.00,3744.18,12/1/2013,12,December,2013 +Channel Partners,France,Paseo,1055,12.00,12660.00,253.20,12406.80,3165.00,9241.80,12/1/2014,12,December,2014 +Channel Partners,Mexico,Paseo,1084,12.00,13008.00,260.16,12747.84,3252.00,9495.84,12/1/2014,12,December,2014 +Government,United States of America,Velo,1566,20.00,31320.00,626.40,30693.60,15660.00,15033.60,10/1/2014,10,October,2014 +Government,Germany,Velo,2966,350.00,1038100.00,20762.00,1017338.00,771160.00,246178.00,10/1/2013,10,October,2013 +Government,Germany,Velo,2877,350.00,1006950.00,20139.00,986811.00,748020.00,238791.00,10/1/2014,10,October,2014 +Enterprise,Germany,Velo,809,125.00,101125.00,2022.50,99102.50,97080.00,2022.50,10/1/2013,10,October,2013 +Enterprise,Mexico,Velo,2145,125.00,268125.00,5362.50,262762.50,257400.00,5362.50,10/1/2013,10,October,2013 +Channel Partners,France,Velo,1055,12.00,12660.00,253.20,12406.80,3165.00,9241.80,12/1/2014,12,December,2014 +Government,Mexico,Velo,544,20.00,10880.00,217.60,10662.40,5440.00,5222.40,12/1/2013,12,December,2013 +Channel Partners,Mexico,Velo,1084,12.00,13008.00,260.16,12747.84,3252.00,9495.84,12/1/2014,12,December,2014 +Enterprise,Mexico,VTT,662,125.00,82750.00,1655.00,81095.00,79440.00,1655.00,6/1/2014,6,June,2014 +Small Business,Germany,VTT,214,300.00,64200.00,1284.00,62916.00,53500.00,9416.00,10/1/2013,10,October,2013 +Government,Germany,VTT,2877,350.00,1006950.00,20139.00,986811.00,748020.00,238791.00,10/1/2014,10,October,2014 +Enterprise,Canada,VTT,2729,125.00,341125.00,6822.50,334302.50,327480.00,6822.50,12/1/2014,12,December,2014 +Government,United States of America,VTT,266,350.00,93100.00,1862.00,91238.00,69160.00,22078.00,12/1/2013,12,December,2013 +Government,Mexico,VTT,1940,350.00,679000.00,13580.00,665420.00,504400.00,161020.00,12/1/2013,12,December,2013 +Small Business,Germany,Amarilla,259,300.00,77700.00,1554.00,76146.00,64750.00,11396.00,3/1/2014,3,March,2014 +Small Business,Mexico,Amarilla,1101,300.00,330300.00,6606.00,323694.00,275250.00,48444.00,3/1/2014,3,March,2014 +Enterprise,Germany,Amarilla,2276,125.00,284500.00,5690.00,278810.00,273120.00,5690.00,5/1/2014,5,May,2014 +Government,Germany,Amarilla,2966,350.00,1038100.00,20762.00,1017338.00,771160.00,246178.00,10/1/2013,10,October,2013 +Government,United States of America,Amarilla,1236,20.00,24720.00,494.40,24225.60,12360.00,11865.60,11/1/2014,11,November,2014 +Government,France,Amarilla,941,20.00,18820.00,376.40,18443.60,9410.00,9033.60,11/1/2014,11,November,2014 +Small Business,Canada,Amarilla,1916,300.00,574800.00,11496.00,563304.00,479000.00,84304.00,12/1/2014,12,December,2014 +Enterprise,France,Carretera,4243.5,125.00,530437.50,15913.13,514524.38,509220.00,5304.38,4/1/2014,4,April,2014 +Government,Germany,Carretera,2580,20.00,51600.00,1548.00,50052.00,25800.00,24252.00,4/1/2014,4,April,2014 +Small Business,Germany,Carretera,689,300.00,206700.00,6201.00,200499.00,172250.00,28249.00,6/1/2014,6,June,2014 +Channel Partners,United States of America,Carretera,1947,12.00,23364.00,700.92,22663.08,5841.00,16822.08,9/1/2014,9,September,2014 +Channel Partners,Canada,Carretera,908,12.00,10896.00,326.88,10569.12,2724.00,7845.12,12/1/2013,12,December,2013 +Government,Germany,Montana,1958,7.00,13706.00,411.18,13294.82,9790.00,3504.82,2/1/2014,2,February,2014 +Channel Partners,France,Montana,1901,12.00,22812.00,684.36,22127.64,5703.00,16424.64,6/1/2014,6,June,2014 +Government,France,Montana,544,7.00,3808.00,114.24,3693.76,2720.00,973.76,9/1/2014,9,September,2014 +Government,Germany,Montana,1797,350.00,628950.00,18868.50,610081.50,467220.00,142861.50,9/1/2013,9,September,2013 +Enterprise,France,Montana,1287,125.00,160875.00,4826.25,156048.75,154440.00,1608.75,12/1/2014,12,December,2014 +Enterprise,Germany,Montana,1706,125.00,213250.00,6397.50,206852.50,204720.00,2132.50,12/1/2014,12,December,2014 +Small Business,France,Paseo,2434.5,300.00,730350.00,21910.50,708439.50,608625.00,99814.50,1/1/2014,1,January,2014 +Enterprise,Canada,Paseo,1774,125.00,221750.00,6652.50,215097.50,212880.00,2217.50,3/1/2014,3,March,2014 +Channel Partners,France,Paseo,1901,12.00,22812.00,684.36,22127.64,5703.00,16424.64,6/1/2014,6,June,2014 +Small Business,Germany,Paseo,689,300.00,206700.00,6201.00,200499.00,172250.00,28249.00,6/1/2014,6,June,2014 +Enterprise,Germany,Paseo,1570,125.00,196250.00,5887.50,190362.50,188400.00,1962.50,6/1/2014,6,June,2014 +Channel Partners,United States of America,Paseo,1369.5,12.00,16434.00,493.02,15940.98,4108.50,11832.48,7/1/2014,7,July,2014 +Enterprise,Canada,Paseo,2009,125.00,251125.00,7533.75,243591.25,241080.00,2511.25,10/1/2014,10,October,2014 +Midmarket,Germany,Paseo,1945,15.00,29175.00,875.25,28299.75,19450.00,8849.75,10/1/2013,10,October,2013 +Enterprise,France,Paseo,1287,125.00,160875.00,4826.25,156048.75,154440.00,1608.75,12/1/2014,12,December,2014 +Enterprise,Germany,Paseo,1706,125.00,213250.00,6397.50,206852.50,204720.00,2132.50,12/1/2014,12,December,2014 +Enterprise,Canada,Velo,2009,125.00,251125.00,7533.75,243591.25,241080.00,2511.25,10/1/2014,10,October,2014 +Small Business,United States of America,VTT,2844,300.00,853200.00,25596.00,827604.00,711000.00,116604.00,2/1/2014,2,February,2014 +Channel Partners,Mexico,VTT,1916,12.00,22992.00,689.76,22302.24,5748.00,16554.24,4/1/2014,4,April,2014 +Enterprise,Germany,VTT,1570,125.00,196250.00,5887.50,190362.50,188400.00,1962.50,6/1/2014,6,June,2014 +Small Business,Canada,VTT,1874,300.00,562200.00,16866.00,545334.00,468500.00,76834.00,8/1/2014,8,August,2014 +Government,Mexico,VTT,1642,350.00,574700.00,17241.00,557459.00,426920.00,130539.00,8/1/2014,8,August,2014 +Midmarket,Germany,VTT,1945,15.00,29175.00,875.25,28299.75,19450.00,8849.75,10/1/2013,10,October,2013 +Government,Canada,Carretera,831,20.00,16620.00,498.60,16121.40,8310.00,7811.40,5/1/2014,5,May,2014 +Government,Mexico,Paseo,1760,7.00,12320.00,369.60,11950.40,8800.00,3150.40,9/1/2013,9,September,2013 +Government,Canada,Velo,3850.5,20.00,77010.00,2310.30,74699.70,38505.00,36194.70,4/1/2014,4,April,2014 +Channel Partners,Germany,VTT,2479,12.00,29748.00,892.44,28855.56,7437.00,21418.56,1/1/2014,1,January,2014 +Midmarket,Mexico,Montana,2031,15.00,30465.00,1218.60,29246.40,20310.00,8936.40,10/1/2014,10,October,2014 +Midmarket,Mexico,Paseo,2031,15.00,30465.00,1218.60,29246.40,20310.00,8936.40,10/1/2014,10,October,2014 +Midmarket,France,Paseo,2261,15.00,33915.00,1356.60,32558.40,22610.00,9948.40,12/1/2013,12,December,2013 +Government,United States of America,Velo,736,20.00,14720.00,588.80,14131.20,7360.00,6771.20,9/1/2013,9,September,2013 +Government,Canada,Carretera,2851,7.00,19957.00,798.28,19158.72,14255.00,4903.72,10/1/2013,10,October,2013 +Small Business,Germany,Carretera,2021,300.00,606300.00,24252.00,582048.00,505250.00,76798.00,10/1/2014,10,October,2014 +Government,United States of America,Carretera,274,350.00,95900.00,3836.00,92064.00,71240.00,20824.00,12/1/2014,12,December,2014 +Midmarket,Canada,Montana,1967,15.00,29505.00,1180.20,28324.80,19670.00,8654.80,3/1/2014,3,March,2014 +Small Business,Germany,Montana,1859,300.00,557700.00,22308.00,535392.00,464750.00,70642.00,8/1/2014,8,August,2014 +Government,Canada,Montana,2851,7.00,19957.00,798.28,19158.72,14255.00,4903.72,10/1/2013,10,October,2013 +Small Business,Germany,Montana,2021,300.00,606300.00,24252.00,582048.00,505250.00,76798.00,10/1/2014,10,October,2014 +Enterprise,Mexico,Montana,1138,125.00,142250.00,5690.00,136560.00,136560.00,0.00,12/1/2014,12,December,2014 +Government,Canada,Paseo,4251,7.00,29757.00,1190.28,28566.72,21255.00,7311.72,1/1/2014,1,January,2014 +Enterprise,Germany,Paseo,795,125.00,99375.00,3975.00,95400.00,95400.00,0.00,3/1/2014,3,March,2014 +Small Business,Germany,Paseo,1414.5,300.00,424350.00,16974.00,407376.00,353625.00,53751.00,4/1/2014,4,April,2014 +Small Business,United States of America,Paseo,2918,300.00,875400.00,35016.00,840384.00,729500.00,110884.00,5/1/2014,5,May,2014 +Government,United States of America,Paseo,3450,350.00,1207500.00,48300.00,1159200.00,897000.00,262200.00,7/1/2014,7,July,2014 +Enterprise,France,Paseo,2988,125.00,373500.00,14940.00,358560.00,358560.00,0.00,7/1/2014,7,July,2014 +Midmarket,Canada,Paseo,218,15.00,3270.00,130.80,3139.20,2180.00,959.20,9/1/2014,9,September,2014 +Government,Canada,Paseo,2074,20.00,41480.00,1659.20,39820.80,20740.00,19080.80,9/1/2014,9,September,2014 +Government,United States of America,Paseo,1056,20.00,21120.00,844.80,20275.20,10560.00,9715.20,9/1/2014,9,September,2014 +Midmarket,United States of America,Paseo,671,15.00,10065.00,402.60,9662.40,6710.00,2952.40,10/1/2013,10,October,2013 +Midmarket,Mexico,Paseo,1514,15.00,22710.00,908.40,21801.60,15140.00,6661.60,10/1/2013,10,October,2013 +Government,United States of America,Paseo,274,350.00,95900.00,3836.00,92064.00,71240.00,20824.00,12/1/2014,12,December,2014 +Enterprise,Mexico,Paseo,1138,125.00,142250.00,5690.00,136560.00,136560.00,0.00,12/1/2014,12,December,2014 +Channel Partners,United States of America,Velo,1465,12.00,17580.00,703.20,16876.80,4395.00,12481.80,3/1/2014,3,March,2014 +Government,Canada,Velo,2646,20.00,52920.00,2116.80,50803.20,26460.00,24343.20,9/1/2013,9,September,2013 +Government,France,Velo,2177,350.00,761950.00,30478.00,731472.00,566020.00,165452.00,10/1/2014,10,October,2014 +Channel Partners,France,VTT,866,12.00,10392.00,415.68,9976.32,2598.00,7378.32,5/1/2014,5,May,2014 +Government,United States of America,VTT,349,350.00,122150.00,4886.00,117264.00,90740.00,26524.00,9/1/2013,9,September,2013 +Government,France,VTT,2177,350.00,761950.00,30478.00,731472.00,566020.00,165452.00,10/1/2014,10,October,2014 +Midmarket,Mexico,VTT,1514,15.00,22710.00,908.40,21801.60,15140.00,6661.60,10/1/2013,10,October,2013 +Government,Mexico,Amarilla,1865,350.00,652750.00,26110.00,626640.00,484900.00,141740.00,2/1/2014,2,February,2014 +Enterprise,Mexico,Amarilla,1074,125.00,134250.00,5370.00,128880.00,128880.00,0.00,4/1/2014,4,April,2014 +Government,Germany,Amarilla,1907,350.00,667450.00,26698.00,640752.00,495820.00,144932.00,9/1/2014,9,September,2014 +Midmarket,United States of America,Amarilla,671,15.00,10065.00,402.60,9662.40,6710.00,2952.40,10/1/2013,10,October,2013 +Government,Canada,Amarilla,1778,350.00,622300.00,24892.00,597408.00,462280.00,135128.00,12/1/2013,12,December,2013 +Government,Germany,Montana,1159,7.00,8113.00,405.65,7707.35,5795.00,1912.35,10/1/2013,10,October,2013 +Government,Germany,Paseo,1372,7.00,9604.00,480.20,9123.80,6860.00,2263.80,1/1/2014,1,January,2014 +Government,Canada,Paseo,2349,7.00,16443.00,822.15,15620.85,11745.00,3875.85,9/1/2013,9,September,2013 +Government,Mexico,Paseo,2689,7.00,18823.00,941.15,17881.85,13445.00,4436.85,10/1/2014,10,October,2014 +Channel Partners,Canada,Paseo,2431,12.00,29172.00,1458.60,27713.40,7293.00,20420.40,12/1/2014,12,December,2014 +Channel Partners,Canada,Velo,2431,12.00,29172.00,1458.60,27713.40,7293.00,20420.40,12/1/2014,12,December,2014 +Government,Mexico,VTT,2689,7.00,18823.00,941.15,17881.85,13445.00,4436.85,10/1/2014,10,October,2014 +Government,Mexico,Amarilla,1683,7.00,11781.00,589.05,11191.95,8415.00,2776.95,7/1/2014,7,July,2014 +Channel Partners,Mexico,Amarilla,1123,12.00,13476.00,673.80,12802.20,3369.00,9433.20,8/1/2014,8,August,2014 +Government,Germany,Amarilla,1159,7.00,8113.00,405.65,7707.35,5795.00,1912.35,10/1/2013,10,October,2013 +Channel Partners,France,Carretera,1865,12.00,22380.00,1119.00,21261.00,5595.00,15666.00,2/1/2014,2,February,2014 +Channel Partners,Germany,Carretera,1116,12.00,13392.00,669.60,12722.40,3348.00,9374.40,2/1/2014,2,February,2014 +Government,France,Carretera,1563,20.00,31260.00,1563.00,29697.00,15630.00,14067.00,5/1/2014,5,May,2014 +Small Business,United States of America,Carretera,991,300.00,297300.00,14865.00,282435.00,247750.00,34685.00,6/1/2014,6,June,2014 +Government,Germany,Carretera,1016,7.00,7112.00,355.60,6756.40,5080.00,1676.40,11/1/2013,11,November,2013 +Midmarket,Mexico,Carretera,2791,15.00,41865.00,2093.25,39771.75,27910.00,11861.75,11/1/2014,11,November,2014 +Government,United States of America,Carretera,570,7.00,3990.00,199.50,3790.50,2850.00,940.50,12/1/2014,12,December,2014 +Government,France,Carretera,2487,7.00,17409.00,870.45,16538.55,12435.00,4103.55,12/1/2014,12,December,2014 +Government,France,Montana,1384.5,350.00,484575.00,24228.75,460346.25,359970.00,100376.25,1/1/2014,1,January,2014 +Enterprise,United States of America,Montana,3627,125.00,453375.00,22668.75,430706.25,435240.00,-4533.75,7/1/2014,7,July,2014 +Government,Mexico,Montana,720,350.00,252000.00,12600.00,239400.00,187200.00,52200.00,9/1/2013,9,September,2013 +Channel Partners,Germany,Montana,2342,12.00,28104.00,1405.20,26698.80,7026.00,19672.80,11/1/2014,11,November,2014 +Small Business,Mexico,Montana,1100,300.00,330000.00,16500.00,313500.00,275000.00,38500.00,12/1/2013,12,December,2013 +Government,France,Paseo,1303,20.00,26060.00,1303.00,24757.00,13030.00,11727.00,2/1/2014,2,February,2014 +Enterprise,United States of America,Paseo,2992,125.00,374000.00,18700.00,355300.00,359040.00,-3740.00,3/1/2014,3,March,2014 +Enterprise,France,Paseo,2385,125.00,298125.00,14906.25,283218.75,286200.00,-2981.25,3/1/2014,3,March,2014 +Small Business,Mexico,Paseo,1607,300.00,482100.00,24105.00,457995.00,401750.00,56245.00,4/1/2014,4,April,2014 +Government,United States of America,Paseo,2327,7.00,16289.00,814.45,15474.55,11635.00,3839.55,5/1/2014,5,May,2014 +Small Business,United States of America,Paseo,991,300.00,297300.00,14865.00,282435.00,247750.00,34685.00,6/1/2014,6,June,2014 +Government,United States of America,Paseo,602,350.00,210700.00,10535.00,200165.00,156520.00,43645.00,6/1/2014,6,June,2014 +Midmarket,France,Paseo,2620,15.00,39300.00,1965.00,37335.00,26200.00,11135.00,9/1/2014,9,September,2014 +Government,Canada,Paseo,1228,350.00,429800.00,21490.00,408310.00,319280.00,89030.00,10/1/2013,10,October,2013 +Government,Canada,Paseo,1389,20.00,27780.00,1389.00,26391.00,13890.00,12501.00,10/1/2013,10,October,2013 +Enterprise,United States of America,Paseo,861,125.00,107625.00,5381.25,102243.75,103320.00,-1076.25,10/1/2014,10,October,2014 +Enterprise,France,Paseo,704,125.00,88000.00,4400.00,83600.00,84480.00,-880.00,10/1/2013,10,October,2013 +Government,Canada,Paseo,1802,20.00,36040.00,1802.00,34238.00,18020.00,16218.00,12/1/2013,12,December,2013 +Government,United States of America,Paseo,2663,20.00,53260.00,2663.00,50597.00,26630.00,23967.00,12/1/2014,12,December,2014 +Government,France,Paseo,2136,7.00,14952.00,747.60,14204.40,10680.00,3524.40,12/1/2013,12,December,2013 +Midmarket,Germany,Paseo,2116,15.00,31740.00,1587.00,30153.00,21160.00,8993.00,12/1/2013,12,December,2013 +Midmarket,United States of America,Velo,555,15.00,8325.00,416.25,7908.75,5550.00,2358.75,1/1/2014,1,January,2014 +Midmarket,Mexico,Velo,2861,15.00,42915.00,2145.75,40769.25,28610.00,12159.25,1/1/2014,1,January,2014 +Enterprise,Germany,Velo,807,125.00,100875.00,5043.75,95831.25,96840.00,-1008.75,2/1/2014,2,February,2014 +Government,United States of America,Velo,602,350.00,210700.00,10535.00,200165.00,156520.00,43645.00,6/1/2014,6,June,2014 +Government,United States of America,Velo,2832,20.00,56640.00,2832.00,53808.00,28320.00,25488.00,8/1/2014,8,August,2014 +Government,France,Velo,1579,20.00,31580.00,1579.00,30001.00,15790.00,14211.00,8/1/2014,8,August,2014 +Enterprise,United States of America,Velo,861,125.00,107625.00,5381.25,102243.75,103320.00,-1076.25,10/1/2014,10,October,2014 +Enterprise,France,Velo,704,125.00,88000.00,4400.00,83600.00,84480.00,-880.00,10/1/2013,10,October,2013 +Government,France,Velo,1033,20.00,20660.00,1033.00,19627.00,10330.00,9297.00,12/1/2013,12,December,2013 +Small Business,Germany,Velo,1250,300.00,375000.00,18750.00,356250.00,312500.00,43750.00,12/1/2014,12,December,2014 +Government,Canada,VTT,1389,20.00,27780.00,1389.00,26391.00,13890.00,12501.00,10/1/2013,10,October,2013 +Government,United States of America,VTT,1265,20.00,25300.00,1265.00,24035.00,12650.00,11385.00,11/1/2013,11,November,2013 +Government,Germany,VTT,2297,20.00,45940.00,2297.00,43643.00,22970.00,20673.00,11/1/2013,11,November,2013 +Government,United States of America,VTT,2663,20.00,53260.00,2663.00,50597.00,26630.00,23967.00,12/1/2014,12,December,2014 +Government,United States of America,VTT,570,7.00,3990.00,199.50,3790.50,2850.00,940.50,12/1/2014,12,December,2014 +Government,France,VTT,2487,7.00,17409.00,870.45,16538.55,12435.00,4103.55,12/1/2014,12,December,2014 +Government,Germany,Amarilla,1350,350.00,472500.00,23625.00,448875.00,351000.00,97875.00,2/1/2014,2,February,2014 +Government,Canada,Amarilla,552,350.00,193200.00,9660.00,183540.00,143520.00,40020.00,8/1/2014,8,August,2014 +Government,Canada,Amarilla,1228,350.00,429800.00,21490.00,408310.00,319280.00,89030.00,10/1/2013,10,October,2013 +Small Business,Germany,Amarilla,1250,300.00,375000.00,18750.00,356250.00,312500.00,43750.00,12/1/2014,12,December,2014 +Midmarket,France,Paseo,3801,15.00,57015.00,3420.90,53594.10,38010.00,15584.10,4/1/2014,4,April,2014 +Government,United States of America,Carretera,1117.5,20.00,22350.00,1341.00,21009.00,11175.00,9834.00,1/1/2014,1,January,2014 +Midmarket,Canada,Carretera,2844,15.00,42660.00,2559.60,40100.40,28440.00,11660.40,6/1/2014,6,June,2014 +Channel Partners,Mexico,Carretera,562,12.00,6744.00,404.64,6339.36,1686.00,4653.36,9/1/2014,9,September,2014 +Channel Partners,Canada,Carretera,2299,12.00,27588.00,1655.28,25932.72,6897.00,19035.72,10/1/2013,10,October,2013 +Midmarket,United States of America,Carretera,2030,15.00,30450.00,1827.00,28623.00,20300.00,8323.00,11/1/2014,11,November,2014 +Government,United States of America,Carretera,263,7.00,1841.00,110.46,1730.54,1315.00,415.54,11/1/2013,11,November,2013 +Enterprise,Germany,Carretera,887,125.00,110875.00,6652.50,104222.50,106440.00,-2217.50,12/1/2013,12,December,2013 +Government,Mexico,Montana,980,350.00,343000.00,20580.00,322420.00,254800.00,67620.00,4/1/2014,4,April,2014 +Government,Germany,Montana,1460,350.00,511000.00,30660.00,480340.00,379600.00,100740.00,5/1/2014,5,May,2014 +Government,France,Montana,1403,7.00,9821.00,589.26,9231.74,7015.00,2216.74,10/1/2013,10,October,2013 +Channel Partners,United States of America,Montana,2723,12.00,32676.00,1960.56,30715.44,8169.00,22546.44,11/1/2014,11,November,2014 +Government,France,Paseo,1496,350.00,523600.00,31416.00,492184.00,388960.00,103224.00,6/1/2014,6,June,2014 +Channel Partners,Canada,Paseo,2299,12.00,27588.00,1655.28,25932.72,6897.00,19035.72,10/1/2013,10,October,2013 +Government,United States of America,Paseo,727,350.00,254450.00,15267.00,239183.00,189020.00,50163.00,10/1/2013,10,October,2013 +Enterprise,Canada,Velo,952,125.00,119000.00,7140.00,111860.00,114240.00,-2380.00,2/1/2014,2,February,2014 +Enterprise,United States of America,Velo,2755,125.00,344375.00,20662.50,323712.50,330600.00,-6887.50,2/1/2014,2,February,2014 +Midmarket,Germany,Velo,1530,15.00,22950.00,1377.00,21573.00,15300.00,6273.00,5/1/2014,5,May,2014 +Government,France,Velo,1496,350.00,523600.00,31416.00,492184.00,388960.00,103224.00,6/1/2014,6,June,2014 +Government,Mexico,Velo,1498,7.00,10486.00,629.16,9856.84,7490.00,2366.84,6/1/2014,6,June,2014 +Small Business,France,Velo,1221,300.00,366300.00,21978.00,344322.00,305250.00,39072.00,10/1/2013,10,October,2013 +Government,France,Velo,2076,350.00,726600.00,43596.00,683004.00,539760.00,143244.00,10/1/2013,10,October,2013 +Midmarket,Canada,VTT,2844,15.00,42660.00,2559.60,40100.40,28440.00,11660.40,6/1/2014,6,June,2014 +Government,Mexico,VTT,1498,7.00,10486.00,629.16,9856.84,7490.00,2366.84,6/1/2014,6,June,2014 +Small Business,France,VTT,1221,300.00,366300.00,21978.00,344322.00,305250.00,39072.00,10/1/2013,10,October,2013 +Government,Mexico,VTT,1123,20.00,22460.00,1347.60,21112.40,11230.00,9882.40,11/1/2013,11,November,2013 +Small Business,Canada,VTT,2436,300.00,730800.00,43848.00,686952.00,609000.00,77952.00,12/1/2013,12,December,2013 +Enterprise,France,Amarilla,1987.5,125.00,248437.50,14906.25,233531.25,238500.00,-4968.75,1/1/2014,1,January,2014 +Government,Mexico,Amarilla,1679,350.00,587650.00,35259.00,552391.00,436540.00,115851.00,9/1/2014,9,September,2014 +Government,United States of America,Amarilla,727,350.00,254450.00,15267.00,239183.00,189020.00,50163.00,10/1/2013,10,October,2013 +Government,France,Amarilla,1403,7.00,9821.00,589.26,9231.74,7015.00,2216.74,10/1/2013,10,October,2013 +Government,France,Amarilla,2076,350.00,726600.00,43596.00,683004.00,539760.00,143244.00,10/1/2013,10,October,2013 +Government,France,Montana,1757,20.00,35140.00,2108.40,33031.60,17570.00,15461.60,10/1/2013,10,October,2013 +Midmarket,United States of America,Paseo,2198,15.00,32970.00,1978.20,30991.80,21980.00,9011.80,8/1/2014,8,August,2014 +Midmarket,Germany,Paseo,1743,15.00,26145.00,1568.70,24576.30,17430.00,7146.30,8/1/2014,8,August,2014 +Midmarket,United States of America,Paseo,1153,15.00,17295.00,1037.70,16257.30,11530.00,4727.30,10/1/2014,10,October,2014 +Government,France,Paseo,1757,20.00,35140.00,2108.40,33031.60,17570.00,15461.60,10/1/2013,10,October,2013 +Government,Germany,Velo,1001,20.00,20020.00,1201.20,18818.80,10010.00,8808.80,8/1/2014,8,August,2014 +Government,Mexico,Velo,1333,7.00,9331.00,559.86,8771.14,6665.00,2106.14,11/1/2014,11,November,2014 +Midmarket,United States of America,VTT,1153,15.00,17295.00,1037.70,16257.30,11530.00,4727.30,10/1/2014,10,October,2014 +Channel Partners,Mexico,Carretera,727,12.00,8724.00,610.68,8113.32,2181.00,5932.32,2/1/2014,2,February,2014 +Channel Partners,Canada,Carretera,1884,12.00,22608.00,1582.56,21025.44,5652.00,15373.44,8/1/2014,8,August,2014 +Government,Mexico,Carretera,1834,20.00,36680.00,2567.60,34112.40,18340.00,15772.40,9/1/2013,9,September,2013 +Channel Partners,Mexico,Montana,2340,12.00,28080.00,1965.60,26114.40,7020.00,19094.40,1/1/2014,1,January,2014 +Channel Partners,France,Montana,2342,12.00,28104.00,1967.28,26136.72,7026.00,19110.72,11/1/2014,11,November,2014 +Government,France,Paseo,1031,7.00,7217.00,505.19,6711.81,5155.00,1556.81,9/1/2013,9,September,2013 +Midmarket,Canada,Velo,1262,15.00,18930.00,1325.10,17604.90,12620.00,4984.90,5/1/2014,5,May,2014 +Government,Canada,Velo,1135,7.00,7945.00,556.15,7388.85,5675.00,1713.85,6/1/2014,6,June,2014 +Government,United States of America,Velo,547,7.00,3829.00,268.03,3560.97,2735.00,825.97,11/1/2014,11,November,2014 +Government,Canada,Velo,1582,7.00,11074.00,775.18,10298.82,7910.00,2388.82,12/1/2014,12,December,2014 +Channel Partners,France,VTT,1738.5,12.00,20862.00,1460.34,19401.66,5215.50,14186.16,4/1/2014,4,April,2014 +Channel Partners,Germany,VTT,2215,12.00,26580.00,1860.60,24719.40,6645.00,18074.40,9/1/2013,9,September,2013 +Government,Canada,VTT,1582,7.00,11074.00,775.18,10298.82,7910.00,2388.82,12/1/2014,12,December,2014 +Government,Canada,Amarilla,1135,7.00,7945.00,556.15,7388.85,5675.00,1713.85,6/1/2014,6,June,2014 +Government,United States of America,Carretera,1761,350.00,616350.00,43144.50,573205.50,457860.00,115345.50,3/1/2014,3,March,2014 +Small Business,France,Carretera,448,300.00,134400.00,9408.00,124992.00,112000.00,12992.00,6/1/2014,6,June,2014 +Small Business,France,Carretera,2181,300.00,654300.00,45801.00,608499.00,545250.00,63249.00,10/1/2014,10,October,2014 +Government,France,Montana,1976,20.00,39520.00,2766.40,36753.60,19760.00,16993.60,10/1/2014,10,October,2014 +Small Business,France,Montana,2181,300.00,654300.00,45801.00,608499.00,545250.00,63249.00,10/1/2014,10,October,2014 +Enterprise,Germany,Montana,2500,125.00,312500.00,21875.00,290625.00,300000.00,-9375.00,11/1/2013,11,November,2013 +Small Business,Canada,Paseo,1702,300.00,510600.00,35742.00,474858.00,425500.00,49358.00,5/1/2014,5,May,2014 +Small Business,France,Paseo,448,300.00,134400.00,9408.00,124992.00,112000.00,12992.00,6/1/2014,6,June,2014 +Enterprise,Germany,Paseo,3513,125.00,439125.00,30738.75,408386.25,421560.00,-13173.75,7/1/2014,7,July,2014 +Midmarket,France,Paseo,2101,15.00,31515.00,2206.05,29308.95,21010.00,8298.95,8/1/2014,8,August,2014 +Midmarket,United States of America,Paseo,2931,15.00,43965.00,3077.55,40887.45,29310.00,11577.45,9/1/2013,9,September,2013 +Government,France,Paseo,1535,20.00,30700.00,2149.00,28551.00,15350.00,13201.00,9/1/2014,9,September,2014 +Small Business,Germany,Paseo,1123,300.00,336900.00,23583.00,313317.00,280750.00,32567.00,9/1/2013,9,September,2013 +Small Business,Canada,Paseo,1404,300.00,421200.00,29484.00,391716.00,351000.00,40716.00,11/1/2013,11,November,2013 +Channel Partners,Mexico,Paseo,2763,12.00,33156.00,2320.92,30835.08,8289.00,22546.08,11/1/2013,11,November,2013 +Government,Germany,Paseo,2125,7.00,14875.00,1041.25,13833.75,10625.00,3208.75,12/1/2013,12,December,2013 +Small Business,France,Velo,1659,300.00,497700.00,34839.00,462861.00,414750.00,48111.00,7/1/2014,7,July,2014 +Government,Mexico,Velo,609,20.00,12180.00,852.60,11327.40,6090.00,5237.40,8/1/2014,8,August,2014 +Enterprise,Germany,Velo,2087,125.00,260875.00,18261.25,242613.75,250440.00,-7826.25,9/1/2014,9,September,2014 +Government,France,Velo,1976,20.00,39520.00,2766.40,36753.60,19760.00,16993.60,10/1/2014,10,October,2014 +Government,United States of America,Velo,1421,20.00,28420.00,1989.40,26430.60,14210.00,12220.60,12/1/2013,12,December,2013 +Small Business,United States of America,Velo,1372,300.00,411600.00,28812.00,382788.00,343000.00,39788.00,12/1/2014,12,December,2014 +Government,Germany,Velo,588,20.00,11760.00,823.20,10936.80,5880.00,5056.80,12/1/2013,12,December,2013 +Channel Partners,Canada,VTT,3244.5,12.00,38934.00,2725.38,36208.62,9733.50,26475.12,1/1/2014,1,January,2014 +Small Business,France,VTT,959,300.00,287700.00,20139.00,267561.00,239750.00,27811.00,2/1/2014,2,February,2014 +Small Business,Mexico,VTT,2747,300.00,824100.00,57687.00,766413.00,686750.00,79663.00,2/1/2014,2,February,2014 +Enterprise,Canada,Amarilla,1645,125.00,205625.00,14393.75,191231.25,197400.00,-6168.75,5/1/2014,5,May,2014 +Government,France,Amarilla,2876,350.00,1006600.00,70462.00,936138.00,747760.00,188378.00,9/1/2014,9,September,2014 +Enterprise,Germany,Amarilla,994,125.00,124250.00,8697.50,115552.50,119280.00,-3727.50,9/1/2013,9,September,2013 +Government,Canada,Amarilla,1118,20.00,22360.00,1565.20,20794.80,11180.00,9614.80,11/1/2014,11,November,2014 +Small Business,United States of America,Amarilla,1372,300.00,411600.00,28812.00,382788.00,343000.00,39788.00,12/1/2014,12,December,2014 +Government,Canada,Montana,488,7.00,3416.00,273.28,3142.72,2440.00,702.72,2/1/2014,2,February,2014 +Government,United States of America,Montana,1282,20.00,25640.00,2051.20,23588.80,12820.00,10768.80,6/1/2014,6,June,2014 +Government,Canada,Paseo,257,7.00,1799.00,143.92,1655.08,1285.00,370.08,5/1/2014,5,May,2014 +Government,United States of America,Amarilla,1282,20.00,25640.00,2051.20,23588.80,12820.00,10768.80,6/1/2014,6,June,2014 +Enterprise,Mexico,Carretera,1540,125.00,192500.00,15400.00,177100.00,184800.00,-7700.00,8/1/2014,8,August,2014 +Midmarket,France,Carretera,490,15.00,7350.00,588.00,6762.00,4900.00,1862.00,11/1/2014,11,November,2014 +Government,Mexico,Carretera,1362,350.00,476700.00,38136.00,438564.00,354120.00,84444.00,12/1/2014,12,December,2014 +Midmarket,France,Montana,2501,15.00,37515.00,3001.20,34513.80,25010.00,9503.80,3/1/2014,3,March,2014 +Government,Canada,Montana,708,20.00,14160.00,1132.80,13027.20,7080.00,5947.20,6/1/2014,6,June,2014 +Government,Germany,Montana,645,20.00,12900.00,1032.00,11868.00,6450.00,5418.00,7/1/2014,7,July,2014 +Small Business,France,Montana,1562,300.00,468600.00,37488.00,431112.00,390500.00,40612.00,8/1/2014,8,August,2014 +Small Business,Canada,Montana,1283,300.00,384900.00,30792.00,354108.00,320750.00,33358.00,9/1/2013,9,September,2013 +Midmarket,Germany,Montana,711,15.00,10665.00,853.20,9811.80,7110.00,2701.80,12/1/2014,12,December,2014 +Enterprise,Mexico,Paseo,1114,125.00,139250.00,11140.00,128110.00,133680.00,-5570.00,3/1/2014,3,March,2014 +Government,Germany,Paseo,1259,7.00,8813.00,705.04,8107.96,6295.00,1812.96,4/1/2014,4,April,2014 +Government,Germany,Paseo,1095,7.00,7665.00,613.20,7051.80,5475.00,1576.80,5/1/2014,5,May,2014 +Government,Germany,Paseo,1366,20.00,27320.00,2185.60,25134.40,13660.00,11474.40,6/1/2014,6,June,2014 +Small Business,Mexico,Paseo,2460,300.00,738000.00,59040.00,678960.00,615000.00,63960.00,6/1/2014,6,June,2014 +Government,United States of America,Paseo,678,7.00,4746.00,379.68,4366.32,3390.00,976.32,8/1/2014,8,August,2014 +Government,Germany,Paseo,1598,7.00,11186.00,894.88,10291.12,7990.00,2301.12,8/1/2014,8,August,2014 +Government,Germany,Paseo,2409,7.00,16863.00,1349.04,15513.96,12045.00,3468.96,9/1/2013,9,September,2013 +Government,Germany,Paseo,1934,20.00,38680.00,3094.40,35585.60,19340.00,16245.60,9/1/2014,9,September,2014 +Government,Mexico,Paseo,2993,20.00,59860.00,4788.80,55071.20,29930.00,25141.20,9/1/2014,9,September,2014 +Government,Germany,Paseo,2146,350.00,751100.00,60088.00,691012.00,557960.00,133052.00,11/1/2013,11,November,2013 +Government,Mexico,Paseo,1946,7.00,13622.00,1089.76,12532.24,9730.00,2802.24,12/1/2013,12,December,2013 +Government,Mexico,Paseo,1362,350.00,476700.00,38136.00,438564.00,354120.00,84444.00,12/1/2014,12,December,2014 +Channel Partners,Canada,Velo,598,12.00,7176.00,574.08,6601.92,1794.00,4807.92,3/1/2014,3,March,2014 +Government,United States of America,Velo,2907,7.00,20349.00,1627.92,18721.08,14535.00,4186.08,6/1/2014,6,June,2014 +Government,Germany,Velo,2338,7.00,16366.00,1309.28,15056.72,11690.00,3366.72,6/1/2014,6,June,2014 +Small Business,France,Velo,386,300.00,115800.00,9264.00,106536.00,96500.00,10036.00,11/1/2013,11,November,2013 +Small Business,Mexico,Velo,635,300.00,190500.00,15240.00,175260.00,158750.00,16510.00,12/1/2014,12,December,2014 +Government,France,VTT,574.5,350.00,201075.00,16086.00,184989.00,149370.00,35619.00,4/1/2014,4,April,2014 +Government,Germany,VTT,2338,7.00,16366.00,1309.28,15056.72,11690.00,3366.72,6/1/2014,6,June,2014 +Government,France,VTT,381,350.00,133350.00,10668.00,122682.00,99060.00,23622.00,8/1/2014,8,August,2014 +Government,Germany,VTT,422,350.00,147700.00,11816.00,135884.00,109720.00,26164.00,8/1/2014,8,August,2014 +Small Business,Canada,VTT,2134,300.00,640200.00,51216.00,588984.00,533500.00,55484.00,9/1/2014,9,September,2014 +Small Business,United States of America,VTT,808,300.00,242400.00,19392.00,223008.00,202000.00,21008.00,12/1/2013,12,December,2013 +Government,Canada,Amarilla,708,20.00,14160.00,1132.80,13027.20,7080.00,5947.20,6/1/2014,6,June,2014 +Government,United States of America,Amarilla,2907,7.00,20349.00,1627.92,18721.08,14535.00,4186.08,6/1/2014,6,June,2014 +Government,Germany,Amarilla,1366,20.00,27320.00,2185.60,25134.40,13660.00,11474.40,6/1/2014,6,June,2014 +Small Business,Mexico,Amarilla,2460,300.00,738000.00,59040.00,678960.00,615000.00,63960.00,6/1/2014,6,June,2014 +Government,Germany,Amarilla,1520,20.00,30400.00,2432.00,27968.00,15200.00,12768.00,11/1/2014,11,November,2014 +Midmarket,Germany,Amarilla,711,15.00,10665.00,853.20,9811.80,7110.00,2701.80,12/1/2014,12,December,2014 +Channel Partners,Mexico,Amarilla,1375,12.00,16500.00,1320.00,15180.00,4125.00,11055.00,12/1/2013,12,December,2013 +Small Business,Mexico,Amarilla,635,300.00,190500.00,15240.00,175260.00,158750.00,16510.00,12/1/2014,12,December,2014 +Government,United States of America,VTT,436.5,20.00,8730.00,698.40,8031.60,4365.00,3666.60,7/1/2014,7,July,2014 +Small Business,Canada,Carretera,1094,300.00,328200.00,29538.00,298662.00,273500.00,25162.00,6/1/2014,6,June,2014 +Channel Partners,Mexico,Carretera,367,12.00,4404.00,396.36,4007.64,1101.00,2906.64,10/1/2013,10,October,2013 +Small Business,Canada,Montana,3802.5,300.00,1140750.00,102667.50,1038082.50,950625.00,87457.50,4/1/2014,4,April,2014 +Government,France,Montana,1666,350.00,583100.00,52479.00,530621.00,433160.00,97461.00,5/1/2014,5,May,2014 +Small Business,France,Montana,322,300.00,96600.00,8694.00,87906.00,80500.00,7406.00,9/1/2013,9,September,2013 +Channel Partners,Canada,Montana,2321,12.00,27852.00,2506.68,25345.32,6963.00,18382.32,11/1/2014,11,November,2014 +Enterprise,France,Montana,1857,125.00,232125.00,20891.25,211233.75,222840.00,-11606.25,11/1/2013,11,November,2013 +Government,Canada,Montana,1611,7.00,11277.00,1014.93,10262.07,8055.00,2207.07,12/1/2013,12,December,2013 +Enterprise,United States of America,Montana,2797,125.00,349625.00,31466.25,318158.75,335640.00,-17481.25,12/1/2014,12,December,2014 +Small Business,Germany,Montana,334,300.00,100200.00,9018.00,91182.00,83500.00,7682.00,12/1/2013,12,December,2013 +Small Business,Mexico,Paseo,2565,300.00,769500.00,69255.00,700245.00,641250.00,58995.00,1/1/2014,1,January,2014 +Government,Mexico,Paseo,2417,350.00,845950.00,76135.50,769814.50,628420.00,141394.50,1/1/2014,1,January,2014 +Midmarket,United States of America,Paseo,3675,15.00,55125.00,4961.25,50163.75,36750.00,13413.75,4/1/2014,4,April,2014 +Small Business,Canada,Paseo,1094,300.00,328200.00,29538.00,298662.00,273500.00,25162.00,6/1/2014,6,June,2014 +Midmarket,France,Paseo,1227,15.00,18405.00,1656.45,16748.55,12270.00,4478.55,10/1/2014,10,October,2014 +Channel Partners,Mexico,Paseo,367,12.00,4404.00,396.36,4007.64,1101.00,2906.64,10/1/2013,10,October,2013 +Small Business,France,Paseo,1324,300.00,397200.00,35748.00,361452.00,331000.00,30452.00,11/1/2014,11,November,2014 +Channel Partners,Germany,Paseo,1775,12.00,21300.00,1917.00,19383.00,5325.00,14058.00,11/1/2013,11,November,2013 +Enterprise,United States of America,Paseo,2797,125.00,349625.00,31466.25,318158.75,335640.00,-17481.25,12/1/2014,12,December,2014 +Midmarket,Mexico,Velo,245,15.00,3675.00,330.75,3344.25,2450.00,894.25,5/1/2014,5,May,2014 +Small Business,Canada,Velo,3793.5,300.00,1138050.00,102424.50,1035625.50,948375.00,87250.50,7/1/2014,7,July,2014 +Government,Germany,Velo,1307,350.00,457450.00,41170.50,416279.50,339820.00,76459.50,7/1/2014,7,July,2014 +Enterprise,Canada,Velo,567,125.00,70875.00,6378.75,64496.25,68040.00,-3543.75,9/1/2014,9,September,2014 +Enterprise,Mexico,Velo,2110,125.00,263750.00,23737.50,240012.50,253200.00,-13187.50,9/1/2014,9,September,2014 +Government,Canada,Velo,1269,350.00,444150.00,39973.50,404176.50,329940.00,74236.50,10/1/2014,10,October,2014 +Channel Partners,United States of America,VTT,1956,12.00,23472.00,2112.48,21359.52,5868.00,15491.52,1/1/2014,1,January,2014 +Small Business,Germany,VTT,2659,300.00,797700.00,71793.00,725907.00,664750.00,61157.00,2/1/2014,2,February,2014 +Government,United States of America,VTT,1351.5,350.00,473025.00,42572.25,430452.75,351390.00,79062.75,4/1/2014,4,April,2014 +Channel Partners,Germany,VTT,880,12.00,10560.00,950.40,9609.60,2640.00,6969.60,5/1/2014,5,May,2014 +Small Business,United States of America,VTT,1867,300.00,560100.00,50409.00,509691.00,466750.00,42941.00,9/1/2014,9,September,2014 +Channel Partners,France,VTT,2234,12.00,26808.00,2412.72,24395.28,6702.00,17693.28,9/1/2013,9,September,2013 +Midmarket,France,VTT,1227,15.00,18405.00,1656.45,16748.55,12270.00,4478.55,10/1/2014,10,October,2014 +Enterprise,Mexico,VTT,877,125.00,109625.00,9866.25,99758.75,105240.00,-5481.25,11/1/2014,11,November,2014 +Government,United States of America,Amarilla,2071,350.00,724850.00,65236.50,659613.50,538460.00,121153.50,9/1/2014,9,September,2014 +Government,Canada,Amarilla,1269,350.00,444150.00,39973.50,404176.50,329940.00,74236.50,10/1/2014,10,October,2014 +Midmarket,Germany,Amarilla,970,15.00,14550.00,1309.50,13240.50,9700.00,3540.50,11/1/2013,11,November,2013 +Government,Mexico,Amarilla,1694,20.00,33880.00,3049.20,30830.80,16940.00,13890.80,11/1/2014,11,November,2014 +Government,Germany,Carretera,663,20.00,13260.00,1193.40,12066.60,6630.00,5436.60,5/1/2014,5,May,2014 +Government,Canada,Carretera,819,7.00,5733.00,515.97,5217.03,4095.00,1122.03,7/1/2014,7,July,2014 +Channel Partners,Germany,Carretera,1580,12.00,18960.00,1706.40,17253.60,4740.00,12513.60,9/1/2014,9,September,2014 +Government,Mexico,Carretera,521,7.00,3647.00,328.23,3318.77,2605.00,713.77,12/1/2014,12,December,2014 +Government,United States of America,Paseo,973,20.00,19460.00,1751.40,17708.60,9730.00,7978.60,3/1/2014,3,March,2014 +Government,Mexico,Paseo,1038,20.00,20760.00,1868.40,18891.60,10380.00,8511.60,6/1/2014,6,June,2014 +Government,Germany,Paseo,360,7.00,2520.00,226.80,2293.20,1800.00,493.20,10/1/2014,10,October,2014 +Channel Partners,France,Velo,1967,12.00,23604.00,2124.36,21479.64,5901.00,15578.64,3/1/2014,3,March,2014 +Midmarket,Mexico,Velo,2628,15.00,39420.00,3547.80,35872.20,26280.00,9592.20,4/1/2014,4,April,2014 +Government,Germany,VTT,360,7.00,2520.00,226.80,2293.20,1800.00,493.20,10/1/2014,10,October,2014 +Government,France,VTT,2682,20.00,53640.00,4827.60,48812.40,26820.00,21992.40,11/1/2013,11,November,2013 +Government,Mexico,VTT,521,7.00,3647.00,328.23,3318.77,2605.00,713.77,12/1/2014,12,December,2014 +Government,Mexico,Amarilla,1038,20.00,20760.00,1868.40,18891.60,10380.00,8511.60,6/1/2014,6,June,2014 +Midmarket,Canada,Amarilla,1630.5,15.00,24457.50,2201.18,22256.33,16305.00,5951.33,7/1/2014,7,July,2014 +Channel Partners,France,Amarilla,306,12.00,3672.00,330.48,3341.52,918.00,2423.52,12/1/2013,12,December,2013 +Channel Partners,United States of America,Carretera,386,12.00,4632.00,463.20,4168.80,1158.00,3010.80,10/1/2013,10,October,2013 +Government,United States of America,Montana,2328,7.00,16296.00,1629.60,14666.40,11640.00,3026.40,9/1/2014,9,September,2014 +Channel Partners,United States of America,Paseo,386,12.00,4632.00,463.20,4168.80,1158.00,3010.80,10/1/2013,10,October,2013 +Enterprise,United States of America,Carretera,3445.5,125.00,430687.50,43068.75,387618.75,413460.00,-25841.25,4/1/2014,4,April,2014 +Enterprise,France,Carretera,1482,125.00,185250.00,18525.00,166725.00,177840.00,-11115.00,12/1/2013,12,December,2013 +Government,United States of America,Montana,2313,350.00,809550.00,80955.00,728595.00,601380.00,127215.00,5/1/2014,5,May,2014 +Enterprise,United States of America,Montana,1804,125.00,225500.00,22550.00,202950.00,216480.00,-13530.00,11/1/2013,11,November,2013 +Midmarket,France,Montana,2072,15.00,31080.00,3108.00,27972.00,20720.00,7252.00,12/1/2014,12,December,2014 +Government,France,Paseo,1954,20.00,39080.00,3908.00,35172.00,19540.00,15632.00,3/1/2014,3,March,2014 +Small Business,Mexico,Paseo,591,300.00,177300.00,17730.00,159570.00,147750.00,11820.00,5/1/2014,5,May,2014 +Midmarket,France,Paseo,2167,15.00,32505.00,3250.50,29254.50,21670.00,7584.50,10/1/2013,10,October,2013 +Government,Germany,Paseo,241,20.00,4820.00,482.00,4338.00,2410.00,1928.00,10/1/2014,10,October,2014 +Midmarket,Germany,Velo,681,15.00,10215.00,1021.50,9193.50,6810.00,2383.50,1/1/2014,1,January,2014 +Midmarket,Germany,Velo,510,15.00,7650.00,765.00,6885.00,5100.00,1785.00,4/1/2014,4,April,2014 +Midmarket,United States of America,Velo,790,15.00,11850.00,1185.00,10665.00,7900.00,2765.00,5/1/2014,5,May,2014 +Government,France,Velo,639,350.00,223650.00,22365.00,201285.00,166140.00,35145.00,7/1/2014,7,July,2014 +Enterprise,United States of America,Velo,1596,125.00,199500.00,19950.00,179550.00,191520.00,-11970.00,9/1/2014,9,September,2014 +Small Business,United States of America,Velo,2294,300.00,688200.00,68820.00,619380.00,573500.00,45880.00,10/1/2013,10,October,2013 +Government,Germany,Velo,241,20.00,4820.00,482.00,4338.00,2410.00,1928.00,10/1/2014,10,October,2014 +Government,Germany,Velo,2665,7.00,18655.00,1865.50,16789.50,13325.00,3464.50,11/1/2014,11,November,2014 +Enterprise,Canada,Velo,1916,125.00,239500.00,23950.00,215550.00,229920.00,-14370.00,12/1/2013,12,December,2013 +Small Business,France,Velo,853,300.00,255900.00,25590.00,230310.00,213250.00,17060.00,12/1/2014,12,December,2014 +Enterprise,Mexico,VTT,341,125.00,42625.00,4262.50,38362.50,40920.00,-2557.50,5/1/2014,5,May,2014 +Midmarket,Mexico,VTT,641,15.00,9615.00,961.50,8653.50,6410.00,2243.50,7/1/2014,7,July,2014 +Government,United States of America,VTT,2807,350.00,982450.00,98245.00,884205.00,729820.00,154385.00,8/1/2014,8,August,2014 +Small Business,Mexico,VTT,432,300.00,129600.00,12960.00,116640.00,108000.00,8640.00,9/1/2014,9,September,2014 +Small Business,United States of America,VTT,2294,300.00,688200.00,68820.00,619380.00,573500.00,45880.00,10/1/2013,10,October,2013 +Midmarket,France,VTT,2167,15.00,32505.00,3250.50,29254.50,21670.00,7584.50,10/1/2013,10,October,2013 +Enterprise,Canada,VTT,2529,125.00,316125.00,31612.50,284512.50,303480.00,-18967.50,11/1/2014,11,November,2014 +Government,Germany,VTT,1870,350.00,654500.00,65450.00,589050.00,486200.00,102850.00,12/1/2013,12,December,2013 +Enterprise,United States of America,Amarilla,579,125.00,72375.00,7237.50,65137.50,69480.00,-4342.50,1/1/2014,1,January,2014 +Government,Canada,Amarilla,2240,350.00,784000.00,78400.00,705600.00,582400.00,123200.00,2/1/2014,2,February,2014 +Small Business,United States of America,Amarilla,2993,300.00,897900.00,89790.00,808110.00,748250.00,59860.00,3/1/2014,3,March,2014 +Channel Partners,Canada,Amarilla,3520.5,12.00,42246.00,4224.60,38021.40,10561.50,27459.90,4/1/2014,4,April,2014 +Government,Mexico,Amarilla,2039,20.00,40780.00,4078.00,36702.00,20390.00,16312.00,5/1/2014,5,May,2014 +Channel Partners,Germany,Amarilla,2574,12.00,30888.00,3088.80,27799.20,7722.00,20077.20,8/1/2014,8,August,2014 +Government,Canada,Amarilla,707,350.00,247450.00,24745.00,222705.00,183820.00,38885.00,9/1/2014,9,September,2014 +Midmarket,France,Amarilla,2072,15.00,31080.00,3108.00,27972.00,20720.00,7252.00,12/1/2014,12,December,2014 +Small Business,France,Amarilla,853,300.00,255900.00,25590.00,230310.00,213250.00,17060.00,12/1/2014,12,December,2014 +Channel Partners,France,Carretera,1198,12.00,14376.00,1581.36,12794.64,3594.00,9200.64,10/1/2013,10,October,2013 +Government,France,Paseo,2532,7.00,17724.00,1949.64,15774.36,12660.00,3114.36,4/1/2014,4,April,2014 +Channel Partners,France,Paseo,1198,12.00,14376.00,1581.36,12794.64,3594.00,9200.64,10/1/2013,10,October,2013 +Midmarket,Canada,Velo,384,15.00,5760.00,633.60,5126.40,3840.00,1286.40,1/1/2014,1,January,2014 +Channel Partners,Germany,Velo,472,12.00,5664.00,623.04,5040.96,1416.00,3624.96,10/1/2014,10,October,2014 +Government,United States of America,VTT,1579,7.00,11053.00,1215.83,9837.17,7895.00,1942.17,3/1/2014,3,March,2014 +Channel Partners,Mexico,VTT,1005,12.00,12060.00,1326.60,10733.40,3015.00,7718.40,9/1/2013,9,September,2013 +Midmarket,United States of America,Amarilla,3199.5,15.00,47992.50,5279.18,42713.33,31995.00,10718.33,7/1/2014,7,July,2014 +Channel Partners,Germany,Amarilla,472,12.00,5664.00,623.04,5040.96,1416.00,3624.96,10/1/2014,10,October,2014 +Channel Partners,Canada,Carretera,1937,12.00,23244.00,2556.84,20687.16,5811.00,14876.16,2/1/2014,2,February,2014 +Government,Germany,Carretera,792,350.00,277200.00,30492.00,246708.00,205920.00,40788.00,3/1/2014,3,March,2014 +Small Business,Germany,Carretera,2811,300.00,843300.00,92763.00,750537.00,702750.00,47787.00,7/1/2014,7,July,2014 +Enterprise,France,Carretera,2441,125.00,305125.00,33563.75,271561.25,292920.00,-21358.75,10/1/2014,10,October,2014 +Midmarket,Canada,Carretera,1560,15.00,23400.00,2574.00,20826.00,15600.00,5226.00,11/1/2013,11,November,2013 +Government,Mexico,Carretera,2706,7.00,18942.00,2083.62,16858.38,13530.00,3328.38,11/1/2013,11,November,2013 +Government,Germany,Montana,766,350.00,268100.00,29491.00,238609.00,199160.00,39449.00,1/1/2014,1,January,2014 +Government,Germany,Montana,2992,20.00,59840.00,6582.40,53257.60,29920.00,23337.60,10/1/2013,10,October,2013 +Midmarket,Mexico,Montana,2157,15.00,32355.00,3559.05,28795.95,21570.00,7225.95,12/1/2014,12,December,2014 +Small Business,Canada,Paseo,873,300.00,261900.00,28809.00,233091.00,218250.00,14841.00,1/1/2014,1,January,2014 +Government,Mexico,Paseo,1122,20.00,22440.00,2468.40,19971.60,11220.00,8751.60,3/1/2014,3,March,2014 +Government,Canada,Paseo,2104.5,350.00,736575.00,81023.25,655551.75,547170.00,108381.75,7/1/2014,7,July,2014 +Channel Partners,Canada,Paseo,4026,12.00,48312.00,5314.32,42997.68,12078.00,30919.68,7/1/2014,7,July,2014 +Channel Partners,France,Paseo,2425.5,12.00,29106.00,3201.66,25904.34,7276.50,18627.84,7/1/2014,7,July,2014 +Government,Canada,Paseo,2394,20.00,47880.00,5266.80,42613.20,23940.00,18673.20,8/1/2014,8,August,2014 +Midmarket,Mexico,Paseo,1984,15.00,29760.00,3273.60,26486.40,19840.00,6646.40,8/1/2014,8,August,2014 +Enterprise,France,Paseo,2441,125.00,305125.00,33563.75,271561.25,292920.00,-21358.75,10/1/2014,10,October,2014 +Government,Germany,Paseo,2992,20.00,59840.00,6582.40,53257.60,29920.00,23337.60,10/1/2013,10,October,2013 +Small Business,Canada,Paseo,1366,300.00,409800.00,45078.00,364722.00,341500.00,23222.00,11/1/2014,11,November,2014 +Government,France,Velo,2805,20.00,56100.00,6171.00,49929.00,28050.00,21879.00,9/1/2013,9,September,2013 +Midmarket,Mexico,Velo,655,15.00,9825.00,1080.75,8744.25,6550.00,2194.25,9/1/2013,9,September,2013 +Government,Mexico,Velo,344,350.00,120400.00,13244.00,107156.00,89440.00,17716.00,10/1/2013,10,October,2013 +Government,Canada,Velo,1808,7.00,12656.00,1392.16,11263.84,9040.00,2223.84,11/1/2014,11,November,2014 +Channel Partners,France,VTT,1734,12.00,20808.00,2288.88,18519.12,5202.00,13317.12,1/1/2014,1,January,2014 +Enterprise,Mexico,VTT,554,125.00,69250.00,7617.50,61632.50,66480.00,-4847.50,1/1/2014,1,January,2014 +Government,Canada,VTT,2935,20.00,58700.00,6457.00,52243.00,29350.00,22893.00,11/1/2013,11,November,2013 +Enterprise,Germany,Amarilla,3165,125.00,395625.00,43518.75,352106.25,379800.00,-27693.75,1/1/2014,1,January,2014 +Government,Mexico,Amarilla,2629,20.00,52580.00,5783.80,46796.20,26290.00,20506.20,1/1/2014,1,January,2014 +Enterprise,France,Amarilla,1433,125.00,179125.00,19703.75,159421.25,171960.00,-12538.75,5/1/2014,5,May,2014 +Enterprise,Mexico,Amarilla,947,125.00,118375.00,13021.25,105353.75,113640.00,-8286.25,9/1/2013,9,September,2013 +Government,Mexico,Amarilla,344,350.00,120400.00,13244.00,107156.00,89440.00,17716.00,10/1/2013,10,October,2013 +Midmarket,Mexico,Amarilla,2157,15.00,32355.00,3559.05,28795.95,21570.00,7225.95,12/1/2014,12,December,2014 +Government,United States of America,Paseo,380,7.00,2660.00,292.60,2367.40,1900.00,467.40,9/1/2013,9,September,2013 +Government,Mexico,Carretera,886,350.00,310100.00,37212.00,272888.00,230360.00,42528.00,6/1/2014,6,June,2014 +Enterprise,Canada,Carretera,2416,125.00,302000.00,36240.00,265760.00,289920.00,-24160.00,9/1/2013,9,September,2013 +Enterprise,Mexico,Carretera,2156,125.00,269500.00,32340.00,237160.00,258720.00,-21560.00,10/1/2014,10,October,2014 +Midmarket,Canada,Carretera,2689,15.00,40335.00,4840.20,35494.80,26890.00,8604.80,11/1/2014,11,November,2014 +Midmarket,United States of America,Montana,677,15.00,10155.00,1218.60,8936.40,6770.00,2166.40,3/1/2014,3,March,2014 +Small Business,France,Montana,1773,300.00,531900.00,63828.00,468072.00,443250.00,24822.00,4/1/2014,4,April,2014 +Government,Mexico,Montana,2420,7.00,16940.00,2032.80,14907.20,12100.00,2807.20,9/1/2014,9,September,2014 +Government,Canada,Montana,2734,7.00,19138.00,2296.56,16841.44,13670.00,3171.44,10/1/2014,10,October,2014 +Government,Mexico,Montana,1715,20.00,34300.00,4116.00,30184.00,17150.00,13034.00,10/1/2013,10,October,2013 +Small Business,France,Montana,1186,300.00,355800.00,42696.00,313104.00,296500.00,16604.00,12/1/2013,12,December,2013 +Small Business,United States of America,Paseo,3495,300.00,1048500.00,125820.00,922680.00,873750.00,48930.00,1/1/2014,1,January,2014 +Government,Mexico,Paseo,886,350.00,310100.00,37212.00,272888.00,230360.00,42528.00,6/1/2014,6,June,2014 +Enterprise,Mexico,Paseo,2156,125.00,269500.00,32340.00,237160.00,258720.00,-21560.00,10/1/2014,10,October,2014 +Government,Mexico,Paseo,905,20.00,18100.00,2172.00,15928.00,9050.00,6878.00,10/1/2014,10,October,2014 +Government,Mexico,Paseo,1715,20.00,34300.00,4116.00,30184.00,17150.00,13034.00,10/1/2013,10,October,2013 +Government,France,Paseo,1594,350.00,557900.00,66948.00,490952.00,414440.00,76512.00,11/1/2014,11,November,2014 +Small Business,Germany,Paseo,1359,300.00,407700.00,48924.00,358776.00,339750.00,19026.00,11/1/2014,11,November,2014 +Small Business,Mexico,Paseo,2150,300.00,645000.00,77400.00,567600.00,537500.00,30100.00,11/1/2014,11,November,2014 +Government,Mexico,Paseo,1197,350.00,418950.00,50274.00,368676.00,311220.00,57456.00,11/1/2014,11,November,2014 +Midmarket,Mexico,Paseo,380,15.00,5700.00,684.00,5016.00,3800.00,1216.00,12/1/2013,12,December,2013 +Government,Mexico,Paseo,1233,20.00,24660.00,2959.20,21700.80,12330.00,9370.80,12/1/2014,12,December,2014 +Government,Mexico,Velo,1395,350.00,488250.00,58590.00,429660.00,362700.00,66960.00,7/1/2014,7,July,2014 +Government,United States of America,Velo,986,350.00,345100.00,41412.00,303688.00,256360.00,47328.00,10/1/2014,10,October,2014 +Government,Mexico,Velo,905,20.00,18100.00,2172.00,15928.00,9050.00,6878.00,10/1/2014,10,October,2014 +Channel Partners,Canada,VTT,2109,12.00,25308.00,3036.96,22271.04,6327.00,15944.04,5/1/2014,5,May,2014 +Midmarket,France,VTT,3874.5,15.00,58117.50,6974.10,51143.40,38745.00,12398.40,7/1/2014,7,July,2014 +Government,Canada,VTT,623,350.00,218050.00,26166.00,191884.00,161980.00,29904.00,9/1/2013,9,September,2013 +Government,United States of America,VTT,986,350.00,345100.00,41412.00,303688.00,256360.00,47328.00,10/1/2014,10,October,2014 +Enterprise,United States of America,VTT,2387,125.00,298375.00,35805.00,262570.00,286440.00,-23870.00,11/1/2014,11,November,2014 +Government,Mexico,VTT,1233,20.00,24660.00,2959.20,21700.80,12330.00,9370.80,12/1/2014,12,December,2014 +Government,United States of America,Amarilla,270,350.00,94500.00,11340.00,83160.00,70200.00,12960.00,2/1/2014,2,February,2014 +Government,France,Amarilla,3421.5,7.00,23950.50,2874.06,21076.44,17107.50,3968.94,7/1/2014,7,July,2014 +Government,Canada,Amarilla,2734,7.00,19138.00,2296.56,16841.44,13670.00,3171.44,10/1/2014,10,October,2014 +Midmarket,United States of America,Amarilla,2548,15.00,38220.00,4586.40,33633.60,25480.00,8153.60,11/1/2013,11,November,2013 +Government,France,Carretera,2521.5,20.00,50430.00,6051.60,44378.40,25215.00,19163.40,1/1/2014,1,January,2014 +Channel Partners,Mexico,Montana,2661,12.00,31932.00,3831.84,28100.16,7983.00,20117.16,5/1/2014,5,May,2014 +Government,Germany,Paseo,1531,20.00,30620.00,3674.40,26945.60,15310.00,11635.60,12/1/2014,12,December,2014 +Government,France,VTT,1491,7.00,10437.00,1252.44,9184.56,7455.00,1729.56,3/1/2014,3,March,2014 +Government,Germany,VTT,1531,20.00,30620.00,3674.40,26945.60,15310.00,11635.60,12/1/2014,12,December,2014 +Channel Partners,Canada,Amarilla,2761,12.00,33132.00,3975.84,29156.16,8283.00,20873.16,9/1/2013,9,September,2013 +Midmarket,United States of America,Carretera,2567,15.00,38505.00,5005.65,33499.35,25670.00,7829.35,6/1/2014,6,June,2014 +Midmarket,United States of America,VTT,2567,15.00,38505.00,5005.65,33499.35,25670.00,7829.35,6/1/2014,6,June,2014 +Government,Canada,Carretera,923,350.00,323050.00,41996.50,281053.50,239980.00,41073.50,3/1/2014,3,March,2014 +Government,France,Carretera,1790,350.00,626500.00,81445.00,545055.00,465400.00,79655.00,3/1/2014,3,March,2014 +Government,Germany,Carretera,442,20.00,8840.00,1149.20,7690.80,4420.00,3270.80,9/1/2013,9,September,2013 +Government,United States of America,Montana,982.5,350.00,343875.00,44703.75,299171.25,255450.00,43721.25,1/1/2014,1,January,2014 +Government,United States of America,Montana,1298,7.00,9086.00,1181.18,7904.82,6490.00,1414.82,2/1/2014,2,February,2014 +Channel Partners,Mexico,Montana,604,12.00,7248.00,942.24,6305.76,1812.00,4493.76,6/1/2014,6,June,2014 +Government,Mexico,Montana,2255,20.00,45100.00,5863.00,39237.00,22550.00,16687.00,7/1/2014,7,July,2014 +Government,Canada,Montana,1249,20.00,24980.00,3247.40,21732.60,12490.00,9242.60,10/1/2014,10,October,2014 +Government,United States of America,Paseo,1438.5,7.00,10069.50,1309.04,8760.47,7192.50,1567.97,1/1/2014,1,January,2014 +Small Business,Germany,Paseo,807,300.00,242100.00,31473.00,210627.00,201750.00,8877.00,1/1/2014,1,January,2014 +Government,United States of America,Paseo,2641,20.00,52820.00,6866.60,45953.40,26410.00,19543.40,2/1/2014,2,February,2014 +Government,Germany,Paseo,2708,20.00,54160.00,7040.80,47119.20,27080.00,20039.20,2/1/2014,2,February,2014 +Government,Canada,Paseo,2632,350.00,921200.00,119756.00,801444.00,684320.00,117124.00,6/1/2014,6,June,2014 +Enterprise,Canada,Paseo,1583,125.00,197875.00,25723.75,172151.25,189960.00,-17808.75,6/1/2014,6,June,2014 +Channel Partners,Mexico,Paseo,571,12.00,6852.00,890.76,5961.24,1713.00,4248.24,7/1/2014,7,July,2014 +Government,France,Paseo,2696,7.00,18872.00,2453.36,16418.64,13480.00,2938.64,8/1/2014,8,August,2014 +Midmarket,Canada,Paseo,1565,15.00,23475.00,3051.75,20423.25,15650.00,4773.25,10/1/2014,10,October,2014 +Government,Canada,Paseo,1249,20.00,24980.00,3247.40,21732.60,12490.00,9242.60,10/1/2014,10,October,2014 +Government,Germany,Paseo,357,350.00,124950.00,16243.50,108706.50,92820.00,15886.50,11/1/2014,11,November,2014 +Channel Partners,Germany,Paseo,1013,12.00,12156.00,1580.28,10575.72,3039.00,7536.72,12/1/2014,12,December,2014 +Midmarket,France,Velo,3997.5,15.00,59962.50,7795.13,52167.38,39975.00,12192.38,1/1/2014,1,January,2014 +Government,Canada,Velo,2632,350.00,921200.00,119756.00,801444.00,684320.00,117124.00,6/1/2014,6,June,2014 +Government,France,Velo,1190,7.00,8330.00,1082.90,7247.10,5950.00,1297.10,6/1/2014,6,June,2014 +Channel Partners,Mexico,Velo,604,12.00,7248.00,942.24,6305.76,1812.00,4493.76,6/1/2014,6,June,2014 +Midmarket,Germany,Velo,660,15.00,9900.00,1287.00,8613.00,6600.00,2013.00,9/1/2013,9,September,2013 +Channel Partners,Mexico,Velo,410,12.00,4920.00,639.60,4280.40,1230.00,3050.40,10/1/2014,10,October,2014 +Small Business,Mexico,Velo,2605,300.00,781500.00,101595.00,679905.00,651250.00,28655.00,11/1/2013,11,November,2013 +Channel Partners,Germany,Velo,1013,12.00,12156.00,1580.28,10575.72,3039.00,7536.72,12/1/2014,12,December,2014 +Enterprise,Canada,VTT,1583,125.00,197875.00,25723.75,172151.25,189960.00,-17808.75,6/1/2014,6,June,2014 +Midmarket,Canada,VTT,1565,15.00,23475.00,3051.75,20423.25,15650.00,4773.25,10/1/2014,10,October,2014 +Enterprise,Canada,Amarilla,1659,125.00,207375.00,26958.75,180416.25,199080.00,-18663.75,1/1/2014,1,January,2014 +Government,France,Amarilla,1190,7.00,8330.00,1082.90,7247.10,5950.00,1297.10,6/1/2014,6,June,2014 +Channel Partners,Mexico,Amarilla,410,12.00,4920.00,639.60,4280.40,1230.00,3050.40,10/1/2014,10,October,2014 +Channel Partners,Germany,Amarilla,1770,12.00,21240.00,2761.20,18478.80,5310.00,13168.80,12/1/2013,12,December,2013 +Government,Mexico,Carretera,2579,20.00,51580.00,7221.20,44358.80,25790.00,18568.80,4/1/2014,4,April,2014 +Government,United States of America,Carretera,1743,20.00,34860.00,4880.40,29979.60,17430.00,12549.60,5/1/2014,5,May,2014 +Government,United States of America,Carretera,2996,7.00,20972.00,2936.08,18035.92,14980.00,3055.92,10/1/2013,10,October,2013 +Government,Germany,Carretera,280,7.00,1960.00,274.40,1685.60,1400.00,285.60,12/1/2014,12,December,2014 +Government,France,Montana,293,7.00,2051.00,287.14,1763.86,1465.00,298.86,2/1/2014,2,February,2014 +Government,United States of America,Montana,2996,7.00,20972.00,2936.08,18035.92,14980.00,3055.92,10/1/2013,10,October,2013 +Midmarket,Germany,Paseo,278,15.00,4170.00,583.80,3586.20,2780.00,806.20,2/1/2014,2,February,2014 +Government,Canada,Paseo,2428,20.00,48560.00,6798.40,41761.60,24280.00,17481.60,3/1/2014,3,March,2014 +Midmarket,United States of America,Paseo,1767,15.00,26505.00,3710.70,22794.30,17670.00,5124.30,9/1/2014,9,September,2014 +Channel Partners,France,Paseo,1393,12.00,16716.00,2340.24,14375.76,4179.00,10196.76,10/1/2014,10,October,2014 +Government,Germany,VTT,280,7.00,1960.00,274.40,1685.60,1400.00,285.60,12/1/2014,12,December,2014 +Channel Partners,France,Amarilla,1393,12.00,16716.00,2340.24,14375.76,4179.00,10196.76,10/1/2014,10,October,2014 +Channel Partners,United States of America,Amarilla,2015,12.00,24180.00,3385.20,20794.80,6045.00,14749.80,12/1/2013,12,December,2013 +Small Business,Mexico,Carretera,801,300.00,240300.00,33642.00,206658.00,200250.00,6408.00,7/1/2014,7,July,2014 +Enterprise,France,Carretera,1023,125.00,127875.00,17902.50,109972.50,122760.00,-12787.50,9/1/2013,9,September,2013 +Small Business,Canada,Carretera,1496,300.00,448800.00,62832.00,385968.00,374000.00,11968.00,10/1/2014,10,October,2014 +Small Business,United States of America,Carretera,1010,300.00,303000.00,42420.00,260580.00,252500.00,8080.00,10/1/2014,10,October,2014 +Midmarket,Germany,Carretera,1513,15.00,22695.00,3177.30,19517.70,15130.00,4387.70,11/1/2014,11,November,2014 +Midmarket,Canada,Carretera,2300,15.00,34500.00,4830.00,29670.00,23000.00,6670.00,12/1/2014,12,December,2014 +Enterprise,Mexico,Carretera,2821,125.00,352625.00,49367.50,303257.50,338520.00,-35262.50,12/1/2013,12,December,2013 +Government,Canada,Montana,2227.5,350.00,779625.00,109147.50,670477.50,579150.00,91327.50,1/1/2014,1,January,2014 +Government,Germany,Montana,1199,350.00,419650.00,58751.00,360899.00,311740.00,49159.00,4/1/2014,4,April,2014 +Government,Canada,Montana,200,350.00,70000.00,9800.00,60200.00,52000.00,8200.00,5/1/2014,5,May,2014 +Government,Canada,Montana,388,7.00,2716.00,380.24,2335.76,1940.00,395.76,9/1/2014,9,September,2014 +Government,Mexico,Montana,1727,7.00,12089.00,1692.46,10396.54,8635.00,1761.54,10/1/2013,10,October,2013 +Midmarket,Canada,Montana,2300,15.00,34500.00,4830.00,29670.00,23000.00,6670.00,12/1/2014,12,December,2014 +Government,Mexico,Paseo,260,20.00,5200.00,728.00,4472.00,2600.00,1872.00,2/1/2014,2,February,2014 +Midmarket,Canada,Paseo,2470,15.00,37050.00,5187.00,31863.00,24700.00,7163.00,9/1/2013,9,September,2013 +Midmarket,Canada,Paseo,1743,15.00,26145.00,3660.30,22484.70,17430.00,5054.70,10/1/2013,10,October,2013 +Channel Partners,United States of America,Paseo,2914,12.00,34968.00,4895.52,30072.48,8742.00,21330.48,10/1/2014,10,October,2014 +Government,France,Paseo,1731,7.00,12117.00,1696.38,10420.62,8655.00,1765.62,10/1/2014,10,October,2014 +Government,Canada,Paseo,700,350.00,245000.00,34300.00,210700.00,182000.00,28700.00,11/1/2014,11,November,2014 +Channel Partners,Canada,Paseo,2222,12.00,26664.00,3732.96,22931.04,6666.00,16265.04,11/1/2013,11,November,2013 +Government,United States of America,Paseo,1177,350.00,411950.00,57673.00,354277.00,306020.00,48257.00,11/1/2014,11,November,2014 +Government,France,Paseo,1922,350.00,672700.00,94178.00,578522.00,499720.00,78802.00,11/1/2013,11,November,2013 +Enterprise,Mexico,Velo,1575,125.00,196875.00,27562.50,169312.50,189000.00,-19687.50,2/1/2014,2,February,2014 +Government,United States of America,Velo,606,20.00,12120.00,1696.80,10423.20,6060.00,4363.20,4/1/2014,4,April,2014 +Small Business,United States of America,Velo,2460,300.00,738000.00,103320.00,634680.00,615000.00,19680.00,7/1/2014,7,July,2014 +Small Business,Canada,Velo,269,300.00,80700.00,11298.00,69402.00,67250.00,2152.00,10/1/2013,10,October,2013 +Small Business,Germany,Velo,2536,300.00,760800.00,106512.00,654288.00,634000.00,20288.00,11/1/2013,11,November,2013 +Government,Mexico,VTT,2903,7.00,20321.00,2844.94,17476.06,14515.00,2961.06,3/1/2014,3,March,2014 +Small Business,United States of America,VTT,2541,300.00,762300.00,106722.00,655578.00,635250.00,20328.00,8/1/2014,8,August,2014 +Small Business,Canada,VTT,269,300.00,80700.00,11298.00,69402.00,67250.00,2152.00,10/1/2013,10,October,2013 +Small Business,Canada,VTT,1496,300.00,448800.00,62832.00,385968.00,374000.00,11968.00,10/1/2014,10,October,2014 +Small Business,United States of America,VTT,1010,300.00,303000.00,42420.00,260580.00,252500.00,8080.00,10/1/2014,10,October,2014 +Government,France,VTT,1281,350.00,448350.00,62769.00,385581.00,333060.00,52521.00,12/1/2013,12,December,2013 +Small Business,Canada,Amarilla,888,300.00,266400.00,37296.00,229104.00,222000.00,7104.00,3/1/2014,3,March,2014 +Enterprise,United States of America,Amarilla,2844,125.00,355500.00,49770.00,305730.00,341280.00,-35550.00,5/1/2014,5,May,2014 +Channel Partners,France,Amarilla,2475,12.00,29700.00,4158.00,25542.00,7425.00,18117.00,8/1/2014,8,August,2014 +Midmarket,Canada,Amarilla,1743,15.00,26145.00,3660.30,22484.70,17430.00,5054.70,10/1/2013,10,October,2013 +Channel Partners,United States of America,Amarilla,2914,12.00,34968.00,4895.52,30072.48,8742.00,21330.48,10/1/2014,10,October,2014 +Government,France,Amarilla,1731,7.00,12117.00,1696.38,10420.62,8655.00,1765.62,10/1/2014,10,October,2014 +Government,Mexico,Amarilla,1727,7.00,12089.00,1692.46,10396.54,8635.00,1761.54,10/1/2013,10,October,2013 +Midmarket,Mexico,Amarilla,1870,15.00,28050.00,3927.00,24123.00,18700.00,5423.00,11/1/2013,11,November,2013 +Enterprise,France,Carretera,1174,125.00,146750.00,22012.50,124737.50,140880.00,-16142.50,8/1/2014,8,August,2014 +Enterprise,Germany,Carretera,2767,125.00,345875.00,51881.25,293993.75,332040.00,-38046.25,8/1/2014,8,August,2014 +Enterprise,Germany,Carretera,1085,125.00,135625.00,20343.75,115281.25,130200.00,-14918.75,10/1/2014,10,October,2014 +Small Business,Mexico,Montana,546,300.00,163800.00,24570.00,139230.00,136500.00,2730.00,10/1/2014,10,October,2014 +Government,Germany,Paseo,1158,20.00,23160.00,3474.00,19686.00,11580.00,8106.00,3/1/2014,3,March,2014 +Midmarket,Canada,Paseo,1614,15.00,24210.00,3631.50,20578.50,16140.00,4438.50,4/1/2014,4,April,2014 +Government,Mexico,Paseo,2535,7.00,17745.00,2661.75,15083.25,12675.00,2408.25,4/1/2014,4,April,2014 +Government,Mexico,Paseo,2851,350.00,997850.00,149677.50,848172.50,741260.00,106912.50,5/1/2014,5,May,2014 +Midmarket,Canada,Paseo,2559,15.00,38385.00,5757.75,32627.25,25590.00,7037.25,8/1/2014,8,August,2014 +Government,United States of America,Paseo,267,20.00,5340.00,801.00,4539.00,2670.00,1869.00,10/1/2013,10,October,2013 +Enterprise,Germany,Paseo,1085,125.00,135625.00,20343.75,115281.25,130200.00,-14918.75,10/1/2014,10,October,2014 +Midmarket,Germany,Paseo,1175,15.00,17625.00,2643.75,14981.25,11750.00,3231.25,10/1/2014,10,October,2014 +Government,United States of America,Paseo,2007,350.00,702450.00,105367.50,597082.50,521820.00,75262.50,11/1/2013,11,November,2013 +Government,Mexico,Paseo,2151,350.00,752850.00,112927.50,639922.50,559260.00,80662.50,11/1/2013,11,November,2013 +Channel Partners,United States of America,Paseo,914,12.00,10968.00,1645.20,9322.80,2742.00,6580.80,12/1/2014,12,December,2014 +Government,France,Paseo,293,20.00,5860.00,879.00,4981.00,2930.00,2051.00,12/1/2014,12,December,2014 +Channel Partners,Mexico,Velo,500,12.00,6000.00,900.00,5100.00,1500.00,3600.00,3/1/2014,3,March,2014 +Midmarket,France,Velo,2826,15.00,42390.00,6358.50,36031.50,28260.00,7771.50,5/1/2014,5,May,2014 +Enterprise,France,Velo,663,125.00,82875.00,12431.25,70443.75,79560.00,-9116.25,9/1/2014,9,September,2014 +Small Business,United States of America,Velo,2574,300.00,772200.00,115830.00,656370.00,643500.00,12870.00,11/1/2013,11,November,2013 +Enterprise,United States of America,Velo,2438,125.00,304750.00,45712.50,259037.50,292560.00,-33522.50,12/1/2013,12,December,2013 +Channel Partners,United States of America,Velo,914,12.00,10968.00,1645.20,9322.80,2742.00,6580.80,12/1/2014,12,December,2014 +Government,Canada,VTT,865.5,20.00,17310.00,2596.50,14713.50,8655.00,6058.50,7/1/2014,7,July,2014 +Midmarket,Germany,VTT,492,15.00,7380.00,1107.00,6273.00,4920.00,1353.00,7/1/2014,7,July,2014 +Government,United States of America,VTT,267,20.00,5340.00,801.00,4539.00,2670.00,1869.00,10/1/2013,10,October,2013 +Midmarket,Germany,VTT,1175,15.00,17625.00,2643.75,14981.25,11750.00,3231.25,10/1/2014,10,October,2014 +Enterprise,Canada,VTT,2954,125.00,369250.00,55387.50,313862.50,354480.00,-40617.50,11/1/2013,11,November,2013 +Enterprise,Germany,VTT,552,125.00,69000.00,10350.00,58650.00,66240.00,-7590.00,11/1/2014,11,November,2014 +Government,France,VTT,293,20.00,5860.00,879.00,4981.00,2930.00,2051.00,12/1/2014,12,December,2014 +Small Business,France,Amarilla,2475,300.00,742500.00,111375.00,631125.00,618750.00,12375.00,3/1/2014,3,March,2014 +Small Business,Mexico,Amarilla,546,300.00,163800.00,24570.00,139230.00,136500.00,2730.00,10/1/2014,10,October,2014 +Government,Mexico,Montana,1368,7.00,9576.00,1436.40,8139.60,6840.00,1299.60,2/1/2014,2,February,2014 +Government,Canada,Paseo,723,7.00,5061.00,759.15,4301.85,3615.00,686.85,4/1/2014,4,April,2014 +Channel Partners,United States of America,VTT,1806,12.00,21672.00,3250.80,18421.20,5418.00,13003.20,5/1/2014,5,May,2014 diff --git a/dotnet/samples/ConceptsV2/Resources/travelinfo.txt b/dotnet/samples/ConceptsV2/Resources/travelinfo.txt new file mode 100644 index 000000000000..21665c82198e --- /dev/null +++ b/dotnet/samples/ConceptsV2/Resources/travelinfo.txt @@ -0,0 +1,217 @@ +Invoice Booking Reference LMNOPQ Trip ID - 11110011111 +Passenger Name(s) +MARKS/SAM ALBERT Agent W2 + + +MICROSOFT CORPORATION 14820 NE 36TH STREET REDMOND WA US 98052 + +American Express Global Business Travel Microsoft Travel +14711 NE 29th Place, Suite 215 +Bellevue, WA 98007 +Phone: +1 (669) 210-8041 + + + + +BILLING CODE : 1010-10010110 +Invoice Information + + + + + + +Invoice Details +Ticket Number + + + + + + + +0277993883295 + + + + + + +Charges +Ticket Base Fare + + + + + + + +306.29 + +Airline Name + +ALASKA AIRLINES + +Ticket Tax Fare 62.01 + +Passenger Name Flight Details + +MARKS/SAM ALBERT +11 Sep 2023 ALASKA AIRLINES +0572 H Class +SEATTLE-TACOMA,WA/RALEIGH DURHAM,NC +13 Sep 2023 ALASKA AIRLINES +0491 M Class +RALEIGH DURHAM,NC/SEATTLE- TACOMA,WA + +Total (USD) Ticket Amount + +368.30 + +Credit Card Information +Charged to Card + + + +AX XXXXXXXXXXX4321 + + + +368.30 + + + + +Payment Details + + + +Charged by Airline +Total Invoice Charge + + + +USD + + + +368.30 +368.30 + +Monday 11 September 2023 + +10:05 AM + +Seattle (SEA) to Durham (RDU) +Airline Booking Ref: ABCXYZ + +Carrier: ALASKA AIRLINES + +Flight: AS 572 + +Status: Confirmed + +Operated By: ALASKA AIRLINES +Origin: Seattle, WA, Seattle-Tacoma International Apt (SEA) + +Departing: Monday 11 September 2023 at 10:05 AM Destination: Durham, Raleigh, Raleigh (RDU) Arriving: Monday 11 September 2023 at 06:15 PM +Additional Information + +Departure Terminal: Not Applicable + +Arrival Terminal: TERMINAL 2 + + +Class: ECONOMY +Aircraft Type: Boeing 737-900 +Meal Service: Not Applicable +Frequent Flyer Number: Not Applicable +Number of Stops: 0 +Greenhouse Gas Emissions: 560 kg CO2e / person + + +Distance: 2354 Miles Estimated Time: 05 hours 10 minutes +Seat: 24A + + +THE WESTIN RALEIGH DURHAM AP +Address: 3931 Macaw Street, Raleigh, NC, 27617, US +Phone: (1) 919-224-1400 Fax: (1) 919-224-1401 +Check In Date: Monday 11 September 2023 Check Out Date: Wednesday 13 September 2023 Number Of Nights: 2 +Rate: USD 280.00 per night may be subject to local taxes and service charges +Guaranteed to: AX XXXXXXXXXXX4321 + +Reference Number: 987654 +Additional Information +Membership ID: 123456789 +CANCEL PERMITTED UP TO 1 DAYS BEFORE CHECKIN + +Status: Confirmed + + +Corporate Id: Not Applicable + +Number Of Rooms: 1 + +Wednesday 13 September 2023 + +07:15 PM + +Durham (RDU) to Seattle (SEA) +Airline Booking Ref: ABCXYZ + +Carrier: ALASKA AIRLINES + +Flight: AS 491 + +Status: Confirmed + +Operated By: ALASKA AIRLINES +Origin: Durham, Raleigh, Raleigh (RDU) +Departing: Wednesday 13 September 2023 at 07:15 PM + + + +Departure Terminal: TERMINAL 2 + +Destination: Seattle, WA, Seattle-Tacoma International Apt (SEA) +Arriving: Wednesday 13 September 2023 at 09:59 PM Arrival Terminal: Not Applicable +Additional Information + + +Class: ECONOMY +Aircraft Type: Boeing 737-900 +Meal Service: Not Applicable +Frequent Flyer Number: Not Applicable +Number of Stops: 0 +Greenhouse Gas Emissions: 560 kg CO2e / person + + +Distance: 2354 Miles Estimated Time: 05 hours 44 minutes +Seat: 16A + + + +Greenhouse Gas Emissions +Total Greenhouse Gas Emissions for this trip is: 1120 kg CO2e / person +Air Fare Information + +Routing : ONLINE RESERVATION +Total Fare : USD 368.30 +Additional Messages +FOR 24X7 Travel Reservations Please Call 1-669-210-8041 Unable To Use Requested As Frequent Flyer Program Invalid Use Of Frequent Flyer Number 0123XYZ Please Contact Corresponding Frequent Travel Program Support Desk For Assistance +Trip Name-Trip From Seattle To Raleigh/Durham +This Ticket Is Nonrefundable. Changes Or Cancellations Must Be Made Prior To Scheduled Flight Departure +All Changes Must Be Made On Same Carrier And Will Be Subject To Service Fee And Difference In Airfare +******************************************************* +Please Be Advised That Certain Mandatory Hotel-Imposed Charges Including But Not Limited To Daily Resort Or Facility Fees May Be Applicable To Your Stay And Payable To The Hotel Operator At Check-Out From The Property. You May Wish To Inquire With The Hotel Before Your Trip Regarding The Existence And Amount Of Such Charges. +******************************************************* +Hotel Cancel Policies Vary Depending On The Property And Date. If You Have Questions Regarding Cancellation Fees Please Call The Travel Office. +Important Information +COVID-19 Updates: Click here to access Travel Vitals https://travelvitals.amexgbt.com for the latest information and advisories compiled by American Express Global Business Travel. + +Carbon Emissions: The total emissions value for this itinerary includes air travel only. Emissions for each individual flight are displayed in the flight details section. For more information on carbon emissions please refer to https://www.amexglobalbusinesstravel.com/sustainable-products-and-platforms. + +For important information regarding your booking in relation to the conditions applying to your booking, managing your booking and travel advisory, please refer to www.amexglobalbusinesstravel.com/booking-info. + +GBT Travel Services UK Limited (GBT UK) and its authorized sublicensees (including Ovation Travel Group and Egencia) use certain trademarks and service marks of American Express Company or its subsidiaries (American Express) in the American Express Global Business Travel and American Express Meetings & Events brands and in connection with its business for permitted uses only under a limited license from American Express (Licensed Marks). The Licensed Marks are trademarks or service marks of, and the property of, American Express. GBT UK is a subsidiary of Global Business Travel Group, Inc. (NYSE: GBTG). American Express holds a minority interest in GBTG, which operates as a separate company from American Express. diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index f282a1ee4c88..8511387b85da 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -31,7 +31,7 @@ await OpenAIAssistantAgent.CreateAsync( { Instructions = HostInstructions, Name = HostName, - Model = this.Model, + ModelName = this.Model, }); // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 628fdf2aa171..f8efab0ea0fd 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -38,6 +38,7 @@ + diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 50d7d9d3b351..4738f5bbe528 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -54,7 +55,7 @@ public static async Task CreateMessageAsync(AssistantClient client, string threa throw new KernelException($"Invalid message role: {message.Role}"); } - if (string.IsNullOrWhiteSpace(message.Content)) + if (message.Items.Count == 0) { return; } @@ -62,14 +63,44 @@ public static async Task CreateMessageAsync(AssistantClient client, string threa MessageCreationOptions options = new() { - //Role = message.Role.ToMessageRole(), // %%% BUG: ASSIGNABLE + //Role = message.Role.ToMessageRole(), // %%% BUG: ASSIGNABLE (Allow assistant or user) }; + if (message.Metadata != null) + { + foreach (var metadata in message.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value?.ToString() ?? string.Empty); + } + } + await client.CreateMessageAsync( threadId, - [message.Content], // %%% + GetMessageContents(), options, cancellationToken).ConfigureAwait(false); + + IEnumerable GetMessageContents() + { + foreach (KernelContent content in message.Items) + { + if (content is TextContent textContent) + { + yield return MessageContent.FromText(content.ToString()); + } + else if (content is ImageContent imageContent) + { + yield return MessageContent.FromImageUrl( + imageContent.Uri != null ? + imageContent.Uri : + new Uri(Convert.ToBase64String(imageContent.Data?.ToArray() ?? []))); // %%% WUT A MESS - API BUG? + } + else if (content is FileReferenceContent fileContent) + { + options.Attachments.Add(new MessageCreationAttachment(fileContent.FileId, [new CodeInterpreterToolDefinition()])); // %%% WUT A MESS - TOOLS? + } + } + } } /// @@ -91,7 +122,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist if (!string.IsNullOrWhiteSpace(message.AssistantId) && !agentNames.TryGetValue(message.AssistantId, out assistantName)) { - Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // %%% CANCEL TOKEN + Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // %%% BUG CANCEL TOKEN if (!string.IsNullOrWhiteSpace(assistant.Name)) { agentNames.Add(assistant.Id, assistant.Name); @@ -148,9 +179,19 @@ public static async IAsyncEnumerable InvokeAsync( RunCreationOptions options = new() { - //InstructionsOverride = agent.Instructions, - //ParallelToolCallsEnabled = true, // %%% - //ResponseFormat = %%% + //AdditionalInstructions, // %%% NO ??? + //AdditionalMessages // %%% NO ??? + //InstructionsOverride = agent.Instructions, // %%% RUN OVERRIDE + //MaxCompletionTokens // %%% RUN OVERRIDE + //MaxPromptTokens // %%% RUN OVERRIDE + //ModelOverride, // %%% RUN OVERRIDE + //NucleusSamplingFactor // %%% RUN OVERRIDE + //ParallelToolCallsEnabled = true, // %%% RUN OVERRIDE + AGENT + //ResponseFormat = // %%% RUN OVERRIDE + //ToolConstraint // %%% RUN OVERRIDE + AGENT + //ToolsOverride // %%% RUN OVERRIDE + //Temperature = agent.Definition.Temperature, // %%% RUN OVERRIDE + //TruncationStrategy // %%% RUN OVERRIDE + AGENT }; options.ToolsOverride.AddRange(agent.Tools); @@ -198,7 +239,7 @@ public static async IAsyncEnumerable InvokeAsync( // Process tool output ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - await client.SubmitToolOutputsToRunAsync(run, toolOutputs).ConfigureAwait(false); // %%% CANCEL TOKEN + await client.SubmitToolOutputsToRunAsync(run, toolOutputs).ConfigureAwait(false); // %%% BUG CANCEL TOKEN } if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled @@ -255,7 +296,7 @@ public static async IAsyncEnumerable InvokeAsync( foreach (MessageContent itemContent in message.Content) { - ChatMessageContent? content = null; + ChatMessageContent? content = null; // %%% ITEMS // Process text content if (!string.IsNullOrEmpty(itemContent.Text)) @@ -384,11 +425,11 @@ private static AnnotationContent GenerateAnnotationContent(TextAnnotation annota { string? fileId = null; - if (string.IsNullOrEmpty(annotation.OutputFileId)) + if (!string.IsNullOrEmpty(annotation.OutputFileId)) { fileId = annotation.OutputFileId; } - else if (string.IsNullOrEmpty(annotation.InputFileId)) + else if (!string.IsNullOrEmpty(annotation.InputFileId)) { fileId = annotation.InputFileId; } diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index bce45b415321..45e78cb6dd34 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -22,34 +22,32 @@ internal static class OpenAIClientFactory /// public static OpenAIClient CreateClient(OpenAIConfiguration config) { - OpenAIClient client; - // Inspect options switch (config.Type) { - case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAIKey: - { - AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); - client = new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); - break; - } - case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAICredential: + case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAI: { AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); - client = new AzureOpenAIClient(config.Endpoint, config.Credentials!, clientOptions); - break; + + if (config.Credential is not null) + { + return new AzureOpenAIClient(config.Endpoint, config.Credential, clientOptions); + } + if (!string.IsNullOrEmpty(config.ApiKey)) + { + return new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); + } + + throw new KernelException($"Unsupported configuration type: {config.Type}"); } case OpenAIConfiguration.OpenAIConfigurationType.OpenAI: { OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(config); - client = new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); - break; + return new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); } default: - throw new KernelException($"Unsupported configuration type: {config.Type}"); + throw new KernelException($"Unsupported configuration state: {config.Type}"); } - - return client; } private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIConfiguration config) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 129f99642465..ca7fe42ae8e1 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -64,7 +64,7 @@ public static async Task CreateAsync( // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); - Assistant model = await client.CreateAssistantAsync(definition.Model, assistantCreationOptions, cancellationToken).ConfigureAwait(false); + Assistant model = await client.CreateAssistantAsync(definition.ModelName, assistantCreationOptions, cancellationToken).ConfigureAwait(false); // Instantiate the agent return @@ -112,7 +112,7 @@ public static async Task RetrieveAsync( AssistantClient client = CreateClient(config); // Retrieve the assistant - Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // %%% CANCEL TOKEN + Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // %%% BUG CANCEL TOKEN // Instantiate the agent return @@ -127,7 +127,7 @@ public static async Task RetrieveAsync( /// /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(CancellationToken cancellationToken = default) + public async Task CreateThreadAsync(CancellationToken cancellationToken = default) // %%% OPTIONS: MESSAGES / TOOL_RESOURCES { ThreadCreationOptions options = new(); // %%% AssistantThread thread = await this._client.CreateThreadAsync(options, cancellationToken).ConfigureAwait(false); @@ -201,7 +201,7 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public IAsyncEnumerable InvokeAsync( + public IAsyncEnumerable InvokeAsync( // %%% OPTIONS string threadId, CancellationToken cancellationToken = default) { @@ -266,9 +266,12 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod Description = model.Description, Instructions = model.Instructions, EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), - VectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(), Metadata = model.Metadata, - Model = model.Model, + ModelName = model.Model, + EnableJsonResponse = model.ResponseFormat == AssistantResponseFormat.JsonObject, + NucleusSamplingFactor = model.NucleusSamplingFactor, + Temperature = model.Temperature, + VectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(), }; private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) @@ -296,9 +299,9 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss Instructions = definition.Instructions, Name = definition.Name, ToolResources = toolResources, - // %%% ResponseFormat = - // %%% Temperature = - // %%% NucleusSamplingFactor = + ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, + Temperature = definition.Temperature, + NucleusSamplingFactor = definition.NucleusSamplingFactor, }; if (definition.Metadata != null) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 47fd333d348b..a656cd1e6dc2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -11,7 +11,7 @@ public sealed class OpenAIAssistantDefinition /// /// Identifies the AI model targeted by the agent. /// - public string? Model { get; init; } + public string? ModelName { get; init; } /// /// The description of the assistant. @@ -38,14 +38,27 @@ public sealed class OpenAIAssistantDefinition /// public bool EnableCodeInterpreter { get; init; } + /// + /// Set if json response-format is enabled. + /// + public bool EnableJsonResponse { get; init; } + + /// + /// %%% + /// + public float? NucleusSamplingFactor { get; init; } + + /// + /// %%% + /// + public float? Temperature { get; init; } + /// /// Enables file-serach if specified. /// public string? VectorStoreId { get; init; } - // %%% ResponseFormat - // %%% Temperature - // %%% NucleusSamplingFactor + // %%% CODE INTERPRETER FILEIDS /// /// A set of up to 16 key/value pairs that can be attached to an agent, used for diff --git a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs index 26bdefff975d..f03079f73a74 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs @@ -12,8 +12,7 @@ public sealed class OpenAIConfiguration { internal enum OpenAIConfigurationType { - AzureOpenAIKey, - AzureOpenAICredential, + AzureOpenAI, OpenAI, } @@ -31,24 +30,24 @@ public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, Ht ApiKey = apiKey, Endpoint = endpoint, HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAIKey, + Type = OpenAIConfigurationType.AzureOpenAI, }; /// /// %%% /// - /// + /// /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credentials, Uri endpoint, HttpClient? httpClient = null) => + public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) => // %%% VERIFY new() { - Credentials = credentials, + Credential = credential, Endpoint = endpoint, HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAICredential, + Type = OpenAIConfigurationType.AzureOpenAI, }; /// @@ -88,7 +87,7 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId }; internal string? ApiKey { get; init; } - internal TokenCredential? Credentials { get; init; } + internal TokenCredential? Credential { get; init; } internal Uri? Endpoint { get; init; } internal HttpClient? HttpClient { get; init; } internal string? OrganizationId { get; init; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs index 555f3adfb7f3..cefb8d05a855 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs @@ -115,7 +115,7 @@ public OpenAIVectorStoreBuilder WithName(string name) /// /// /// - public async Task CreateAsync(CancellationToken cancellationToken) + public async Task CreateAsync(CancellationToken cancellationToken = default) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); VectorStoreClient client = openAIClient.GetVectorStoreClient(); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index a196d74dd74c..527fdc555135 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -33,7 +33,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() OpenAIAssistantDefinition definition = new() { - Model = "testmodel", + ModelName = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); @@ -62,7 +62,7 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() OpenAIAssistantDefinition definition = new() { - Model = "testmodel", + ModelName = "testmodel", Name = "testname", Description = "testdescription", Instructions = "testinstructions", @@ -94,7 +94,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() OpenAIAssistantDefinition definition = new() { - Model = "testmodel", + ModelName = "testmodel", EnableCodeInterpreter = true, VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, @@ -369,7 +369,7 @@ private Task CreateAgentAsync() OpenAIAssistantDefinition definition = new() { - Model = "testmodel", + ModelName = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 9c19372188ff..602eddd07704 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -20,7 +20,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Id); Assert.Null(definition.Name); - Assert.Null(definition.Model); + Assert.Null(definition.ModelName); Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); @@ -39,7 +39,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() { Id = "testid", Name = "testname", - Model = "testmodel", + ModelName = "testmodel", Instructions = "testinstructions", Description = "testdescription", VectorStoreId = "#vs", @@ -49,7 +49,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("testid", definition.Id); Assert.Equal("testname", definition.Name); - Assert.Equal("testmodel", definition.Model); + Assert.Equal("testmodel", definition.ModelName); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); Assert.Equal("#vs", definition.VectorStoreId); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs similarity index 91% rename from dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs rename to dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs index 67672c17cd4e..ac7caad578ac 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs @@ -7,9 +7,9 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI; /// -/// Unit testing of . +/// Unit testing of . /// -public class OpenAIAssistantConfigurationTests +public class OpenAIConfigurationTests { /// /// Verify initial state. @@ -39,4 +39,6 @@ public void VerifyOpenAIAssistantConfigurationAssignment() Assert.Equal("https://localhost/", config.Endpoint.ToString()); Assert.NotNull(config.HttpClient); } + + // %%% MORE } diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index d274ac7706d7..fbea0341810c 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -85,7 +85,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { Instructions = "Answer questions about the menu.", - Model = modelName, + ModelName = modelName, }); AgentGroupChat chat = new(); diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs index 6c4a85104d54..4e2ecf22537e 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs @@ -82,7 +82,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { Instructions = "Answer questions about the menu.", - Model = modelName, + ModelName = modelName, }); AgentGroupChat chat = new(); From b417af03eb0c678c299c8d5653e4d6b7e0043ac6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 6 Jul 2024 15:53:22 -0700 Subject: [PATCH 038/226] Checkpoint --- .../OpenAI/Internal/AssistantThreadActions.cs | 66 ++++++++++++------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 44 +++++++++++-- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- .../OpenAI/OpenAIAssistantDefinition.cs | 19 ++++-- .../OpenAIAssistantExecutionSettings.cs | 30 +++++++++ .../OpenAIAssistantInvocationSettings.cs | 47 +++++++++++++ 6 files changed, 170 insertions(+), 38 deletions(-) create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 4738f5bbe528..01da0ae44e09 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -91,9 +91,7 @@ IEnumerable GetMessageContents() else if (content is ImageContent imageContent) { yield return MessageContent.FromImageUrl( - imageContent.Uri != null ? - imageContent.Uri : - new Uri(Convert.ToBase64String(imageContent.Data?.ToArray() ?? []))); // %%% WUT A MESS - API BUG? + imageContent.Uri ?? new Uri(Convert.ToBase64String(imageContent.Data?.ToArray() ?? []))); // %%% WUT A MESS - API BUG? } else if (content is FileReferenceContent fileContent) { @@ -159,6 +157,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// The assistant agent to interact with the thread. /// The assistant client /// The thread identifier + /// Optional settings to utilize for the invocation /// The logger to utilize (might be agent or channel scoped) /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. @@ -166,6 +165,7 @@ public static async IAsyncEnumerable InvokeAsync( OpenAIAssistantAgent agent, AssistantClient client, string threadId, + OpenAIAssistantInvocationSettings? invocationSettings, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -174,29 +174,10 @@ public static async IAsyncEnumerable InvokeAsync( throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); } + // Create run logger.LogDebug("[{MethodName}] Creating run for agent/thrad: {AgentId}/{ThreadId}", nameof(InvokeAsync), agent.Id, threadId); - RunCreationOptions options = - new() - { - //AdditionalInstructions, // %%% NO ??? - //AdditionalMessages // %%% NO ??? - //InstructionsOverride = agent.Instructions, // %%% RUN OVERRIDE - //MaxCompletionTokens // %%% RUN OVERRIDE - //MaxPromptTokens // %%% RUN OVERRIDE - //ModelOverride, // %%% RUN OVERRIDE - //NucleusSamplingFactor // %%% RUN OVERRIDE - //ParallelToolCallsEnabled = true, // %%% RUN OVERRIDE + AGENT - //ResponseFormat = // %%% RUN OVERRIDE - //ToolConstraint // %%% RUN OVERRIDE + AGENT - //ToolsOverride // %%% RUN OVERRIDE - //Temperature = agent.Definition.Temperature, // %%% RUN OVERRIDE - //TruncationStrategy // %%% RUN OVERRIDE + AGENT - }; - - options.ToolsOverride.AddRange(agent.Tools); - - // Create run + RunCreationOptions options = GenerateRunCreationOptions(agent, invocationSettings); ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); logger.LogInformation("[{MethodName}] Created run: {RunId}", nameof(InvokeAsync), run.Id); @@ -554,4 +535,41 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } + + private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantAgent agent, OpenAIAssistantInvocationSettings? invocationSettings) + { + int? truncationMessageCount = ResolveExecutionSetting(invocationSettings?.TruncationMessageCount, agent.Definition.ExecutionSettings?.TruncationMessageCount); + + RunCreationOptions options = + new() + { + MaxCompletionTokens = ResolveExecutionSetting(invocationSettings?.MaxCompletionTokens, agent.Definition.ExecutionSettings?.MaxCompletionTokens), + MaxPromptTokens = ResolveExecutionSetting(invocationSettings?.MaxPromptTokens, agent.Definition.ExecutionSettings?.MaxPromptTokens), + ModelOverride = invocationSettings?.ModelName, + NucleusSamplingFactor = ResolveExecutionSetting(invocationSettings?.TopP, agent.Definition.TopP), + ParallelToolCallsEnabled = ResolveExecutionSetting(invocationSettings?.ParallelToolCallsEnabled, agent.Definition.ExecutionSettings?.ParallelToolCallsEnabled), + ResponseFormat = ResolveExecutionSetting(invocationSettings?.EnableJsonResponse, agent.Definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, + Temperature = ResolveExecutionSetting(invocationSettings?.Temperature, agent.Definition.Temperature), + //ToolConstraint // %%% RUN OVERRIDE + AGENT + TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, + }; + + options.ToolsOverride.AddRange(agent.Tools); // %%% + + if (invocationSettings?.Metadata != null) + { + foreach (var metadata in invocationSettings.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty); + } + } + + return options; + } + + private static TValue? ResolveExecutionSetting(TValue? setting, TValue? agentSetting) where TValue : struct + => + setting.HasValue && (!agentSetting.HasValue || !EqualityComparer.Default.Equals(setting.Value, agentSetting.Value)) ? + setting.Value : + null; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index ca7fe42ae8e1..6258bb6eed37 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -15,6 +16,8 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// public sealed class OpenAIAssistantAgent : KernelAgent { + private const string SettingsMetadataKey = "__settings"; + private readonly Assistant _assistant; private readonly AssistantClient _client; private readonly string[] _channelKeys; @@ -201,13 +204,26 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public IAsyncEnumerable InvokeAsync( // %%% OPTIONS + public IAsyncEnumerable InvokeAsync( + string threadId, + CancellationToken cancellationToken = default) + => this.InvokeAsync(threadId, settings: null, cancellationToken); + + /// + /// Invoke the assistant on the specified thread. + /// + /// The thread identifier + /// %%% + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + public IAsyncEnumerable InvokeAsync( string threadId, + OpenAIAssistantInvocationSettings? settings, CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(this, this._client, threadId, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(this, this._client, threadId, settings, this.Logger, cancellationToken); } /// @@ -258,7 +274,15 @@ private OpenAIAssistantAgent( } private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model) - => + { + OpenAIAssistantExecutionSettings? settings = null; + + if (model.Metadata.TryGetValue(SettingsMetadataKey, out string? settingsJson)) + { + settings = JsonSerializer.Deserialize(settingsJson); + } + + return new() { Id = model.Id, @@ -268,11 +292,13 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), Metadata = model.Metadata, ModelName = model.Model, - EnableJsonResponse = model.ResponseFormat == AssistantResponseFormat.JsonObject, - NucleusSamplingFactor = model.NucleusSamplingFactor, + EnableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject, + TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, VectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(), + ExecutionSettings = settings, }; + } private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { @@ -301,7 +327,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss ToolResources = toolResources, ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, Temperature = definition.Temperature, - NucleusSamplingFactor = definition.NucleusSamplingFactor, + NucleusSamplingFactor = definition.TopP, }; if (definition.Metadata != null) @@ -312,6 +338,12 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss } } + if (definition.ExecutionSettings != null) + { + string settingsJson = JsonSerializer.Serialize(definition.ExecutionSettings); + assistantCreationOptions.Metadata[SettingsMetadataKey] = settingsJson; + } + if (definition.EnableCodeInterpreter) { assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 19bbf4eb3294..17b47bc404a2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -31,7 +31,7 @@ protected override IAsyncEnumerable InvokeAsync( { agent.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationSettings: null, this.Logger, cancellationToken); } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index a656cd1e6dc2..72de12548365 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -4,7 +4,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// The data associated with an assistant's definition. +/// Defines an assistant. /// public sealed class OpenAIAssistantDefinition { @@ -44,15 +44,22 @@ public sealed class OpenAIAssistantDefinition public bool EnableJsonResponse { get; init; } /// - /// %%% + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. /// - public float? NucleusSamplingFactor { get; init; } + public IReadOnlyDictionary? Metadata { get; init; } /// /// %%% /// public float? Temperature { get; init; } + /// + /// %%% + /// + public float? TopP { get; init; } + /// /// Enables file-serach if specified. /// @@ -61,9 +68,7 @@ public sealed class OpenAIAssistantDefinition // %%% CODE INTERPRETER FILEIDS /// - /// A set of up to 16 key/value pairs that can be attached to an agent, used for - /// storing additional information about that object in a structured format.Keys - /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// %%% /// - public IReadOnlyDictionary? Metadata { get; init; } + public OpenAIAssistantExecutionSettings? ExecutionSettings { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs new file mode 100644 index 000000000000..c01996cd5f76 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft. All rights reserved. +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Defines agent execution settings. +/// +public class OpenAIAssistantExecutionSettings +{ + /// + /// %%% + /// + public int? MaxCompletionTokens { get; init; } + + /// + /// %%% + /// + public int? MaxPromptTokens { get; init; } + + /// + /// %%% + /// + public bool? ParallelToolCallsEnabled { get; init; } + + //ToolConstraint // %%% + + /// + /// %%% + /// + public int? TruncationMessageCount { get; init; } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs new file mode 100644 index 000000000000..6183082f2a43 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Defines per invocation execution settings. +/// +public sealed class OpenAIAssistantInvocationSettings : OpenAIAssistantExecutionSettings +{ + /// + /// Identifies the AI model targeted by the agent. + /// + public string? ModelName { get; init; } + + /// + /// Set if code_interpreter tool is enabled. + /// + public bool EnableCodeInterpreter { get; init; } + + /// + /// Set if file_search tool is enabled. + /// + public bool EnableFileSearch { get; init; } + + /// + /// Set if json response-format is enabled. + /// + public bool? EnableJsonResponse { get; init; } + + /// + /// %%% + /// + public float? Temperature { get; init; } + + /// + /// %%% + /// + public float? TopP { get; init; } + + /// + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// + public IReadOnlyDictionary? Metadata { get; init; } +} From 5a3bb302f87607e6932db000d045d1b1b1194f4b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sat, 6 Jul 2024 15:59:40 -0700 Subject: [PATCH 039/226] Unit-test fix --- dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 527fdc555135..e9873085c79d 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -310,7 +310,7 @@ await OpenAIAssistantAgent.ListDefinitionsAsync( this.SetupResponses( HttpStatusCode.OK, ResponseContent.ListAgentsPageMore, - ResponseContent.ListAgentsPageMore); + ResponseContent.ListAgentsPageFinal); messages = await OpenAIAssistantAgent.ListDefinitionsAsync( From 366d3421ca3660e44abc1172fafb9305ac6ce71f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 13:16:54 -0700 Subject: [PATCH 040/226] Settings update --- .../OpenAIAssistantExecutionSettings.cs | 8 +++++-- .../OpenAIAssistantInvocationSettings.cs | 24 ++++++++++++++++++- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs index c01996cd5f76..d9352d9f91e0 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs @@ -4,7 +4,10 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Defines agent execution settings. /// -public class OpenAIAssistantExecutionSettings +/// +/// %%% +/// +public sealed class OpenAIAssistantExecutionSettings { /// /// %%% @@ -21,7 +24,8 @@ public class OpenAIAssistantExecutionSettings /// public bool? ParallelToolCallsEnabled { get; init; } - //ToolConstraint // %%% + //public ToolConstraint? RequiredTool { get; init; } %%% ENUM ??? + //public KernelFunction? RequiredToolFunction { get; init; } %%% PLUGIN ??? /// /// %%% diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs index 6183082f2a43..e684c07e6a57 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Defines per invocation execution settings. /// -public sealed class OpenAIAssistantInvocationSettings : OpenAIAssistantExecutionSettings +public sealed class OpenAIAssistantInvocationSettings { /// /// Identifies the AI model targeted by the agent. @@ -28,6 +28,28 @@ public sealed class OpenAIAssistantInvocationSettings : OpenAIAssistantExecution /// public bool? EnableJsonResponse { get; init; } + /// + /// %%% + /// + public int? MaxCompletionTokens { get; init; } + + /// + /// %%% + /// + public int? MaxPromptTokens { get; init; } + + /// + /// %%% + /// + public bool? ParallelToolCallsEnabled { get; init; } + + //public ToolConstraint? RequiredTool { get; init; } %%% ENUM ??? + //public KernelFunction? RequiredToolFunction { get; init; } %%% PLUGIN ??? + + /// + /// %%% + /// + public int? TruncationMessageCount { get; init; } /// /// %%% /// From 0cdb397c0feb0427ac2034d7563646970d961bf7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 13:53:08 -0700 Subject: [PATCH 041/226] Intermediate cleanup --- .../OpenAIAssistant_FileManipulation.cs | 12 +-- .../Agents/OpenAIAssistant_FileSearch.cs | 6 +- .../Step6_DependencyInjection.cs | 2 +- .../Step8_OpenAIAssistant.cs | 8 +- .../OpenAI/Internal/AssistantThreadActions.cs | 4 +- .../OpenAI/Internal/OpenAIClientFactory.cs | 6 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 6 +- .../OpenAI/OpenAIAssistantDefinition.cs | 11 ++- .../OpenAIAssistantExecutionSettings.cs | 17 ++-- .../OpenAIAssistantInvocationSettings.cs | 30 ++++-- .../src/Agents/OpenAI/OpenAIConfiguration.cs | 95 +++++++++++-------- dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 2 +- .../IntegrationTests/IntegrationTests.csproj | 5 + .../Agents/OpenAIAssistantAgentTests.cs | 2 +- .../samples/InternalUtilities/BaseTest.cs | 2 +- 15 files changed, 127 insertions(+), 81 deletions(-) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs index 1124f385709d..e0fbb920e2e9 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs @@ -23,7 +23,7 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); + OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); // %%% HACK FileClient fileClient = rootClient.GetFileClient(); await using Stream fileStream = EmbeddedResource.ReadStream("sales.csv")!; @@ -33,7 +33,7 @@ await fileClient.UploadFileAsync( "sales.csv", FileUploadPurpose.Assistants); - //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); %%% + //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); // %%% USE THIS //OpenAIFileReference uploadFile = // await fileService.UploadContentAsync( // new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), @@ -63,8 +63,8 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); - //await fileService.DeleteFileAsync(uploadFile.Id); %%% - await fileClient.DeleteFileAsync(fileInfo.Id); + //await fileService.DeleteFileAsync(uploadFile.Id); // %%% USE THIS + await fileClient.DeleteFileAsync(fileInfo.Id); // %%% HACK } // Local function to invoke agent and display the conversation messages. @@ -84,10 +84,10 @@ async Task InvokeAgentAsync(string input) foreach (AnnotationContent annotation in message.Items.OfType()) { - Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); + Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); // %%% HACK BinaryData fileData = await fileClient.DownloadFileAsync(annotation.FileId!); Console.WriteLine(Encoding.Default.GetString(fileData.ToArray())); - //BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); %%% + //BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); // %%% USE THIS //byte[] byteContent = fileContent.Data?.ToArray() ?? []; //Console.WriteLine(Encoding.Default.GetString(byteContent)); } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs index e73ad5aa9378..aeb237984a83 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs @@ -23,7 +23,7 @@ public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(out [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { - OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); + OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); // %%% HACK FileClient fileClient = rootClient.GetFileClient(); Stream fileStream = EmbeddedResource.ReadStream("travelinfo.txt")!; // %%% USING @@ -40,7 +40,7 @@ await fileClient.UploadFileAsync( OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); - //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); %%% + //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); // %%% USE THIS //OpenAIFileReference uploadFile = // await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), // new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); @@ -53,7 +53,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { ModelName = this.Model, - VectorStoreId = vectorStore.Id, // %%% + VectorStoreId = vectorStore.Id, }); // Create a chat for agent interaction. diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs index 24bcf8bd7c6b..2394686077b9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs @@ -38,7 +38,7 @@ public async Task UseDependencyInjectionToCreateAgentAsync() if (this.UseOpenAIConfig) { - //serviceContainer.AddOpenAIChatCompletion( %%% + //serviceContainer.AddOpenAIChatCompletion( // %%% CONNECTOR IMPL // TestConfiguration.OpenAI.ChatModelId, // TestConfiguration.OpenAI.ApiKey); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 8511387b85da..458c79fd5347 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -26,7 +26,7 @@ public async Task UseSingleOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)), // %%% MODES + config: GetOpenAIConfiguration(), new() { Instructions = HostInstructions, @@ -72,6 +72,12 @@ async Task InvokeAgentAsync(string input) } } + private OpenAIConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIConfiguration.ForOpenAI(this.ApiKey) : + OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + private sealed class MenuPlugin { public const string CorrelationIdArgument = "correlationId"; diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 01da0ae44e09..c2516fb3b494 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -550,11 +550,11 @@ private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantAgen ParallelToolCallsEnabled = ResolveExecutionSetting(invocationSettings?.ParallelToolCallsEnabled, agent.Definition.ExecutionSettings?.ParallelToolCallsEnabled), ResponseFormat = ResolveExecutionSetting(invocationSettings?.EnableJsonResponse, agent.Definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, Temperature = ResolveExecutionSetting(invocationSettings?.Temperature, agent.Definition.Temperature), - //ToolConstraint // %%% RUN OVERRIDE + AGENT + //ToolConstraint // %%% TODO TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, }; - options.ToolsOverride.AddRange(agent.Tools); // %%% + options.ToolsOverride.AddRange(agent.Tools); if (invocationSettings?.Metadata != null) { diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index 45e78cb6dd34..0f6b88c5dfa1 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -16,10 +16,10 @@ internal static class OpenAIClientFactory private const string SingleSpaceKey = " "; /// - /// %%% + /// Creates an OpenAI client based on the provided configuration. /// - /// - /// + /// Configuration required to target a specific Open AI service + /// An initialized Open AI client public static OpenAIClient CreateClient(OpenAIConfiguration config) { // Inspect options diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 6258bb6eed37..5be170398163 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -23,7 +23,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent private readonly string[] _channelKeys; /// - /// %%% + /// The assistant definition. /// public OpenAIAssistantDefinition Definition { get; private init; } @@ -34,7 +34,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent public bool IsDeleted { get; private set; } /// - /// %%% + /// Defines polling behavior for run processing /// public RunPollingConfiguration Polling { get; } = new(); @@ -213,7 +213,7 @@ public IAsyncEnumerable InvokeAsync( /// Invoke the assistant on the specified thread. /// /// The thread identifier - /// %%% + /// Optional invocation settings /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. public IAsyncEnumerable InvokeAsync( diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 72de12548365..7a206685a3ec 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -51,13 +51,18 @@ public sealed class OpenAIAssistantDefinition public IReadOnlyDictionary? Metadata { get; init; } /// - /// %%% + /// The sampling temperature to use, between 0 and 2. /// public float? Temperature { get; init; } /// - /// %%% + /// An alternative to sampling with temperature, called nucleus sampling, where the model + /// considers the results of the tokens with top_p probability mass. + /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. /// + /// + /// Recommended to set this or temperature but not both. + /// public float? TopP { get; init; } /// @@ -68,7 +73,7 @@ public sealed class OpenAIAssistantDefinition // %%% CODE INTERPRETER FILEIDS /// - /// %%% + /// Default execution settings for each agent invocation. /// public OpenAIAssistantExecutionSettings? ExecutionSettings { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs index d9352d9f91e0..6969310ad83c 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs @@ -2,33 +2,34 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Defines agent execution settings. +/// Defines agent execution settings for each invocation. /// /// -/// %%% +/// These settings are persisted as a single entry of the agent's metadata with key: "__settings" /// public sealed class OpenAIAssistantExecutionSettings { /// - /// %%% + /// The maximum number of completion tokens that may be used over the course of the run. /// public int? MaxCompletionTokens { get; init; } /// - /// %%% + /// The maximum number of prompt tokens that may be used over the course of the run. /// public int? MaxPromptTokens { get; init; } /// - /// %%% + /// Enables parallel function calling during tool use. Enabled by default. + /// Use this property to disable. /// public bool? ParallelToolCallsEnabled { get; init; } - //public ToolConstraint? RequiredTool { get; init; } %%% ENUM ??? - //public KernelFunction? RequiredToolFunction { get; init; } %%% PLUGIN ??? + //public ToolConstraint? RequiredTool { get; init; } // %%% ENUM ??? + //public KernelFunction? RequiredToolFunction { get; init; } // %%% PLUGIN ??? /// - /// %%% + /// When set, the thread will be truncated to the N most recent messages in the thread. /// public int? TruncationMessageCount { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs index e684c07e6a57..2e9c61eb05e3 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs @@ -4,12 +4,15 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Defines per invocation execution settings. +/// Defines per invocation execution settings that override the assistant's default settings. /// +/// +/// Not applicable to usage. +/// public sealed class OpenAIAssistantInvocationSettings { /// - /// Identifies the AI model targeted by the agent. + /// Override the AI model targeted by the agent. /// public string? ModelName { get; init; } @@ -29,35 +32,42 @@ public sealed class OpenAIAssistantInvocationSettings public bool? EnableJsonResponse { get; init; } /// - /// %%% + /// The maximum number of completion tokens that may be used over the course of the run. /// public int? MaxCompletionTokens { get; init; } /// - /// %%% + /// The maximum number of prompt tokens that may be used over the course of the run. /// public int? MaxPromptTokens { get; init; } /// - /// %%% + /// Enables parallel function calling during tool use. Enabled by default. + /// Use this property to disable. /// public bool? ParallelToolCallsEnabled { get; init; } - //public ToolConstraint? RequiredTool { get; init; } %%% ENUM ??? - //public KernelFunction? RequiredToolFunction { get; init; } %%% PLUGIN ??? + //public ToolConstraint? RequiredTool { get; init; } // %%% ENUM ??? + //public KernelFunction? RequiredToolFunction { get; init; } // %%% PLUGIN ??? /// - /// %%% + /// When set, the thread will be truncated to the N most recent messages in the thread. /// public int? TruncationMessageCount { get; init; } + /// - /// %%% + /// The sampling temperature to use, between 0 and 2. /// public float? Temperature { get; init; } /// - /// %%% + /// An alternative to sampling with temperature, called nucleus sampling, where the model + /// considers the results of the tokens with top_p probability mass. + /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. /// + /// + /// Recommended to set this or temperature but not both. + /// public float? TopP { get; init; } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs index f03079f73a74..54b7eed768aa 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Configuration for OpenAI services. +/// Configuration to target a specific Open AI service. /// public sealed class OpenAIConfiguration { @@ -23,15 +23,20 @@ internal enum OpenAIConfigurationType /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) => - // %%% VERIFY - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAI, - }; + public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); + Verify.NotNull(endpoint, nameof(endpoint)); + + return + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.AzureOpenAI, + }; + } /// /// %%% @@ -40,15 +45,20 @@ public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, Ht /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) => - // %%% VERIFY - new() - { - Credential = credential, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAI, - }; + public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNull(credential, nameof(credential)); + Verify.NotNull(endpoint, nameof(endpoint)); + + return + new() + { + Credential = credential, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.AzureOpenAI, + }; + } /// /// %%% @@ -57,16 +67,19 @@ public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri /// /// /// - public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) => - // %%% VERIFY - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIConfigurationType.OpenAI, - }; + public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) + { + Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); + return + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + Type = OpenAIConfigurationType.OpenAI, + }; + } /// /// %%% /// @@ -75,16 +88,22 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, /// /// /// - public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) => - // %%% VERIFY - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - OrganizationId = organizationId, - Type = OpenAIConfigurationType.OpenAI, - }; + public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) + { + Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); + Verify.NotNullOrWhiteSpace(organizationId, nameof(organizationId)); + Verify.NotNull(endpoint, nameof(endpoint)); + + return + new() + { + ApiKey = apiKey, + Endpoint = endpoint, + HttpClient = httpClient, + OrganizationId = organizationId, + Type = OpenAIConfigurationType.OpenAI, + }; + } internal string? ApiKey { get; init; } internal TokenCredential? Credential { get; init; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs index 94d59cb88e7a..9fc092d5e29a 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs @@ -73,7 +73,7 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// public async IAsyncEnumerable GetFilesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { - await foreach (VectorStoreFileAssociation file in this._client.GetFileAssociationsAsync(this.VectorStoreId, ListOrder.NewestFirst, filter: null, cancellationToken).ConfigureAwait(false)) // %%% FILTER + await foreach (VectorStoreFileAssociation file in this._client.GetFileAssociationsAsync(this.VectorStoreId, ListOrder.NewestFirst, filter: null, cancellationToken).ConfigureAwait(false)) { yield return file.FileId; } diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index df5afa473ce7..3d1b93a71253 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -17,6 +17,7 @@ + @@ -156,4 +157,8 @@ Always + + + + \ No newline at end of file diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs index 4e2ecf22537e..d084b0fd0ed5 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs @@ -35,7 +35,7 @@ public sealed class OpenAIAssistantAgentTests [InlineData("What is the special soup?", "Clam Chowder")] public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) { - OpenAISettings openAISettings = this._configuration.GetSection("OpenAI").Get(); + OpenAISettings openAISettings = this._configuration.GetSection("OpenAI").Get()!; Assert.NotNull(openAISettings); await this.ExecuteAgentAsync( diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 671524865d33..56d6d147549b 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -42,7 +42,7 @@ protected Kernel CreateKernelWithChatCompletion() if (this.UseOpenAIConfig) { - //builder.AddOpenAIChatCompletion( // %%% + //builder.AddOpenAIChatCompletion( // %%% CONNECTOR // TestConfiguration.OpenAI.ChatModelId, // TestConfiguration.OpenAI.ApiKey); } From 192d59908900a1b8c0c294c82de5e03c6f877cfe Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:01:44 -0700 Subject: [PATCH 042/226] More file support --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 22 ++++++++++++++----- .../OpenAI/OpenAIAssistantDefinition.cs | 7 ++++-- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 5be170398163..1bd9c9080288 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -289,6 +289,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod Name = model.Name, Description = model.Description, Instructions = model.Instructions, + CodeInterpterFileIds = (IReadOnlyList?)(model.ToolResources?.CodeInterpreter?.FileIds), EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), Metadata = model.Metadata, ModelName = model.Model, @@ -303,18 +304,29 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { bool enableFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); + bool hasCodeInterpreterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; ToolResources? toolResources = null; - if (enableFileSearch) + if (enableFileSearch || hasCodeInterpreterFiles) { toolResources = new ToolResources() { - FileSearch = new FileSearchToolResources() - { - VectorStoreIds = [definition.VectorStoreId!], - } + FileSearch = + enableFileSearch ? + new FileSearchToolResources() + { + VectorStoreIds = [definition.VectorStoreId!], + } : + null, + CodeInterpreter = + hasCodeInterpreterFiles ? + new CodeInterpreterToolResources() + { + FileIds = (IList)definition.CodeInterpterFileIds!, + } : + null, }; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 7a206685a3ec..b2e17e5fb17d 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -33,6 +33,11 @@ public sealed class OpenAIAssistantDefinition /// public string? Name { get; init; } + /// + /// Optional file-ids made available to the code_interpreter tool. + /// + public IReadOnlyList? CodeInterpterFileIds { get; init; } + /// /// Set if code-interpreter is enabled. /// @@ -70,8 +75,6 @@ public sealed class OpenAIAssistantDefinition /// public string? VectorStoreId { get; init; } - // %%% CODE INTERPRETER FILEIDS - /// /// Default execution settings for each agent invocation. /// From 43e3579788f1b69d969706a6f1c38887e945e70d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:17:17 -0700 Subject: [PATCH 043/226] File-service clean-up --- .../OpenAIAssistant_FileManipulation.cs | 38 ++++++------------- .../Agents/OpenAIAssistant_FileSearch.cs | 26 ++++--------- 2 files changed, 20 insertions(+), 44 deletions(-) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs index e0fbb920e2e9..69b249801727 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs @@ -4,8 +4,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI; -using OpenAI.Files; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Resources; namespace Agents; @@ -23,21 +22,11 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); // %%% HACK - FileClient fileClient = rootClient.GetFileClient(); - - await using Stream fileStream = EmbeddedResource.ReadStream("sales.csv")!; - OpenAIFileInfo fileInfo = - await fileClient.UploadFileAsync( - fileStream, - "sales.csv", - FileUploadPurpose.Assistants); - - //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); // %%% USE THIS - //OpenAIFileReference uploadFile = - // await fileService.UploadContentAsync( - // new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), - // new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); + OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIFileReference uploadFile = + await fileService.UploadContentAsync( + new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), + new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); // Define the agent OpenAIAssistantAgent agent = @@ -63,8 +52,7 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); - //await fileService.DeleteFileAsync(uploadFile.Id); // %%% USE THIS - await fileClient.DeleteFileAsync(fileInfo.Id); // %%% HACK + await fileService.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -73,7 +61,7 @@ async Task InvokeAgentAsync(string input) chat.AddChatMessage( new(AuthorRole.User, content: null) { - Items = [new TextContent(input), new FileReferenceContent(fileInfo.Id)] + Items = [new TextContent(input), new FileReferenceContent(uploadFile.Id)] }); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); @@ -84,12 +72,10 @@ async Task InvokeAgentAsync(string input) foreach (AnnotationContent annotation in message.Items.OfType()) { - Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); // %%% HACK - BinaryData fileData = await fileClient.DownloadFileAsync(annotation.FileId!); - Console.WriteLine(Encoding.Default.GetString(fileData.ToArray())); - //BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); // %%% USE THIS - //byte[] byteContent = fileContent.Data?.ToArray() ?? []; - //Console.WriteLine(Encoding.Default.GetString(byteContent)); + Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); + BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); + byte[] byteContent = fileContent.Data?.ToArray() ?? []; + Console.WriteLine(Encoding.Default.GetString(byteContent)); } } } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs index aeb237984a83..1fa2ca2de885 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs @@ -3,10 +3,9 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI.Files; -using OpenAI; -using Resources; +using Microsoft.SemanticKernel.Connectors.OpenAI; using OpenAI.VectorStores; +using Resources; namespace Agents; @@ -23,28 +22,18 @@ public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(out [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { - OpenAIClient rootClient = OpenAIClientFactory.CreateClient(GetOpenAIConfiguration()); // %%% HACK - FileClient fileClient = rootClient.GetFileClient(); - - Stream fileStream = EmbeddedResource.ReadStream("travelinfo.txt")!; // %%% USING - OpenAIFileInfo fileInfo = - await fileClient.UploadFileAsync( - fileStream, - "travelinfo.txt", - FileUploadPurpose.Assistants); + OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIFileReference uploadFile = + await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), + new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); VectorStore vectorStore = await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) - .AddFile(fileInfo.Id) + .AddFile(uploadFile.Id) .CreateAsync(); OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); - //OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); // %%% USE THIS - //OpenAIFileReference uploadFile = - // await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - // new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); - // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( @@ -70,6 +59,7 @@ await OpenAIAssistantAgent.CreateAsync( { await agent.DeleteAsync(); await openAIStore.DeleteAsync(); + await fileService.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. From 417f2b4901cdd343265c3296f0961c28824f1997 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:24:48 -0700 Subject: [PATCH 044/226] Refine content generation --- .../OpenAI/Internal/AssistantThreadActions.cs | 90 ++++++------------- 1 file changed, 29 insertions(+), 61 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index c2516fb3b494..4481948dfa50 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -131,19 +131,9 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist foreach (MessageContent itemContent in message.Content) { - ChatMessageContent? content = null; + ChatMessageContent content = GenerateMessageContent(role, assistantName, itemContent); - if (!string.IsNullOrEmpty(itemContent.Text)) - { - content = GenerateTextMessageContent(assistantName, role, itemContent); - } - // Process image content - else if (itemContent.ImageFileId != null) - { - content = GenerateImageFileContent(assistantName, role, itemContent); - } - - if (content is not null) + if (content.Items.Count > 0) { yield return content; } @@ -277,20 +267,9 @@ public static async IAsyncEnumerable InvokeAsync( foreach (MessageContent itemContent in message.Content) { - ChatMessageContent? content = null; // %%% ITEMS - - // Process text content - if (!string.IsNullOrEmpty(itemContent.Text)) - { - content = GenerateTextMessageContent(agent.GetName(), role, itemContent); - } - // Process image content - else if (itemContent.ImageFileId != null) - { - content = GenerateImageFileContent(agent.GetName(), role, itemContent); - } + ChatMessageContent content = GenerateMessageContent(role, agent.Name, itemContent); - if (content is not null) + if (content.Items.Count > 0) { ++messageCount; @@ -425,42 +404,6 @@ private static AnnotationContent GenerateAnnotationContent(TextAnnotation annota }; } - private static ChatMessageContent GenerateImageFileContent(string agentName, AuthorRole role, MessageContent contentImage) - { - return - new ChatMessageContent( - role, - [ - new FileReferenceContent(contentImage.ImageFileId) - ]) - { - AuthorName = agentName, - }; - } - - private static ChatMessageContent? GenerateTextMessageContent(string agentName, AuthorRole role, MessageContent contentMessage) - { - ChatMessageContent? messageContent = null; - - string textContent = contentMessage.Text.Trim(); - - if (!string.IsNullOrWhiteSpace(textContent)) - { - messageContent = - new(role, textContent) - { - AuthorName = agentName - }; - - foreach (TextAnnotation annotation in contentMessage.TextAnnotations) - { - messageContent.Items.Add(GenerateAnnotationContent(annotation)); - } - } - - return messageContent; - } - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string code) { return @@ -503,6 +446,31 @@ private static ChatMessageContent GenerateFunctionResultContent(string agentName return functionCallContent; } + private static ChatMessageContent GenerateMessageContent(AuthorRole role, string? assistantName, MessageContent itemContent) + { + ChatMessageContent content = + new(role, content: null) + { + AuthorName = assistantName, + }; + + if (!string.IsNullOrEmpty(itemContent.Text)) + { + content.Items.Add(new TextContent(itemContent.Text.Trim())); + foreach (TextAnnotation annotation in itemContent.TextAnnotations) + { + content.Items.Add(GenerateAnnotationContent(annotation)); + } + } + // Process image content + else if (itemContent.ImageFileId != null) + { + content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); + } + + return content; + } + private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionSteps, CancellationToken cancellationToken) { Task[] functionTasks = new Task[functionSteps.Length]; From b1084297c32643021cb7d77165c407d9caf24995 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:28:26 -0700 Subject: [PATCH 045/226] Improve sample --- .../ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs index 69b249801727..3b57e8a1c911 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs @@ -35,6 +35,7 @@ await OpenAIAssistantAgent.CreateAsync( config: GetOpenAIConfiguration(), new() { + CodeInterpterFileIds = [uploadFile.Id], EnableCodeInterpreter = true, // Enable code-interpreter ModelName = this.Model, }); @@ -58,11 +59,7 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage( - new(AuthorRole.User, content: null) - { - Items = [new TextContent(input), new FileReferenceContent(uploadFile.Id)] - }); + chat.AddChatMessage(new(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); From ec182607fe176850361c627c39f48eb897df31e3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:42:56 -0700 Subject: [PATCH 046/226] Improve thread creation --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 91 ++++++++++++------- .../OpenAI/OpenAIAssistantDefinition.cs | 2 +- .../OpenAI/OpenAIThreadCreationSettings.cs | 37 ++++++++ 3 files changed, 98 insertions(+), 32 deletions(-) create mode 100644 dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 1bd9c9080288..3db35168aa03 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -130,9 +130,33 @@ public static async Task RetrieveAsync( /// /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(CancellationToken cancellationToken = default) // %%% OPTIONS: MESSAGES / TOOL_RESOURCES + public Task CreateThreadAsync(CancellationToken cancellationToken = default) + => this.CreateThreadAsync(settings: null, cancellationToken); + + /// + /// Create a new assistant thread. + /// + /// %%% + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public async Task CreateThreadAsync(OpenAIThreadCreationSettings? settings, CancellationToken cancellationToken = default) { - ThreadCreationOptions options = new(); // %%% + ThreadCreationOptions options = + new() + { + ToolResources = GenerateToolResources(settings?.VectorStoreId, settings?.CodeInterpterFileIds), + }; + + //options.InitialMessages, // %%% TODO + + if (settings?.Metadata != null) + { + foreach (KeyValuePair item in settings.Metadata) + { + options.Metadata[item.Key] = item.Value; + } + } + AssistantThread thread = await this._client.CreateThreadAsync(options, cancellationToken).ConfigureAwait(false); return thread.Id; @@ -303,40 +327,13 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { - bool enableFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); - bool hasCodeInterpreterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; - - ToolResources? toolResources = null; - - if (enableFileSearch || hasCodeInterpreterFiles) - { - toolResources = - new ToolResources() - { - FileSearch = - enableFileSearch ? - new FileSearchToolResources() - { - VectorStoreIds = [definition.VectorStoreId!], - } : - null, - CodeInterpreter = - hasCodeInterpreterFiles ? - new CodeInterpreterToolResources() - { - FileIds = (IList)definition.CodeInterpterFileIds!, - } : - null, - }; - } - AssistantCreationOptions assistantCreationOptions = new() { Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - ToolResources = toolResources, + ToolResources = GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpterFileIds : null), ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, Temperature = definition.Temperature, NucleusSamplingFactor = definition.TopP, @@ -361,7 +358,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); } - if (enableFileSearch) + if (!string.IsNullOrWhiteSpace(definition.VectorStoreId)) { assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); } @@ -369,6 +366,38 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss return assistantCreationOptions; } + private static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds) + { + bool hasFileSearch = !string.IsNullOrWhiteSpace(vectorStoreId); + bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0; + + ToolResources? toolResources = null; + + if (hasFileSearch || hasCodeInterpreterFiles) + { + toolResources = + new ToolResources() + { + FileSearch = + hasFileSearch ? + new FileSearchToolResources() + { + VectorStoreIds = [vectorStoreId!], + } : + null, + CodeInterpreter = + hasCodeInterpreterFiles ? + new CodeInterpreterToolResources() + { + FileIds = (IList)codeInterpreterFileIds!, + } : + null, + }; + } + + return toolResources; + } + private static AssistantClient CreateClient(OpenAIConfiguration config) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index b2e17e5fb17d..e694a165e912 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -34,7 +34,7 @@ public sealed class OpenAIAssistantDefinition public string? Name { get; init; } /// - /// Optional file-ids made available to the code_interpreter tool. + /// Optional file-ids made available to the code_interpreter tool, if enabled. /// public IReadOnlyList? CodeInterpterFileIds { get; init; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs new file mode 100644 index 000000000000..3a5411d38cb1 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Thread creation settings. +/// +public sealed class OpenAIThreadCreationSettings +{ + /// + /// Optional file-ids made available to the code_interpreter tool, if enabled. + /// + public IReadOnlyList? CodeInterpterFileIds { get; init; } + + /// + /// Set if code-interpreter is enabled. + /// + public bool EnableCodeInterpreter { get; init; } + + /// + /// Optional messages to initialize thread with.. + /// + public IReadOnlyList? Messages { get; init; } + + /// + /// Enables file-serach if specified. + /// + public string? VectorStoreId { get; init; } + + /// + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// + public IReadOnlyDictionary? Metadata { get; init; } +} From 9e9f364dec107d32658dc8f77716e7f9489b1999 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Sun, 7 Jul 2024 14:49:15 -0700 Subject: [PATCH 047/226] Typo --- dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index e694a165e912..53546d44fb5f 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -71,7 +71,7 @@ public sealed class OpenAIAssistantDefinition public float? TopP { get; init; } /// - /// Enables file-serach if specified. + /// Enables file-search if specified. /// public string? VectorStoreId { get; init; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs index 3a5411d38cb1..614ad64f6ba2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs @@ -24,7 +24,7 @@ public sealed class OpenAIThreadCreationSettings public IReadOnlyList? Messages { get; init; } /// - /// Enables file-serach if specified. + /// Enables file-search if specified. /// public string? VectorStoreId { get; init; } From 7b21ee83000001d0df3eeb241fdfb35377438194 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Jul 2024 14:28:50 +0100 Subject: [PATCH 048/226] .Net: Migrate AzureOpenAIAudioToTextService to Azure.AI.OpenAI SDK v2 (#7130) ### Motivation and Context This PR migrates the `AzureOpenAIAudioToTextService` to Azure.AI.OpenAI SDK v2. ### Description 1. The existing `OpenAIAudioToTextExecutionSettings` class is copied to the new `Connectors.AzureOpenAI` project and renamed to `AzureOpenAIAudioToTextExecutionSettings` to represent prompt execution settings for the `AzureOpenAIAudioToTextService`. 2. The `OpenAIAudioToTextExecutionSettings.ResponseFormat` property type has changed from string to the `AudioTranscriptionFormat` enum, which is a breaking change that is tracked in the issue - https://github.com/microsoft/semantic-kernel/issues/70533. 3. The `ClientCore.AudioToText.cs` class is refactored to use the new `AzureOpenAIAudioToTextExecutionSettings` class and to handle the new type of `OpenAIAudioToTextExecutionSettings.ResponseFormat` property. 4. Service collection and kernel builder extension methods are added to register the service in the DI container. 5. Unit and integration tests are added as well. --- ...AzureOpenAIKernelBuilderExtensionsTests.cs | 35 +++ ...eOpenAIServiceCollectionExtensionsTests.cs | 35 +++ .../AzureOpenAIAudioToTextServiceTests.cs | 206 ++++++++++++++ ...OpenAIAudioToTextExecutionSettingsTests.cs | 121 ++++++++ ...AzureOpenAIPromptExecutionSettingsTests.cs | 266 ++++++++++++++++++ ...OpenAITextToAudioExecutionSettingsTests.cs | 107 +++++++ .../Connectors.AzureOpenAI.csproj | 10 - .../Core/ClientCore.AudioToText.cs | 40 ++- .../AzureOpenAIKernelBuilderExtensions.cs | 115 +++++++- .../AzureOpenAIServiceCollectionExtensions.cs | 109 +++++++ .../Services/AzureOpenAIAudioToTextService.cs | 18 +- ...AzureOpenAIAudioToTextExecutionSettings.cs | 222 +++++++++++++++ .../AzureOpenAIAudioToTextTests.cs | 51 ++++ 13 files changed, 1304 insertions(+), 31 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs index bfeebb320ff1..d8e8cdac1658 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIKernelBuilderExtensionsTests.cs @@ -5,6 +5,7 @@ using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; @@ -143,6 +144,40 @@ public void KernelBuilderExtensionsAddAzureOpenAITextToImageService(Initializati #endregion + #region Audio to text + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void KernelBuilderAddAzureOpenAIAudioToTextAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("https://endpoint"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", credentials), + InitializationType.ClientInline => builder.AddAzureOpenAIAudioToText("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.AddAzureOpenAIAudioToText("deployment-name"), + _ => builder + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.IsType(service); + } + + #endregion + public enum InitializationType { ApiKey, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs index 969241f3f23c..2def01271aa6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/AzureOpenAIServiceCollectionExtensionsTests.cs @@ -5,6 +5,7 @@ using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; @@ -143,6 +144,40 @@ public void ServiceCollectionExtensionsAddAzureOpenAITextToImageService(Initiali #endregion + #region Audio to text + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.TokenCredential)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void ServiceCollectionAddAzureOpenAIAudioToTextAddsValidService(InitializationType type) + { + // Arrange + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var client = new AzureOpenAIClient(new Uri("https://endpoint"), "key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", "api-key"), + InitializationType.TokenCredential => builder.Services.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", credentials), + InitializationType.ClientInline => builder.Services.AddAzureOpenAIAudioToText("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.Services.AddAzureOpenAIAudioToText("deployment-name"), + _ => builder.Services + }; + + // Assert + var service = builder.Build().GetRequiredService(); + + Assert.True(service is AzureOpenAIAudioToTextService); + } + + #endregion + public enum InitializationType { ApiKey, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs new file mode 100644 index 000000000000..a0964dafedc0 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Services; +using Moq; +using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureOpenAIAudioToTextExecutionSettings; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIAudioToTextServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + private readonly Mock _mockLoggerFactory; + + public AzureOpenAIAudioToTextServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", "model-id"); + + // Assert + Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); + var service = includeLoggerFactory ? + new AzureOpenAIAudioToTextService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIAudioToTextService("deployment", "https://endpoint", credentials, "model-id"); + + // Assert + Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new AzureOpenAIClient(new Uri("http://host"), "key"); + var service = includeLoggerFactory ? + new AzureOpenAIAudioToTextService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAIAudioToTextService("deployment", client, "model-id"); + + // Assert + Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + } + + [Fact] + public void ItThrowsIfDeploymentNameIsNotProvided() + { + // Act & Assert + Assert.Throws(() => new AzureOpenAIAudioToTextService(" ", "http://host", "apikey")); + Assert.Throws(() => new AzureOpenAIAudioToTextService(" ", azureOpenAIClient: new(new Uri("http://host"), "apikey"))); + Assert.Throws(() => new AzureOpenAIAudioToTextService("", "http://host", "apikey")); + Assert.Throws(() => new AzureOpenAIAudioToTextService("", azureOpenAIClient: new(new Uri("http://host"), "apikey"))); + Assert.Throws(() => new AzureOpenAIAudioToTextService(null!, "http://host", "apikey")); + Assert.Throws(() => new AzureOpenAIAudioToTextService(null!, azureOpenAIClient: new(new Uri("http://host"), "apikey"))); + } + + [Theory] + [MemberData(nameof(ExecutionSettings))] + public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(AzureOpenAIAudioToTextExecutionSettings? settings, Type expectedExceptionType) + { + // Arrange + var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var exception = await Record.ExceptionAsync(() => service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings)); + + // Assert + Assert.NotNull(exception); + Assert.IsType(expectedExceptionType, exception); + } + + [Theory] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default }, "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word }, "word")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment }, "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Word }, "word", "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Segment }, "word", "segment")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Word }, "word", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Default }, "word", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Segment }, "segment", "0")] + [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Default }, "segment", "0")] + public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] granularities, params string[] expectedGranularities) + { + // Arrange + var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { Granularities = granularities }; + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestContent); + Assert.NotNull(result); + + var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); + + foreach (var granularity in expectedGranularities) + { + var expectedMultipart = $"{granularity}\r\n{multiPartBreak}"; + Assert.Contains(expectedMultipart, multiPartData); + } + } + + [Theory] + [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, "verbose_json")] + [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, "json")] + [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt, "vtt")] + [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt, "srt")] + public async Task ItRespectResultFormatExecutionSettingAsync(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat responseFormat, string expectedFormat) + { + // Arrange + var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { ResponseFormat = responseFormat }; + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); + + // Assert + Assert.NotNull(this._messageHandlerStub.RequestContent); + Assert.NotNull(result); + + var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); + + Assert.Contains($"{expectedFormat}\r\n{multiPartBreak}", multiPartData); + } + + [Fact] + public async Task GetTextContentByDefaultWorksCorrectlyAsync() + { + // Arrange + var service = new AzureOpenAIAudioToTextService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent("Test audio-to-text response") + }; + + // Act + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new AzureOpenAIAudioToTextExecutionSettings("file.mp3")); + + // Assert + Assert.NotNull(result); + Assert.Equal("Test audio-to-text response", result[0].Text); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + public static TheoryData ExecutionSettings => new() + { + { new AzureOpenAIAudioToTextExecutionSettings(""), typeof(ArgumentException) }, + { new AzureOpenAIAudioToTextExecutionSettings("file"), typeof(ArgumentException) } + }; +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs new file mode 100644 index 000000000000..4582a79282a4 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAIAudioToTextExecutionSettingsTests +{ + [Fact] + public void ItReturnsDefaultSettingsWhenSettingsAreNull() + { + Assert.NotNull(AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(null)); + } + + [Fact] + public void ItReturnsValidOpenAIAudioToTextExecutionSettings() + { + // Arrange + var audioToTextSettings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + Temperature = 0.2f + }; + + // Act + var settings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(audioToTextSettings); + + // Assert + Assert.Same(audioToTextSettings, settings); + } + + [Fact] + public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() + { + // Arrange + var json = """ + { + "model_id": "model_id", + "language": "en", + "filename": "file.mp3", + "prompt": "prompt", + "response_format": "verbose", + "temperature": 0.2 + } + """; + + var executionSettings = JsonSerializer.Deserialize(json); + + // Act + var settings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); + + // Assert + Assert.NotNull(settings); + Assert.Equal("model_id", settings.ModelId); + Assert.Equal("en", settings.Language); + Assert.Equal("file.mp3", settings.Filename); + Assert.Equal("prompt", settings.Prompt); + Assert.Equal(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, settings.ResponseFormat); + Assert.Equal(0.2f, settings.Temperature); + } + + [Fact] + public void ItClonesAllProperties() + { + var settings = new AzureOpenAIAudioToTextExecutionSettings() + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + Temperature = 0.2f, + Filename = "something.mp3", + }; + + var clone = (AzureOpenAIAudioToTextExecutionSettings)settings.Clone(); + Assert.NotSame(settings, clone); + + Assert.Equal("model_id", clone.ModelId); + Assert.Equal("en", clone.Language); + Assert.Equal("prompt", clone.Prompt); + Assert.Equal(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, clone.ResponseFormat); + Assert.Equal(0.2f, clone.Temperature); + Assert.Equal("something.mp3", clone.Filename); + } + + [Fact] + public void ItFreezesAndPreventsMutation() + { + var settings = new AzureOpenAIAudioToTextExecutionSettings() + { + ModelId = "model_id", + Language = "en", + Prompt = "prompt", + ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + Temperature = 0.2f, + Filename = "something.mp3", + }; + + settings.Freeze(); + Assert.True(settings.IsFrozen); + + Assert.Throws(() => settings.ModelId = "new_model"); + Assert.Throws(() => settings.Language = "some_format"); + Assert.Throws(() => settings.Prompt = "prompt"); + Assert.Throws(() => settings.ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple); + Assert.Throws(() => settings.Temperature = 0.2f); + Assert.Throws(() => settings.Filename = "something"); + + settings.Freeze(); // idempotent + Assert.True(settings.IsFrozen); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs new file mode 100644 index 000000000000..e67ecbd0572e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs @@ -0,0 +1,266 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; + +/// +/// Unit tests for class. +/// +public class AzureOpenAIPromptExecutionSettingsTests +{ + [Fact] + public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() + { + // Arrange + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); + + // Assert + Assert.Equal(1, executionSettings.Temperature); + Assert.Equal(1, executionSettings.TopP); + Assert.Equal(0, executionSettings.FrequencyPenalty); + Assert.Equal(0, executionSettings.PresencePenalty); + Assert.Null(executionSettings.StopSequences); + Assert.Null(executionSettings.TokenSelectionBiases); + Assert.Null(executionSettings.TopLogprobs); + Assert.Null(executionSettings.Logprobs); + Assert.Null(executionSettings.AzureChatDataSource); + Assert.Equal(128, executionSettings.MaxTokens); + } + + [Fact] + public void ItUsesExistingOpenAIExecutionSettings() + { + // Arrange + AzureOpenAIPromptExecutionSettings actualSettings = new() + { + Temperature = 0.7, + TopP = 0.7, + FrequencyPenalty = 0.7, + PresencePenalty = 0.7, + StopSequences = new string[] { "foo", "bar" }, + ChatSystemPrompt = "chat system prompt", + MaxTokens = 128, + Logprobs = true, + TopLogprobs = 5, + TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + Assert.Equal(actualSettings, executionSettings); + } + + [Fact] + public void ItCanUseOpenAIExecutionSettings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() { + { "max_tokens", 1000 }, + { "temperature", 0 } + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(1000, executionSettings.MaxTokens); + Assert.Equal(0, executionSettings.Temperature); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", 0.7 }, + { "top_p", 0.7 }, + { "frequency_penalty", 0.7 }, + { "presence_penalty", 0.7 }, + { "results_per_prompt", 2 }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", 128 }, + { "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 }, + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", "0.7" }, + { "top_p", "0.7" }, + { "frequency_penalty", "0.7" }, + { "presence_penalty", "0.7" }, + { "results_per_prompt", "2" }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", "128" }, + { "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 } + } + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase() + { + // Arrange + var json = """ + { + "temperature": 0.7, + "top_p": 0.7, + "frequency_penalty": 0.7, + "presence_penalty": 0.7, + "stop_sequences": [ "foo", "bar" ], + "chat_system_prompt": "chat system prompt", + "token_selection_biases": { "1": 2, "3": 4 }, + "max_tokens": 128, + "seed": 123456, + "logprobs": true, + "top_logprobs": 5 + } + """; + var actualSettings = JsonSerializer.Deserialize(json); + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Theory] + [InlineData("", "")] + [InlineData("System prompt", "System prompt")] + public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) + { + // Arrange & Act + var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; + + // Assert + Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); + } + + [Fact] + public void PromptExecutionSettingsCloneWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + var clone = executionSettings!.Clone(); + + // Assert + Assert.Equal(executionSettings.ModelId, clone.ModelId); + Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); + } + + [Fact] + public void PromptExecutionSettingsFreezeWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": [ "DONE" ], + "token_selection_biases": { "1": 2, "3": 4 } + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + executionSettings!.Freeze(); + + // Assert + Assert.True(executionSettings.IsFrozen); + Assert.Throws(() => executionSettings.ModelId = "gpt-4"); + Assert.Throws(() => executionSettings.Temperature = 1); + Assert.Throws(() => executionSettings.TopP = 1); + Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); + Assert.Throws(() => executionSettings.TokenSelectionBiases?.Add(5, 6)); + + executionSettings!.Freeze(); // idempotent + Assert.True(executionSettings.IsFrozen); + } + + [Fact] + public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() + { + // Arrange + var executionSettings = new AzureOpenAIPromptExecutionSettings { StopSequences = [] }; + + // Act +#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions + var executionSettingsWithData = AzureOpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings); +#pragma warning restore CS0618 + // Assert + Assert.Null(executionSettingsWithData.StopSequences); + } + + private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings executionSettings) + { + Assert.NotNull(executionSettings); + Assert.Equal(0.7, executionSettings.Temperature); + Assert.Equal(0.7, executionSettings.TopP); + Assert.Equal(0.7, executionSettings.FrequencyPenalty); + Assert.Equal(0.7, executionSettings.PresencePenalty); + Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); + Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); + Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); + Assert.Equal(128, executionSettings.MaxTokens); + Assert.Equal(123456, executionSettings.Seed); + Assert.Equal(true, executionSettings.Logprobs); + Assert.Equal(5, executionSettings.TopLogprobs); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs new file mode 100644 index 000000000000..3eadbe124e10 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; + +/// +/// Unit tests for class. +/// +public sealed class AzureOpenAITextToAudioExecutionSettingsTests +{ + [Fact] + public void ItReturnsDefaultSettingsWhenSettingsAreNull() + { + Assert.NotNull(AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(null)); + } + + [Fact] + public void ItReturnsValidOpenAITextToAudioExecutionSettings() + { + // Arrange + var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings("voice") + { + ModelId = "model_id", + ResponseFormat = "mp3", + Speed = 1.0f + }; + + // Act + var settings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(textToAudioSettings); + + // Assert + Assert.Same(textToAudioSettings, settings); + } + + [Fact] + public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() + { + // Arrange + var json = """ + { + "model_id": "model_id", + "voice": "voice", + "response_format": "mp3", + "speed": 1.2 + } + """; + + var executionSettings = JsonSerializer.Deserialize(json); + + // Act + var settings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + + // Assert + Assert.NotNull(settings); + Assert.Equal("model_id", settings.ModelId); + Assert.Equal("voice", settings.Voice); + Assert.Equal("mp3", settings.ResponseFormat); + Assert.Equal(1.2f, settings.Speed); + } + + [Fact] + public void ItClonesAllProperties() + { + var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings() + { + ModelId = "some_model", + ResponseFormat = "some_format", + Speed = 3.14f, + Voice = "something" + }; + + var clone = (AzureOpenAITextToAudioExecutionSettings)textToAudioSettings.Clone(); + Assert.NotSame(textToAudioSettings, clone); + + Assert.Equal("some_model", clone.ModelId); + Assert.Equal("some_format", clone.ResponseFormat); + Assert.Equal(3.14f, clone.Speed); + Assert.Equal("something", clone.Voice); + } + + [Fact] + public void ItFreezesAndPreventsMutation() + { + var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings() + { + ModelId = "some_model", + ResponseFormat = "some_format", + Speed = 3.14f, + Voice = "something" + }; + + textToAudioSettings.Freeze(); + Assert.True(textToAudioSettings.IsFrozen); + + Assert.Throws(() => textToAudioSettings.ModelId = "new_model"); + Assert.Throws(() => textToAudioSettings.ResponseFormat = "some_format"); + Assert.Throws(() => textToAudioSettings.Speed = 3.14f); + Assert.Throws(() => textToAudioSettings.Voice = "something"); + + textToAudioSettings.Freeze(); // idempotent + Assert.True(textToAudioSettings.IsFrozen); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 0fe5ad9344b3..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,20 +21,10 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. - - - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index 59c1173bd780..bc6f5f16752f 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -31,33 +31,34 @@ internal async Task> GetTextFromAudioContentsAsync( throw new ArgumentException("The input audio content is not readable.", nameof(input)); } - OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; - AudioTranscriptionOptions? audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); + AzureOpenAIAudioToTextExecutionSettings audioExecutionSettings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; + AudioTranscriptionOptions audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); Verify.ValidFilename(audioExecutionSettings?.Filename); using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); - AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; + AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.DeploymentOrModelName).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; - return [new(responseData.Text, this.ModelId, metadata: GetResponseMetadata(responseData))]; + return [new(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData))]; } /// - /// Converts to type. + /// Converts to type. /// - /// Instance of . + /// Instance of . /// Instance of . - private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) + private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(AzureOpenAIAudioToTextExecutionSettings executionSettings) => new() { Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), Language = executionSettings.Language, Prompt = executionSettings.Prompt, - Temperature = executionSettings.Temperature + Temperature = executionSettings.Temperature, + ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; - private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) + private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) { AudioTimestampGranularities result = AudioTimestampGranularities.Default; @@ -67,8 +68,8 @@ private static AudioTimestampGranularities ConvertToAudioTimestampGranularities( { var openAIGranularity = granularity switch { - OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, - OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, + AzureOpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, + AzureOpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, _ => AudioTimestampGranularities.Default }; @@ -86,4 +87,21 @@ private static AudioTimestampGranularities ConvertToAudioTimestampGranularities( [nameof(audioTranscription.Duration)] = audioTranscription.Duration, [nameof(audioTranscription.Segments)] = audioTranscription.Segments }; + + private static AudioTranscriptionFormat? ConvertResponseFormat(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat? responseFormat) + { + if (responseFormat is null) + { + return null; + } + + return responseFormat switch + { + AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple => AudioTranscriptionFormat.Simple, + AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose => AudioTranscriptionFormat.Verbose, + AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt => AudioTranscriptionFormat.Vtt, + AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt => AudioTranscriptionFormat.Srt, + _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), + }; + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs index 1d995745bdde..cb91a512e004 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -8,6 +8,7 @@ using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; @@ -263,7 +264,7 @@ public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// The HttpClient to use with this service. /// The same instance as . - [Experimental("SKEXP0001")] + [Experimental("SKEXP0010")] public static IKernelBuilder AddAzureOpenAITextToAudio( this IKernelBuilder builder, string deploymentName, @@ -403,6 +404,118 @@ public static IKernelBuilder AddAzureOpenAITextToImage( #endregion + #region Audio-to-Text + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAIAudioToText( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + new AzureKeyCredential(apiKey), + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAIAudioToText( + this IKernelBuilder builder, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? serviceId = null, + string? modelId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + credentials, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddAzureOpenAIAudioToText( + this IKernelBuilder builder, + string deploymentName, + AzureOpenAIClient? openAIClient = null, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(deploymentName); + + Func factory = (serviceProvider, _) => + new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, factory); + + return builder; + } + + #endregion + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index 4df5711603ab..c073624c2bb0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using Azure.Core; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Embeddings; @@ -379,6 +380,114 @@ public static IServiceCollection AddAzureOpenAITextToImage( #endregion + #region Audio-to-Text + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAIAudioToText( + this IServiceCollection services, + string deploymentName, + string endpoint, + string apiKey, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNullOrWhiteSpace(apiKey); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + new AzureKeyCredential(apiKey), + HttpClientProvider.GetHttpClient(serviceProvider)); + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAIAudioToText( + this IServiceCollection services, + string deploymentName, + string endpoint, + TokenCredential credentials, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + Verify.NotNullOrWhiteSpace(endpoint); + Verify.NotNull(credentials); + + Func factory = (serviceProvider, _) => + { + AzureOpenAIClient client = CreateAzureOpenAIClient( + endpoint, + credentials, + HttpClientProvider.GetHttpClient(serviceProvider)); + return new(deploymentName, client, modelId, serviceProvider.GetService()); + }; + + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + /// + /// Adds the to the . + /// + /// The instance to augment. + /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddAzureOpenAIAudioToText( + this IServiceCollection services, + string deploymentName, + AzureOpenAIClient? openAIClient = null, + string? serviceId = null, + string? modelId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(deploymentName); + + Func factory = (serviceProvider, _) => + new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, factory); + + return services; + } + + #endregion + private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs index 313402adce99..991342398599 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs @@ -16,17 +16,17 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// /// Azure OpenAI audio-to-text service. /// -[Experimental("SKEXP0001")] +[Experimental("SKEXP0010")] public sealed class AzureOpenAIAudioToTextService : IAudioToTextService { /// Core implementation shared by Azure OpenAI services. - private readonly AzureOpenAIClientCore _core; + private readonly ClientCore _core; /// public IReadOnlyDictionary Attributes => this._core.Attributes; /// - /// Creates an instance of the with API key auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -47,7 +47,7 @@ public AzureOpenAIAudioToTextService( } /// - /// Creates an instance of the with AAD auth. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart @@ -68,19 +68,19 @@ public AzureOpenAIAudioToTextService( } /// - /// Creates an instance of the using the specified . + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . + /// Custom . /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// The to use for logging. If null, no logging will be performed. public AzureOpenAIAudioToTextService( string deploymentName, - OpenAIClient openAIClient, + AzureOpenAIClient azureOpenAIClient, string? modelId = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } @@ -90,5 +90,5 @@ public Task> GetTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken); + => this._core.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs new file mode 100644 index 000000000000..0f8115c70910 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.Text; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Execution settings for Azure OpenAI audio-to-text request. +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAIAudioToTextExecutionSettings : PromptExecutionSettings +{ + /// + /// Filename or identifier associated with audio data. + /// Should be in format {filename}.{extension} + /// + [JsonPropertyName("filename")] + public string Filename + { + get => this._filename; + + set + { + this.ThrowIfFrozen(); + this._filename = value; + } + } + + /// + /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es'). + /// + [JsonPropertyName("language")] + public string? Language + { + get => this._language; + + set + { + this.ThrowIfFrozen(); + this._language = value; + } + } + + /// + /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. + /// + [JsonPropertyName("prompt")] + public string? Prompt + { + get => this._prompt; + + set + { + this.ThrowIfFrozen(); + this._prompt = value; + } + } + + /// + /// The format of the transcript output, in one of these options: Text, Simple, Verbose, Sttor vtt. Default is 'json'. + /// + [JsonPropertyName("response_format")] + [JsonConverter(typeof(JsonStringEnumConverter))] + public AudioTranscriptionFormat? ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The sampling temperature, between 0 and 1. + /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. + /// If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. + /// Default is 0. + /// + [JsonPropertyName("temperature")] + public float Temperature + { + get => this._temperature; + + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. + /// + [JsonPropertyName("granularities")] + public IReadOnlyList? Granularities { get; set; } + + /// + /// Creates an instance of class with default filename - "file.mp3". + /// + public AzureOpenAIAudioToTextExecutionSettings() + : this(DefaultFilename) + { + } + + /// + /// Creates an instance of class. + /// + /// Filename or identifier associated with audio data. Should be in format {filename}.{extension} + public AzureOpenAIAudioToTextExecutionSettings(string filename) + { + this._filename = filename; + } + + /// + public override PromptExecutionSettings Clone() + { + return new AzureOpenAIAudioToTextExecutionSettings(this.Filename) + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + ResponseFormat = this.ResponseFormat, + Language = this.Language, + Prompt = this.Prompt + }; + } + + /// + /// Converts to derived type. + /// + /// Instance of . + /// Instance of . + public static AzureOpenAIAudioToTextExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) + { + if (executionSettings is null) + { + return new AzureOpenAIAudioToTextExecutionSettings(); + } + + if (executionSettings is AzureOpenAIAudioToTextExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + + if (openAIExecutionSettings is not null) + { + return openAIExecutionSettings; + } + + throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIAudioToTextExecutionSettings)}", nameof(executionSettings)); + } + + /// + /// The timestamp granularities available to populate transcriptions. + /// + public enum TimeStampGranularities + { + /// + /// Not specified. + /// + Default = 0, + + /// + /// The transcription is segmented by word. + /// + Word = 1, + + /// + /// The timestamp of transcription is by segment. + /// + Segment = 2, + } + + /// + /// Specifies the format of the audio transcription. + /// + public enum AudioTranscriptionFormat + { + /// + /// Response body that is a JSON object containing a single 'text' field for the transcription. + /// + Simple, + + /// + /// Use a response body that is a JSON object containing transcription text along with timing, segments, and other metadata. + /// + Verbose, + + /// + /// Response body that is plain text in SubRip (SRT) format that also includes timing information. + /// + Srt, + + /// + /// Response body that is plain text in Web Video Text Tracks (VTT) format that also includes timing information. + /// + Vtt, + } + + #region private ================================================================================ + + private const string DefaultFilename = "file.mp3"; + + private float _temperature = 0; + private AudioTranscriptionFormat? _responseFormat; + private string _filename; + private string? _language; + private string? _prompt; + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs new file mode 100644 index 000000000000..3319b4f055e8 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.IO; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; + +public sealed class AzureOpenAIAudioToTextTests() +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Fact] + public async Task AzureOpenAIAudioToTextTestAsync() + { + // Arrange + const string Filename = "test_audio.wav"; + + AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAIAudioToText").Get(); + Assert.NotNull(azureOpenAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddAzureOpenAIAudioToText( + azureOpenAIConfiguration.DeploymentName, + azureOpenAIConfiguration.Endpoint, + azureOpenAIConfiguration.ApiKey) + .Build(); + + var service = kernel.GetRequiredService(); + + await using Stream audio = File.OpenRead($"./TestData/{Filename}"); + var audioData = await BinaryData.FromStreamAsync(audio); + + // Act + var result = await service.GetTextContentAsync(new AudioContent(audioData, mimeType: "audio/wav"), new OpenAIAudioToTextExecutionSettings(Filename)); + + // Assert + Assert.Contains("The sun rises in the east and sets in the west.", result.Text, StringComparison.OrdinalIgnoreCase); + } +} From 6fd7c7ff64b0d1e2b6619738d1afe4d26a9cf074 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 08:05:58 -0700 Subject: [PATCH 049/226] Exclude V1 sample --- dotnet/samples/Concepts/Concepts.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index dfcddf2920c5..f12e501aab3e 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -104,6 +104,7 @@ + From d230cce1a18aa1680e75eb340c38ef2e1afe7a39 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:23:52 +0100 Subject: [PATCH 050/226] .Net: OpenAI V2 - Audio to Text - Response Format as Enum conversion for format (#7141) ### Motivation and Context - Updates Execution Settings for `ResponseFormat` from `string` to `enum`. - Updates the Unit Tests to validate. --- ...OpenAIAudioToTextExecutionSettingsTests.cs | 14 ++++---- .../Core/ClientCore.AudioToText.cs | 20 ++++++++++- .../OpenAIAudioToTextExecutionSettings.cs | 33 +++++++++++++++++-- 3 files changed, 56 insertions(+), 11 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs index e01345c82f03..4f443fdcc02a 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs @@ -28,7 +28,7 @@ public void ItReturnsValidOpenAIAudioToTextExecutionSettings() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = "text", + ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, Temperature = 0.2f }; @@ -49,7 +49,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() "language": "en", "filename": "file.mp3", "prompt": "prompt", - "response_format": "text", + "response_format": "verbose", "temperature": 0.2 } """; @@ -65,7 +65,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() Assert.Equal("en", settings.Language); Assert.Equal("file.mp3", settings.Filename); Assert.Equal("prompt", settings.Prompt); - Assert.Equal("text", settings.ResponseFormat); + Assert.Equal(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, settings.ResponseFormat); Assert.Equal(0.2f, settings.Temperature); } @@ -77,7 +77,7 @@ public void ItClonesAllProperties() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = "text", + ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, Temperature = 0.2f, Filename = "something.mp3", }; @@ -88,7 +88,7 @@ public void ItClonesAllProperties() Assert.Equal("model_id", clone.ModelId); Assert.Equal("en", clone.Language); Assert.Equal("prompt", clone.Prompt); - Assert.Equal("text", clone.ResponseFormat); + Assert.Equal(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, clone.ResponseFormat); Assert.Equal(0.2f, clone.Temperature); Assert.Equal("something.mp3", clone.Filename); } @@ -101,7 +101,7 @@ public void ItFreezesAndPreventsMutation() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = "text", + ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, Temperature = 0.2f, Filename = "something.mp3", }; @@ -112,7 +112,7 @@ public void ItFreezesAndPreventsMutation() Assert.Throws(() => settings.ModelId = "new_model"); Assert.Throws(() => settings.Language = "some_format"); Assert.Throws(() => settings.Prompt = "prompt"); - Assert.Throws(() => settings.ResponseFormat = "something"); + Assert.Throws(() => settings.ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple); Assert.Throws(() => settings.Temperature = 0.2f); Assert.Throws(() => settings.Filename = "something"); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs index 77ec85fe9c10..e8e974655175 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -54,7 +54,8 @@ internal async Task> GetTextFromAudioContentsAsync( Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), Language = executionSettings.Language, Prompt = executionSettings.Prompt, - Temperature = executionSettings.Temperature + Temperature = executionSettings.Temperature, + ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) @@ -79,6 +80,23 @@ private static AudioTimestampGranularities ConvertToAudioTimestampGranularities( return result; } + private static AudioTranscriptionFormat? ConvertResponseFormat(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat? responseFormat) + { + if (responseFormat is null) + { + return null; + } + + return responseFormat switch + { + OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple => AudioTranscriptionFormat.Simple, + OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose => AudioTranscriptionFormat.Verbose, + OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt => AudioTranscriptionFormat.Vtt, + OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt => AudioTranscriptionFormat.Srt, + _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), + }; + } + private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) => new(3) { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs index 5d87768c5ddd..845c0220ef89 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -61,10 +61,11 @@ public string? Prompt } /// - /// The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. Default is 'json'. + /// The format of the transcript output, in one of these options: Text, Simple, Verbose, Sttor vtt. Default is 'json'. /// [JsonPropertyName("response_format")] - public string ResponseFormat + [JsonConverter(typeof(JsonStringEnumConverter))] + public AudioTranscriptionFormat? ResponseFormat { get => this._responseFormat; @@ -175,12 +176,38 @@ public enum TimeStampGranularities Segment = 2, } + /// + /// Specifies the format of the audio transcription. + /// + public enum AudioTranscriptionFormat + { + /// + /// Response body that is a JSON object containing a single 'text' field for the transcription. + /// + Simple, + + /// + /// Use a response body that is a JSON object containing transcription text along with timing, segments, and other metadata. + /// + Verbose, + + /// + /// Response body that is plain text in SubRip (SRT) format that also includes timing information. + /// + Srt, + + /// + /// Response body that is plain text in Web Video Text Tracks (VTT) format that also includes timing information. + /// + Vtt, + } + #region private ================================================================================ private const string DefaultFilename = "file.mp3"; private float _temperature = 0; - private string _responseFormat = "json"; + private AudioTranscriptionFormat? _responseFormat; private string _filename; private string? _language; private string? _prompt; From 923860483f5821f51b1ebd48f4e4b0351e8aabb5 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Jul 2024 16:24:18 +0100 Subject: [PATCH 051/226] .Net: AzureOpenAI services cleanup (#7140) ### Motivation, Context, and Description 1. The `ClientCore.DeploymentOrModelName` property is renamed to `ClientCore.DeploymentName` to reflect it's actual purpose. 2. The `AzureOpenAIPromptExecutionSettings` class is moved to the `Settings` folder to reside alongside prompt execution setting classes for other services. --- .../Core/ClientCore.ChatCompletion.cs | 14 +++++++------- .../Core/ClientCore.Embeddings.cs | 2 +- .../Core/ClientCore.TextToAudio.cs | 2 +- .../Core/ClientCore.TextToImage.cs | 2 +- .../Connectors.AzureOpenAI/Core/ClientCore.cs | 10 +++++----- .../AzureOpenAIPromptExecutionSettings.cs | 0 6 files changed, 15 insertions(+), 15 deletions(-) rename dotnet/src/Connectors/Connectors.AzureOpenAI/{ => Settings}/AzureOpenAIPromptExecutionSettings.cs (100%) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index 14b8c6a38ae0..c9a6f26f94ef 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -160,11 +160,11 @@ internal async Task> GetChatMessageContentsAsy // Make the request. OpenAIChatCompletion? chatCompletion = null; AzureOpenAIChatMessageContent chatMessageContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chat, chatExecutionSettings)) { try { - chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; this.LogUsage(chatCompletion.Usage); } @@ -369,13 +369,13 @@ internal async IAsyncEnumerable GetStrea ChatToolCall[]? toolCalls = null; FunctionCallContent[]? functionCallContents = null; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chat, chatExecutionSettings)) { // Make the request. AsyncResultCollection response; try { - response = RunRequest(() => this.Client.GetChatClient(this.DeploymentOrModelName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); + response = RunRequest(() => this.Client.GetChatClient(this.DeploymentName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -422,7 +422,7 @@ internal async IAsyncEnumerable GetStrea AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentOrModelName, metadata); + var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentName, metadata); foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) { @@ -919,7 +919,7 @@ private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentOrModelName, GetChatCompletionMetadata(completion)); + var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentName, GetChatCompletionMetadata(completion)); message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); @@ -928,7 +928,7 @@ private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatComplet private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { - var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) + var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentName, toolCalls, metadata) { AuthorName = authorName, }; diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs index 6f1aaaf16efc..20c4736f27c7 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs @@ -36,7 +36,7 @@ internal async Task>> GetEmbeddingsAsync( Dimensions = dimensions }; - var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentOrModelName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); var embeddings = response.Value; if (embeddings.Count != data.Count) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs index 4351b15607bd..4cb78c74d658 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs @@ -76,6 +76,6 @@ private string GetModelId(AzureOpenAITextToAudioExecutionSettings executionSetti return !string.IsNullOrWhiteSpace(modelId) ? modelId! : !string.IsNullOrWhiteSpace(executionSettings.ModelId) ? executionSettings.ModelId! : - this.DeploymentOrModelName; + this.DeploymentName; } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs index 46335d6289de..fefa13203ba7 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs @@ -36,7 +36,7 @@ internal async Task GenerateImageAsync( ResponseFormat = GeneratedImageFormat.Uri }; - ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.DeploymentOrModelName).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.DeploymentName).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); return response.Value.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs index 571d24d95c3b..6f669d5eede4 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs @@ -27,9 +27,9 @@ internal partial class ClientCore internal static string DeploymentNameKey => "DeploymentName"; /// - /// Model Id or Deployment Name + /// Deployment name. /// - internal string DeploymentOrModelName { get; set; } = string.Empty; + internal string DeploymentName { get; set; } = string.Empty; /// /// Azure OpenAI Client @@ -74,7 +74,7 @@ internal ClientCore( var options = GetAzureOpenAIClientOptions(httpClient); this.Logger = logger ?? NullLogger.Instance; - this.DeploymentOrModelName = deploymentName; + this.DeploymentName = deploymentName; this.Endpoint = new Uri(endpoint); this.Client = new AzureOpenAIClient(this.Endpoint, apiKey, options); @@ -103,7 +103,7 @@ internal ClientCore( var options = GetAzureOpenAIClientOptions(httpClient); this.Logger = logger ?? NullLogger.Instance; - this.DeploymentOrModelName = deploymentName; + this.DeploymentName = deploymentName; this.Endpoint = new Uri(endpoint); this.Client = new AzureOpenAIClient(this.Endpoint, credential, options); @@ -127,7 +127,7 @@ internal ClientCore( Verify.NotNull(openAIClient); this.Logger = logger ?? NullLogger.Instance; - this.DeploymentOrModelName = deploymentName; + this.DeploymentName = deploymentName; this.Client = openAIClient; this.AddAttribute(DeploymentNameKey, deploymentName); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIPromptExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs From ec0aa8cfac9c712a505deb4fb8acd993045fdcff Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 08:25:14 -0700 Subject: [PATCH 052/226] Project clean-up --- .../Agents/OpenAIAssistantAgentTests.cs | 135 ------------------ .../IntegrationTests/IntegrationTests.csproj | 5 - 2 files changed, 140 deletions(-) delete mode 100644 dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs deleted file mode 100644 index fbea0341810c..000000000000 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.ComponentModel; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Agents.OpenAI; - -#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. - -public sealed class OpenAIAssistantAgentTests(ITestOutputHelper output) : IDisposable -{ - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - /// - /// Integration test for using function calling - /// and targeting Open AI services. - /// - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData("What is the special soup?", "Clam Chowder")] - public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) - { - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - await this.ExecuteAgentAsync( - new(openAIConfiguration.ApiKey), - openAIConfiguration.ModelId, - input, - expectedAnswerContains); - } - - /// - /// Integration test for using function calling - /// and targeting Azure OpenAI services. - /// - [Theory(Skip = "No supported endpoint configured.")] - [InlineData("What is the special soup?", "Clam Chowder")] - public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAnswerContains) - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - await this.ExecuteAgentAsync( - new(azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.Endpoint), - azureOpenAIConfiguration.ChatDeploymentName!, - input, - expectedAnswerContains); - } - - private async Task ExecuteAgentAsync( - OpenAIAssistantConfiguration config, - string modelName, - string input, - string expected) - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - - Kernel kernel = this._kernelBuilder.Build(); - - KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - kernel.Plugins.Add(plugin); - - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel, - config, - new() - { - Instructions = "Answer questions about the menu.", - ModelName = modelName, - }); - - AgentGroupChat chat = new(); - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - // Act - StringBuilder builder = new(); - await foreach (var message in chat.InvokeAsync(agent)) - { - builder.Append(message.Content); - } - - // Assert - Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase); - } - - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - - public sealed class MenuPlugin - { - [KernelFunction, Description("Provides a list of specials from the menu.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } - - [KernelFunction, Description("Provides the price of the requested menu item.")] - public string GetItemPrice( - [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } - } -} diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index 3d1b93a71253..df5afa473ce7 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -17,7 +17,6 @@ - @@ -157,8 +156,4 @@ Always - - - - \ No newline at end of file From 13a93183b572904b678b1c126d2a7853891037d4 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 8 Jul 2024 17:40:08 +0100 Subject: [PATCH 053/226] .Net: Copy OpenAI file service to the Connectors.AzureOpenAI project (#7148) ### Motivation, Context and Description This PR: 1. Copies OpenAIFileService and the other classes related to it to the Connectors.AzureOpenAI project before refactoring them to use the Azure.AI.OpenAI SDK v2. 2. Renames the classes and changes namespaces accordingly. 3. Adds/changes no functionality in any of those classes. 4. Temporarily removes the classes from the compilation process. They will be included back in the next PR. --------- Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../Connectors.AzureOpenAI.csproj | 16 +++ .../Core/ClientCore.AudioToText.cs | 4 +- .../Core/ClientCore.File.cs | 124 +++++++++++++++++ .../Model/AzureOpenAIFilePurpose.cs | 22 +++ .../Model/AzureOpenAIFileReference.cs | 38 ++++++ .../Services/AzureOpenAIFileService.cs | 128 ++++++++++++++++++ .../AzureOpenAIFileUploadExecutionSettings.cs | 35 +++++ 7 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..65a954656bf9 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,10 +21,26 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + + + + + + + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index bc6f5f16752f..b3910feaf1cb 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -38,9 +38,9 @@ internal async Task> GetTextFromAudioContentsAsync( using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); - AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.DeploymentOrModelName).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; + AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.DeploymentName).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; - return [new(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData))]; + return [new(responseData.Text, this.DeploymentName, metadata: GetResponseMetadata(responseData))]; } /// diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs new file mode 100644 index 000000000000..32a97ed1e803 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs @@ -0,0 +1,124 @@ +// Copyright (c) Microsoft. All rights reserved. + +/* +Phase 05 +- Ignoring the specific Purposes not implemented by current FileService. +*/ + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using OpenAI.Files; + +using OAIFilePurpose = OpenAI.Files.OpenAIFilePurpose; +using SKFilePurpose = Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + /// + /// Uploads a file to OpenAI. + /// + /// File name + /// File content + /// Purpose of the file + /// Cancellation token + /// Uploaded file information + internal async Task UploadFileAsync( + string fileName, + Stream fileContent, + SKFilePurpose purpose, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().UploadFileAsync(fileContent, fileName, ConvertToOpenAIFilePurpose(purpose), cancellationToken)).ConfigureAwait(false); + return ConvertToFileReference(response.Value); + } + + /// + /// Delete a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + internal async Task DeleteFileAsync( + string fileId, + CancellationToken cancellationToken) + { + await RunRequestAsync(() => this.Client.GetFileClient().DeleteFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + } + + /// + /// Retrieve metadata for a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The metadata associated with the specified file identifier. + internal async Task GetFileAsync( + string fileId, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + return ConvertToFileReference(response.Value); + } + + /// + /// Retrieve metadata for all previously uploaded files. + /// + /// The to monitor for cancellation requests. The default is . + /// The metadata of all uploaded files. + internal async Task> GetFilesAsync(CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFilesAsync(cancellationToken: cancellationToken)).ConfigureAwait(false); + return response.Value.Select(ConvertToFileReference); + } + + /// + /// Retrieve the file content from a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The file content as + /// + /// Files uploaded with do not support content retrieval. + /// + internal async Task GetFileContentAsync( + string fileId, + CancellationToken cancellationToken) + { + ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().DownloadFileAsync(fileId, cancellationToken)).ConfigureAwait(false); + return response.Value.ToArray(); + } + + private static OpenAIFileReference ConvertToFileReference(OpenAIFileInfo fileInfo) + => new() + { + Id = fileInfo.Id, + CreatedTimestamp = fileInfo.CreatedAt.DateTime, + FileName = fileInfo.Filename, + SizeInBytes = (int)(fileInfo.SizeInBytes ?? 0), + Purpose = ConvertToFilePurpose(fileInfo.Purpose), + }; + + private static FileUploadPurpose ConvertToOpenAIFilePurpose(SKFilePurpose purpose) + { + if (purpose == SKFilePurpose.Assistants) { return FileUploadPurpose.Assistants; } + if (purpose == SKFilePurpose.FineTune) { return FileUploadPurpose.FineTune; } + + throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); + } + + private static SKFilePurpose ConvertToFilePurpose(OAIFilePurpose purpose) + { + if (purpose == OAIFilePurpose.Assistants) { return SKFilePurpose.Assistants; } + if (purpose == OAIFilePurpose.FineTune) { return SKFilePurpose.FineTune; } + + throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs new file mode 100644 index 000000000000..0e7e7f46f233 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Defines the purpose associated with the uploaded file. +/// +[Experimental("SKEXP0010")] +public enum AzureOpenAIFilePurpose +{ + /// + /// File to be used by assistants for model processing. + /// + Assistants, + + /// + /// File to be used by fine-tuning jobs. + /// + FineTune, +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs new file mode 100644 index 000000000000..80166c30e77b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// References an uploaded file by id. +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAIFileReference +{ + /// + /// The file identifier. + /// + public string Id { get; set; } = string.Empty; + + /// + /// The timestamp the file was uploaded.s + /// + public DateTime CreatedTimestamp { get; set; } + + /// + /// The name of the file.s + /// + public string FileName { get; set; } = string.Empty; + + /// + /// Describes the associated purpose of the file. + /// + public OpenAIFilePurpose Purpose { get; set; } + + /// + /// The file size, in bytes. + /// + public int SizeInBytes { get; set; } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs new file mode 100644 index 000000000000..6a2f3d01014a --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs @@ -0,0 +1,128 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// File service access for Azure OpenAI. +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAIFileService +{ + /// + /// OpenAI client for HTTP operations. + /// + private readonly ClientCore _client; + + /// + /// Initializes a new instance of the class. + /// + /// Non-default endpoint for the OpenAI API. + /// API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIFileService( + Uri endpoint, + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + + this._client = new(null, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + } + + /// + /// Initializes a new instance of the class. + /// + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public AzureOpenAIFileService( + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + + this._client = new(null, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + } + + /// + /// Remove a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + public Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + + return this._client.DeleteFileAsync(id, cancellationToken); + } + + /// + /// Retrieve the file content from a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The file content as + /// + /// Files uploaded with do not support content retrieval. + /// + public async Task GetFileContentAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + var bytes = await this._client.GetFileContentAsync(id, cancellationToken).ConfigureAwait(false); + + // The mime type of the downloaded file is not provided by the OpenAI API. + return new(bytes, null); + } + + /// + /// Retrieve metadata for a previously uploaded file. + /// + /// The uploaded file identifier. + /// The to monitor for cancellation requests. The default is . + /// The metadata associated with the specified file identifier. + public Task GetFileAsync(string id, CancellationToken cancellationToken = default) + { + Verify.NotNull(id, nameof(id)); + return this._client.GetFileAsync(id, cancellationToken); + } + + /// + /// Retrieve metadata for all previously uploaded files. + /// + /// The to monitor for cancellation requests. The default is . + /// The metadata of all uploaded files. + public async Task> GetFilesAsync(CancellationToken cancellationToken = default) + => await this._client.GetFilesAsync(cancellationToken).ConfigureAwait(false); + + /// + /// Upload a file. + /// + /// The file content as + /// The upload settings + /// The to monitor for cancellation requests. The default is . + /// The file metadata. + public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default) + { + Verify.NotNull(settings, nameof(settings)); + Verify.NotNull(fileContent.Data, nameof(fileContent.Data)); + + using var memoryStream = new MemoryStream(fileContent.Data.Value.ToArray()); + return await this._client.UploadFileAsync(settings.FileName, memoryStream, settings.Purpose, cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs new file mode 100644 index 000000000000..c7676c86076b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Execution settings associated with Azure Open AI file upload . +/// +[Experimental("SKEXP0010")] +public sealed class AzureOpenAIFileUploadExecutionSettings +{ + /// + /// Initializes a new instance of the class. + /// + /// The file name + /// The file purpose + public AzureOpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose) + { + Verify.NotNull(fileName, nameof(fileName)); + + this.FileName = fileName; + this.Purpose = purpose; + } + + /// + /// The file name. + /// + public string FileName { get; } + + /// + /// The file purpose. + /// + public OpenAIFilePurpose Purpose { get; } +} From b2c172653191a5bd91f67785b97a1761251bf1b4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 10:19:41 -0700 Subject: [PATCH 054/226] Dependency clean-up: IntegrationTests --- .../Agents/OpenAIAssistant_ChartMaker.cs | 6 ++--- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 6 ++--- .../OpenAIAssistant_FileManipulation.cs | 6 ++--- .../Agents/OpenAIAssistant_FileSearch.cs | 6 ++--- .../Step8_OpenAIAssistant.cs | 6 ++--- .../OpenAI/Internal/OpenAIClientFactory.cs | 10 ++++----- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 10 ++++----- ...ation.cs => OpenAIServiceConfiguration.cs} | 22 +++++++++---------- dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 4 ++-- .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 2 +- .../OpenAI/OpenAIAssistantAgentTests.cs | 6 ++--- .../OpenAI/OpenAIConfigurationTests.cs | 6 ++--- .../IntegrationTests/IntegrationTests.csproj | 4 ++-- .../Agents/OpenAIAssistantAgentTests.cs | 11 ++++------ 14 files changed, 51 insertions(+), 54 deletions(-) rename dotnet/src/Agents/OpenAI/{OpenAIConfiguration.cs => OpenAIServiceConfiguration.cs} (75%) diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs index 70b4fe3d99fe..63ed511742f8 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs @@ -83,9 +83,9 @@ async Task InvokeAgentAsync(string input) } } - private OpenAIConfiguration GetOpenAIConfiguration() + private OpenAIServiceConfiguration GetOpenAIConfiguration() => this.UseOpenAIConfig ? - OpenAIConfiguration.ForOpenAI(this.ApiKey) : - OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs index 5a5314935eca..646c1f244967 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -54,9 +54,9 @@ async Task InvokeAgentAsync(string input) } } - private OpenAIConfiguration GetOpenAIConfiguration() + private OpenAIServiceConfiguration GetOpenAIConfiguration() => this.UseOpenAIConfig ? - OpenAIConfiguration.ForOpenAI(this.ApiKey) : - OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs index 3b57e8a1c911..a0fa5a074694 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs @@ -78,9 +78,9 @@ async Task InvokeAgentAsync(string input) } } - private OpenAIConfiguration GetOpenAIConfiguration() + private OpenAIServiceConfiguration GetOpenAIConfiguration() => this.UseOpenAIConfig ? - OpenAIConfiguration.ForOpenAI(this.ApiKey) : - OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs index 1fa2ca2de885..9fef5a0c5830 100644 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs @@ -76,9 +76,9 @@ async Task InvokeAgentAsync(string input) } } - private OpenAIConfiguration GetOpenAIConfiguration() + private OpenAIServiceConfiguration GetOpenAIConfiguration() => this.UseOpenAIConfig ? - OpenAIConfiguration.ForOpenAI(this.ApiKey) : - OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 458c79fd5347..c2c1868f5edd 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -72,11 +72,11 @@ async Task InvokeAgentAsync(string input) } } - private OpenAIConfiguration GetOpenAIConfiguration() + private OpenAIServiceConfiguration GetOpenAIConfiguration() => this.UseOpenAIConfig ? - OpenAIConfiguration.ForOpenAI(this.ApiKey) : - OpenAIConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); private sealed class MenuPlugin { diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index 0f6b88c5dfa1..d71973de5d7b 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -20,12 +20,12 @@ internal static class OpenAIClientFactory /// /// Configuration required to target a specific Open AI service /// An initialized Open AI client - public static OpenAIClient CreateClient(OpenAIConfiguration config) + public static OpenAIClient CreateClient(OpenAIServiceConfiguration config) { // Inspect options switch (config.Type) { - case OpenAIConfiguration.OpenAIConfigurationType.AzureOpenAI: + case OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI: { AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); @@ -40,7 +40,7 @@ public static OpenAIClient CreateClient(OpenAIConfiguration config) throw new KernelException($"Unsupported configuration type: {config.Type}"); } - case OpenAIConfiguration.OpenAIConfigurationType.OpenAI: + case OpenAIServiceConfiguration.OpenAIServiceType.OpenAI: { OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(config); return new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); @@ -50,7 +50,7 @@ public static OpenAIClient CreateClient(OpenAIConfiguration config) } } - private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIConfiguration config) + private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIServiceConfiguration config) { AzureOpenAIClientOptions options = new() @@ -64,7 +64,7 @@ private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIConfigura return options; } - private static OpenAIClientOptions CreateOpenAIClientOptions(OpenAIConfiguration config) + private static OpenAIClientOptions CreateOpenAIClientOptions(OpenAIServiceConfiguration config) { OpenAIClientOptions options = new() diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 3db35168aa03..39dbd32ebe22 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -53,7 +53,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// An instance public static async Task CreateAsync( Kernel kernel, - OpenAIConfiguration config, + OpenAIServiceConfiguration config, OpenAIAssistantDefinition definition, CancellationToken cancellationToken = default) { @@ -84,7 +84,7 @@ public static async Task CreateAsync( /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( - OpenAIConfiguration config, + OpenAIServiceConfiguration config, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create the client @@ -107,7 +107,7 @@ public static async IAsyncEnumerable ListDefinitionsA /// An instance public static async Task RetrieveAsync( Kernel kernel, - OpenAIConfiguration config, + OpenAIServiceConfiguration config, string id, CancellationToken cancellationToken = default) { @@ -398,13 +398,13 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss return toolResources; } - private static AssistantClient CreateClient(OpenAIConfiguration config) + private static AssistantClient CreateClient(OpenAIServiceConfiguration config) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); return openAIClient.GetAssistantClient(); } - private static IEnumerable DefineChannelKeys(OpenAIConfiguration config) + private static IEnumerable DefineChannelKeys(OpenAIServiceConfiguration config) { // Distinguish from other channel types. yield return typeof(AgentChannel).FullName!; diff --git a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs similarity index 75% rename from dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs rename to dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs index 54b7eed768aa..66b1c97952f9 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs @@ -8,9 +8,9 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Configuration to target a specific Open AI service. /// -public sealed class OpenAIConfiguration +public sealed class OpenAIServiceConfiguration { - internal enum OpenAIConfigurationType + internal enum OpenAIServiceType { AzureOpenAI, OpenAI, @@ -23,7 +23,7 @@ internal enum OpenAIConfigurationType /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) + public static OpenAIServiceConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) { Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); Verify.NotNull(endpoint, nameof(endpoint)); @@ -34,7 +34,7 @@ public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, Ht ApiKey = apiKey, Endpoint = endpoint, HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAI, + Type = OpenAIServiceType.AzureOpenAI, }; } @@ -45,7 +45,7 @@ public static OpenAIConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, Ht /// /// /// - public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) + public static OpenAIServiceConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) { Verify.NotNull(credential, nameof(credential)); Verify.NotNull(endpoint, nameof(endpoint)); @@ -56,7 +56,7 @@ public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri Credential = credential, Endpoint = endpoint, HttpClient = httpClient, - Type = OpenAIConfigurationType.AzureOpenAI, + Type = OpenAIServiceType.AzureOpenAI, }; } @@ -67,7 +67,7 @@ public static OpenAIConfiguration ForAzureOpenAI(TokenCredential credential, Uri /// /// /// - public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) + public static OpenAIServiceConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); @@ -77,7 +77,7 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, ApiKey = apiKey, Endpoint = endpoint, HttpClient = httpClient, - Type = OpenAIConfigurationType.OpenAI, + Type = OpenAIServiceType.OpenAI, }; } /// @@ -88,7 +88,7 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, /// /// /// - public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) + public static OpenAIServiceConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); Verify.NotNullOrWhiteSpace(organizationId, nameof(organizationId)); @@ -101,7 +101,7 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId Endpoint = endpoint, HttpClient = httpClient, OrganizationId = organizationId, - Type = OpenAIConfigurationType.OpenAI, + Type = OpenAIServiceType.OpenAI, }; } @@ -110,5 +110,5 @@ public static OpenAIConfiguration ForOpenAI(string apiKey, string organizationId internal Uri? Endpoint { get; init; } internal HttpClient? HttpClient { get; init; } internal string? OrganizationId { get; init; } - internal OpenAIConfigurationType Type { get; init; } + internal OpenAIServiceType Type { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs index 9fc092d5e29a..74714fd66e02 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs @@ -26,7 +26,7 @@ public sealed class OpenAIVectorStore /// /// /// - public static IAsyncEnumerable GetVectorStoresAsync(OpenAIConfiguration config, CancellationToken cancellationToken = default) + public static IAsyncEnumerable GetVectorStoresAsync(OpenAIServiceConfiguration config, CancellationToken cancellationToken = default) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); VectorStoreClient client = openAIClient.GetVectorStoreClient(); @@ -39,7 +39,7 @@ public static IAsyncEnumerable GetVectorStoresAsync(OpenAIConfigura /// /// /// - public OpenAIVectorStore(string vectorStoreId, OpenAIConfiguration config) + public OpenAIVectorStore(string vectorStoreId, OpenAIServiceConfiguration config) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); this._client = openAIClient.GetVectorStoreClient(); diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs index cefb8d05a855..ac2c23293d73 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs @@ -11,7 +11,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// %%% /// -public sealed class OpenAIVectorStoreBuilder(OpenAIConfiguration config) +public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) { private string? _name; private FileChunkingStrategy? _chunkingStrategy; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index e9873085c79d..01285e2372a2 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -381,10 +381,10 @@ private Task CreateAgentAsync() definition); } - private OpenAIConfiguration CreateTestConfiguration(bool targetAzure = false) + private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) => targetAzure ? - OpenAIConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : - OpenAIConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); + OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); private void SetupResponse(HttpStatusCode statusCode, string content) { diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs index ac7caad578ac..14237eebcd19 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs @@ -7,7 +7,7 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI; /// -/// Unit testing of . +/// Unit testing of . /// public class OpenAIConfigurationTests { @@ -17,7 +17,7 @@ public class OpenAIConfigurationTests [Fact] public void VerifyOpenAIAssistantConfigurationInitialState() { - OpenAIConfiguration config = OpenAIConfiguration.ForOpenAI(apiKey: "testkey"); + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey"); Assert.Equal("testkey", config.ApiKey); Assert.Null(config.Endpoint); @@ -32,7 +32,7 @@ public void VerifyOpenAIAssistantConfigurationAssignment() { using HttpClient client = new(); - OpenAIConfiguration config = OpenAIConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); Assert.Equal("testkey", config.ApiKey); Assert.NotNull(config.Endpoint); diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index df5afa473ce7..4e3089d00432 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -60,8 +60,8 @@ - - + diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs index d084b0fd0ed5..7e34c4ebe65e 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs @@ -11,9 +11,6 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -using OpenAIConfiguration = Microsoft.SemanticKernel.Agents.OpenAI.OpenAIConfiguration; -using OpenAISettings = SemanticKernel.IntegrationTests.TestSettings.OpenAIConfiguration; - namespace SemanticKernel.IntegrationTests.Agents.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. @@ -35,11 +32,11 @@ public sealed class OpenAIAssistantAgentTests [InlineData("What is the special soup?", "Clam Chowder")] public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) { - OpenAISettings openAISettings = this._configuration.GetSection("OpenAI").Get()!; + OpenAIConfiguration openAISettings = this._configuration.GetSection("OpenAI").Get()!; Assert.NotNull(openAISettings); await this.ExecuteAgentAsync( - OpenAIConfiguration.ForOpenAI(openAISettings.ApiKey), + OpenAIServiceConfiguration.ForOpenAI(openAISettings.ApiKey), openAISettings.ModelId, input, expectedAnswerContains); @@ -57,14 +54,14 @@ public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAn Assert.NotNull(azureOpenAIConfiguration); await this.ExecuteAgentAsync( - OpenAIConfiguration.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), + OpenAIServiceConfiguration.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), azureOpenAIConfiguration.ChatDeploymentName!, input, expectedAnswerContains); } private async Task ExecuteAgentAsync( - OpenAIConfiguration config, + OpenAIServiceConfiguration config, string modelName, string input, string expected) From 7e4cd3984fb260862cc521635e541f7b4105077c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 10:25:28 -0700 Subject: [PATCH 055/226] Fix path case for unbuntu --- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index f8efab0ea0fd..a4cd9b8b9f57 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -19,7 +19,7 @@ - + From e0a33c048f9de12ec0282f6b69d719c13c3d91e5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 10:32:56 -0700 Subject: [PATCH 056/226] Comments and orgid clean-up --- .../OpenAI/Internal/OpenAIClientFactory.cs | 5 -- .../OpenAI/OpenAIServiceConfiguration.cs | 52 +++++-------------- .../src/Http/HttpHeaderConstant.cs | 3 -- 3 files changed, 12 insertions(+), 48 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index d71973de5d7b..ddfd8aef8c31 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -73,11 +73,6 @@ private static OpenAIClientOptions CreateOpenAIClientOptions(OpenAIServiceConfig Endpoint = config.Endpoint ?? config.HttpClient?.BaseAddress, }; - if (!string.IsNullOrWhiteSpace(config.OrganizationId)) - { - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.OpenAIOrganizationId, config.OrganizationId!), PipelinePosition.PerCall); - } - ConfigureClientOptions(config.HttpClient, options); return options; diff --git a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs index 66b1c97952f9..5b93f74fb87b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs @@ -17,12 +17,11 @@ internal enum OpenAIServiceType } /// - /// %%% + /// Produce a that targets an Azure OpenAI endpoint using an API key. /// - /// - /// - /// - /// + /// The API key + /// The service endpoint + /// Custom for HTTP requests. public static OpenAIServiceConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) { Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); @@ -39,12 +38,11 @@ public static OpenAIServiceConfiguration ForAzureOpenAI(string apiKey, Uri endpo } /// - /// %%% + /// Produce a that targets an Azure OpenAI endpoint using an token credentials. /// - /// - /// - /// - /// + /// The credentials + /// The service endpoint + /// Custom for HTTP requests. public static OpenAIServiceConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) { Verify.NotNull(credential, nameof(credential)); @@ -61,12 +59,11 @@ public static OpenAIServiceConfiguration ForAzureOpenAI(TokenCredential credenti } /// - /// %%% + /// Produce a that targets OpenAI services using an API key. /// - /// - /// - /// - /// + /// The API key + /// An optional endpoint + /// Custom for HTTP requests. public static OpenAIServiceConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); @@ -80,35 +77,10 @@ public static OpenAIServiceConfiguration ForOpenAI(string apiKey, Uri? endpoint Type = OpenAIServiceType.OpenAI, }; } - /// - /// %%% - /// - /// - /// - /// - /// - /// - public static OpenAIServiceConfiguration ForOpenAI(string apiKey, string organizationId, Uri? endpoint = null, HttpClient? httpClient = null) - { - Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); - Verify.NotNullOrWhiteSpace(organizationId, nameof(organizationId)); - Verify.NotNull(endpoint, nameof(endpoint)); - - return - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - OrganizationId = organizationId, - Type = OpenAIServiceType.OpenAI, - }; - } internal string? ApiKey { get; init; } internal TokenCredential? Credential { get; init; } internal Uri? Endpoint { get; init; } internal HttpClient? HttpClient { get; init; } - internal string? OrganizationId { get; init; } internal OpenAIServiceType Type { get; init; } } diff --git a/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs b/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs index a0d0dea0b50a..db45523ee3bd 100644 --- a/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs +++ b/dotnet/src/InternalUtilities/src/Http/HttpHeaderConstant.cs @@ -13,9 +13,6 @@ public static class Names { /// HTTP header name to use to include the Semantic Kernel package version in all HTTP requests issued by Semantic Kernel. public static string SemanticKernelVersion => "Semantic-Kernel-Version"; - - /// HTTP header name to use to include the Open AI organization identifier. - public static string OpenAIOrganizationId => "OpenAI-Organization"; } public static class Values From a042a8554d148b5a97d15ed4d6fc695daa5b1197 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 10:47:49 -0700 Subject: [PATCH 057/226] Comments --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 6 +-- dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 40 ++++++++-------- .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 48 ++++++++++--------- 3 files changed, 48 insertions(+), 46 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 39dbd32ebe22..8c7ee432246e 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -47,7 +47,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// Define a new . /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service, such as the api-key. + /// Configuration for accessing the Assistants API service. /// The assistant definition. /// The to monitor for cancellation requests. The default is . /// An instance @@ -80,7 +80,7 @@ public static async Task CreateAsync( /// /// Retrieve a list of assistant definitions: . /// - /// Configuration for accessing the Assistants API service, such as the api-key. + /// Configuration for accessing the Assistants API service. /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( @@ -101,7 +101,7 @@ public static async IAsyncEnumerable ListDefinitionsA /// Retrieve a by identifier. /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service, such as the api-key. + /// Configuration for accessing the Assistants API service. /// The agent identifier /// The to monitor for cancellation requests. The default is . /// An instance diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs index 74714fd66e02..2d9bd616b21b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs @@ -9,23 +9,23 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// %%% +/// Supports management operations for a >. /// public sealed class OpenAIVectorStore { private readonly VectorStoreClient _client; /// - /// %%% + /// The identifier of the targeted vectore store /// public string VectorStoreId { get; } /// - /// %%% + /// List all vector stores. /// - /// - /// - /// + /// Configuration for accessing the vector-store service. + /// The to monitor for cancellation requests. The default is . + /// An enumeration of models. public static IAsyncEnumerable GetVectorStoresAsync(OpenAIServiceConfiguration config, CancellationToken cancellationToken = default) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); @@ -35,10 +35,10 @@ public static IAsyncEnumerable GetVectorStoresAsync(OpenAIServiceCo } /// - /// %%% + /// Initializes a new instance of the class. /// - /// - /// + /// The identifier of the targeted vectore store + /// Configuration for accessing the vector-store service. public OpenAIVectorStore(string vectorStoreId, OpenAIServiceConfiguration config) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); @@ -47,29 +47,27 @@ public OpenAIVectorStore(string vectorStoreId, OpenAIServiceConfiguration config this.VectorStoreId = vectorStoreId; } - // %%% BATCH JOBS ??? - /// - /// %%% + /// Add a file from the vector store. /// - /// - /// + /// The file to add, by identifier. + /// The to monitor for cancellation requests. The default is . /// public async Task AddFileAsync(string fileId, CancellationToken cancellationToken = default) => await this._client.AddFileToVectorStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); /// - /// %%% + /// Deletes the entire vector store. /// - /// + /// The to monitor for cancellation requests. The default is . /// public async Task DeleteAsync(CancellationToken cancellationToken = default) => await this._client.DeleteVectorStoreAsync(this.VectorStoreId, cancellationToken).ConfigureAwait(false); /// - /// %%% + /// List the files (by identifier) in the vector store. /// - /// + /// The to monitor for cancellation requests. The default is . /// public async IAsyncEnumerable GetFilesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { @@ -80,10 +78,10 @@ public async IAsyncEnumerable GetFilesAsync([EnumeratorCancellation] Can } /// - /// %%% + /// Remove a file from the vector store. /// - /// - /// + /// The file to remove, by identifier. + /// The to monitor for cancellation requests. The default is . /// public async Task RemoveFileAsync(string fileId, CancellationToken cancellationToken = default) => await this._client.RemoveFileFromStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs index ac2c23293d73..8983e1009bfc 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs @@ -9,8 +9,9 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// %%% +/// Fluent builder for creating a new . /// +/// Configuration for accessing the vector-store service. public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) { private string? _name; @@ -20,7 +21,7 @@ public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) private Dictionary? _metadata; /// - /// %%% + /// Added a file (by identifier) to the vector store. /// /// public OpenAIVectorStoreBuilder AddFile(string fileId) @@ -32,10 +33,10 @@ public OpenAIVectorStoreBuilder AddFile(string fileId) } /// - /// %%% + /// Added files (by identifier) to the vector store. /// /// - public OpenAIVectorStoreBuilder AddFile(string[] fileIds) + public OpenAIVectorStoreBuilder AddFiles(string[] fileIds) { this._fileIds ??= []; this._fileIds.AddRange(fileIds); @@ -44,10 +45,10 @@ public OpenAIVectorStoreBuilder AddFile(string[] fileIds) } /// - /// %%% + /// Define the vector store chunking strategy (if not default). /// - /// - /// + /// The maximum number of tokens in each chunk. + /// The number of tokens that overlap between chunks. public OpenAIVectorStoreBuilder WithChunkingStrategy(int maxTokensPerChunk, int overlappingTokenCount) { this._chunkingStrategy = FileChunkingStrategy.CreateStaticStrategy(maxTokensPerChunk, overlappingTokenCount); @@ -56,9 +57,9 @@ public OpenAIVectorStoreBuilder WithChunkingStrategy(int maxTokensPerChunk, int } /// - /// %%% + /// The number of days of from the last use until vector store will expire. /// - /// + /// The duration (in days) from the last usage. public OpenAIVectorStoreBuilder WithExpiration(TimeSpan duration) { this._expirationPolicy = new VectorStoreExpirationPolicy(VectorStoreExpirationAnchor.LastActiveAt, duration.Days); @@ -67,11 +68,15 @@ public OpenAIVectorStoreBuilder WithExpiration(TimeSpan duration) } /// - /// %%% + /// Adds a single key/value pair to the metadata. /// - /// - /// - /// + /// The metadata key + /// The metadata value + /// + /// The metadata is a set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// > public OpenAIVectorStoreBuilder WithMetadata(string key, string value) { this._metadata ??= []; @@ -82,10 +87,11 @@ public OpenAIVectorStoreBuilder WithMetadata(string key, string value) } /// - /// %%% + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. /// - /// - /// + /// The metadata public OpenAIVectorStoreBuilder WithMetadata(IDictionary metadata) { this._metadata ??= []; @@ -99,10 +105,9 @@ public OpenAIVectorStoreBuilder WithMetadata(IDictionary metadat } /// - /// %%% + /// Defines the name of the vector store when not anonymous. /// - /// - /// + /// The store name. public OpenAIVectorStoreBuilder WithName(string name) { this._name = name; @@ -111,10 +116,9 @@ public OpenAIVectorStoreBuilder WithName(string name) } /// - /// %%% + /// Creates a as defined. /// - /// - /// + /// The to monitor for cancellation requests. The default is . public async Task CreateAsync(CancellationToken cancellationToken = default) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); From 3081083cfa09e26bb829e3096fdae8d30b9f05c2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 10:55:54 -0700 Subject: [PATCH 058/226] More clean-up --- dotnet/Directory.Packages.props | 3 +-- .../src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 8 ++++---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index bb4233ad6ba9..f2a129da8726 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,11 +5,10 @@ true - + - diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 4481948dfa50..c7a9342e6700 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -88,14 +88,14 @@ IEnumerable GetMessageContents() { yield return MessageContent.FromText(content.ToString()); } - else if (content is ImageContent imageContent) + else if (content is ImageContent imageContent && imageContent.Data.HasValue) { yield return MessageContent.FromImageUrl( - imageContent.Uri ?? new Uri(Convert.ToBase64String(imageContent.Data?.ToArray() ?? []))); // %%% WUT A MESS - API BUG? + imageContent.Uri ?? new Uri(Convert.ToBase64String(imageContent.Data.Value.ToArray()))); } else if (content is FileReferenceContent fileContent) { - options.Attachments.Add(new MessageCreationAttachment(fileContent.FileId, [new CodeInterpreterToolDefinition()])); // %%% WUT A MESS - TOOLS? + options.Attachments.Add(new MessageCreationAttachment(fileContent.FileId, [new CodeInterpreterToolDefinition()])); } } } @@ -210,7 +210,7 @@ public static async IAsyncEnumerable InvokeAsync( // Process tool output ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - await client.SubmitToolOutputsToRunAsync(run, toolOutputs).ConfigureAwait(false); // %%% BUG CANCEL TOKEN + await client.SubmitToolOutputsToRunAsync(threadId, run.Id, toolOutputs, cancellationToken).ConfigureAwait(false); } if (logger.IsEnabled(LogLevel.Information)) // Avoid boxing if not enabled From d3b5528416fb0e6f9cf1e84e7caf48fd6468bda5 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 11:36:14 -0700 Subject: [PATCH 059/226] Coverage --- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 26 ++++++ .../OpenAIAssistantInvocationSettingsTests.cs | 68 +++++++++++++++ .../OpenAI/OpenAIConfigurationTests.cs | 44 ---------- .../OpenAI/OpenAIServiceConfigurationTests.cs | 84 +++++++++++++++++++ .../OpenAIThreadCreationSettingsTests.cs | 52 ++++++++++++ 5 files changed, 230 insertions(+), 44 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 602eddd07704..e692166986eb 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -24,8 +24,13 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); + Assert.Null(definition.ExecutionSettings); + Assert.Null(definition.Temperature); + Assert.Null(definition.TopP); Assert.Null(definition.VectorStoreId); + Assert.Null(definition.CodeInterpterFileIds); Assert.False(definition.EnableCodeInterpreter); + Assert.False(definition.EnableJsonResponse); } /// @@ -44,7 +49,19 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Description = "testdescription", VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, + Temperature = 2, + TopP = 0, + ExecutionSettings = + new() + { + MaxCompletionTokens = 1000, + MaxPromptTokens = 1000, + ParallelToolCallsEnabled = false, + TruncationMessageCount = 12, + }, + CodeInterpterFileIds = ["file1"], EnableCodeInterpreter = true, + EnableJsonResponse = true, }; Assert.Equal("testid", definition.Id); @@ -53,7 +70,16 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); Assert.Equal("#vs", definition.VectorStoreId); + Assert.Equal(2, definition.Temperature); + Assert.Equal(0, definition.TopP); + Assert.NotNull(definition.ExecutionSettings); + Assert.Equal(1000, definition.ExecutionSettings.MaxCompletionTokens); + Assert.Equal(1000, definition.ExecutionSettings.MaxPromptTokens); + Assert.Equal(12, definition.ExecutionSettings.TruncationMessageCount); + Assert.False(definition.ExecutionSettings.ParallelToolCallsEnabled); Assert.Single(definition.Metadata); + Assert.Single(definition.CodeInterpterFileIds); Assert.True(definition.EnableCodeInterpreter); + Assert.True(definition.EnableJsonResponse); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs new file mode 100644 index 000000000000..ac9ae051bf4d --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIAssistantInvocationSettingsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void OpenAIAssistantInvocationSettingsInitialState() + { + OpenAIAssistantInvocationSettings settings = new(); + + Assert.Null(settings.ModelName); + Assert.Null(settings.Metadata); + Assert.Null(settings.Temperature); + Assert.Null(settings.TopP); + Assert.Null(settings.ParallelToolCallsEnabled); + Assert.Null(settings.MaxCompletionTokens); + Assert.Null(settings.MaxPromptTokens); + Assert.Null(settings.TruncationMessageCount); + Assert.Null(settings.EnableJsonResponse); + Assert.False(settings.EnableCodeInterpreter); + Assert.False(settings.EnableFileSearch); + } + + /// + /// Verify initialization. + /// + [Fact] + public void OpenAIAssistantInvocationSettingsAssignment() + { + OpenAIAssistantInvocationSettings settings = + new() + { + ModelName = "testmodel", + Metadata = new Dictionary() { { "a", "1" } }, + MaxCompletionTokens = 1000, + MaxPromptTokens = 1000, + ParallelToolCallsEnabled = false, + TruncationMessageCount = 12, + Temperature = 2, + TopP = 0, + EnableCodeInterpreter = true, + EnableJsonResponse = true, + EnableFileSearch = true, + }; + + Assert.Equal("testmodel", settings.ModelName); + Assert.Equal(2, settings.Temperature); + Assert.Equal(0, settings.TopP); + Assert.Equal(1000, settings.MaxCompletionTokens); + Assert.Equal(1000, settings.MaxPromptTokens); + Assert.Equal(12, settings.TruncationMessageCount); + Assert.False(settings.ParallelToolCallsEnabled); + Assert.Single(settings.Metadata); + Assert.True(settings.EnableCodeInterpreter); + Assert.True(settings.EnableJsonResponse); + Assert.True(settings.EnableFileSearch); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs deleted file mode 100644 index 14237eebcd19..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIConfigurationTests.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI; - -/// -/// Unit testing of . -/// -public class OpenAIConfigurationTests -{ - /// - /// Verify initial state. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationInitialState() - { - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey"); - - Assert.Equal("testkey", config.ApiKey); - Assert.Null(config.Endpoint); - Assert.Null(config.HttpClient); - } - - /// - /// Verify assignment. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationAssignment() - { - using HttpClient client = new(); - - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); - - Assert.Equal("testkey", config.ApiKey); - Assert.NotNull(config.Endpoint); - Assert.Equal("https://localhost/", config.Endpoint.ToString()); - Assert.NotNull(config.HttpClient); - } - - // %%% MORE -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs new file mode 100644 index 000000000000..dce5d5c9ceaf --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using Azure.Core; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIServiceConfigurationTests +{ + /// + /// Verify Open AI service configuration. + /// + [Fact] + public void VerifyOpenAIAssistantConfiguration() + { + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey"); + + Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.OpenAI, config.Type); + Assert.Equal("testkey", config.ApiKey); + Assert.Null(config.Credential); + Assert.Null(config.Endpoint); + Assert.Null(config.HttpClient); + } + + /// + /// Verify Open AI service configuration with endpoint. + /// + [Fact] + public void VerifyOpenAIAssistantProxyConfiguration() + { + using HttpClient client = new(); + + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); + + Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.OpenAI, config.Type); + Assert.Equal("testkey", config.ApiKey); + Assert.Null(config.Credential); + Assert.NotNull(config.Endpoint); + Assert.Equal("https://localhost/", config.Endpoint.ToString()); + Assert.NotNull(config.HttpClient); + } + + /// + /// Verify Azure Open AI service configuration with API key. + /// + [Fact] + public void VerifyAzureOpenAIAssistantApiKeyConfiguration() + { + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost")); + + Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI, config.Type); + Assert.Equal("testkey", config.ApiKey); + Assert.Null(config.Credential); + Assert.NotNull(config.Endpoint); + Assert.Equal("https://localhost/", config.Endpoint.ToString()); + Assert.Null(config.HttpClient); + } + + /// + /// Verify Azure Open AI service configuration with API key. + /// + [Fact] + public void VerifyAzureOpenAIAssistantCredentialConfiguration() + { + using HttpClient client = new(); + + Mock credential = new(); + + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(credential.Object, endpoint: new Uri("https://localhost"), client); + + Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI, config.Type); + Assert.Null(config.ApiKey); + Assert.NotNull(config.Credential); + Assert.NotNull(config.Endpoint); + Assert.Equal("https://localhost/", config.Endpoint.ToString()); + Assert.NotNull(config.HttpClient); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs new file mode 100644 index 000000000000..0c096c0ba62e --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIThreadCreationSettingsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void OpenAIThreadCreationSettingsInitialState() + { + OpenAIThreadCreationSettings settings = new(); + + Assert.Null(settings.Messages); + Assert.Null(settings.Metadata); + Assert.Null(settings.VectorStoreId); + Assert.Null(settings.CodeInterpterFileIds); + Assert.False(settings.EnableCodeInterpreter); + } + + /// + /// Verify initialization. + /// + [Fact] + public void OpenAIThreadCreationSettingsAssignment() + { + OpenAIThreadCreationSettings definition = + new() + { + Messages = [new ChatMessageContent(AuthorRole.User, "test")], + VectorStoreId = "#vs", + Metadata = new Dictionary() { { "a", "1" } }, + CodeInterpterFileIds = ["file1"], + EnableCodeInterpreter = true, + }; + + Assert.Single(definition.Messages); + Assert.Single(definition.Metadata); + Assert.Equal("#vs", definition.VectorStoreId); + Assert.Single(definition.CodeInterpterFileIds); + Assert.True(definition.EnableCodeInterpreter); + } +} From 0ae6739e71ffa18e57c176d5c1e24b0ce1550fdd Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 11:37:52 -0700 Subject: [PATCH 060/226] Typo --- dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs index 2d9bd616b21b..a882729cde14 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs @@ -16,7 +16,7 @@ public sealed class OpenAIVectorStore private readonly VectorStoreClient _client; /// - /// The identifier of the targeted vectore store + /// The identifier of the targeted vector store /// public string VectorStoreId { get; } @@ -37,7 +37,7 @@ public static IAsyncEnumerable GetVectorStoresAsync(OpenAIServiceCo /// /// Initializes a new instance of the class. /// - /// The identifier of the targeted vectore store + /// The identifier of the targeted vector store /// Configuration for accessing the vector-store service. public OpenAIVectorStore(string vectorStoreId, OpenAIServiceConfiguration config) { From 18f6ad2e4e0028e7bb93e2499cbc2a5efc2d919f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 13:17:38 -0700 Subject: [PATCH 061/226] More test coverage --- .../OpenAI/OpenAIServiceConfiguration.cs | 4 +- .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 10 +- .../OpenAI/OpenAIClientFactoryTests.cs | 65 +++ .../OpenAI/OpenAIVectorStoreBuilderTests.cs | 129 +++++ .../OpenAI/OpenAIVectorStoreTests.cs | 546 ++++++++++++++++++ 5 files changed, 743 insertions(+), 11 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs diff --git a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs index 5b93f74fb87b..1bc6431e5487 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs @@ -64,10 +64,8 @@ public static OpenAIServiceConfiguration ForAzureOpenAI(TokenCredential credenti /// The API key /// An optional endpoint /// Custom for HTTP requests. - public static OpenAIServiceConfiguration ForOpenAI(string apiKey, Uri? endpoint = null, HttpClient? httpClient = null) + public static OpenAIServiceConfiguration ForOpenAI(string? apiKey, Uri? endpoint = null, HttpClient? httpClient = null) { - Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); - return new() { diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs index 8983e1009bfc..65e9bf5c7496 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs @@ -17,8 +17,8 @@ public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) private string? _name; private FileChunkingStrategy? _chunkingStrategy; private VectorStoreExpirationPolicy? _expirationPolicy; - private List? _fileIds; - private Dictionary? _metadata; + private readonly List _fileIds = []; + private readonly Dictionary _metadata = []; /// /// Added a file (by identifier) to the vector store. @@ -26,7 +26,6 @@ public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) /// public OpenAIVectorStoreBuilder AddFile(string fileId) { - this._fileIds ??= []; this._fileIds.Add(fileId); return this; @@ -38,7 +37,6 @@ public OpenAIVectorStoreBuilder AddFile(string fileId) /// public OpenAIVectorStoreBuilder AddFiles(string[] fileIds) { - this._fileIds ??= []; this._fileIds.AddRange(fileIds); return this; @@ -79,8 +77,6 @@ public OpenAIVectorStoreBuilder WithExpiration(TimeSpan duration) /// > public OpenAIVectorStoreBuilder WithMetadata(string key, string value) { - this._metadata ??= []; - this._metadata[key] = value; return this; @@ -94,8 +90,6 @@ public OpenAIVectorStoreBuilder WithMetadata(string key, string value) /// The metadata public OpenAIVectorStoreBuilder WithMetadata(IDictionary metadata) { - this._metadata ??= []; - foreach (KeyValuePair item in this._metadata) { this._metadata[item.Key] = item.Value; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs new file mode 100644 index 000000000000..46b45e419213 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using Azure.Core; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Moq; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIClientFactoryTests +{ + /// + /// Verify that the factory can create a client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByKey() + { + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); + OpenAIClient client = OpenAIClientFactory.CreateClient(config); + Assert.NotNull(client); + } + + /// + /// Verify that the factory can create a client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByCredential() + { + Mock mockCredential = new(); + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost")); + OpenAIClient client = OpenAIClientFactory.CreateClient(config); + Assert.NotNull(client); + } + + /// + /// Verify that the factory can create a client for various OpenAI service configurations. + /// + [Theory] + [InlineData(null, null)] + [InlineData("key", null)] + [InlineData("key", "http://myproxy:9819")] + public void VerifyOpenAIClientFactoryTargetOpenAI(string? key, string? endpoint) + { + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null); + OpenAIClient client = OpenAIClientFactory.CreateClient(config); + Assert.NotNull(client); + } + + /// + /// Verify that the factory can create a client with http proxy. + /// + [Fact] + public void VerifyOpenAIClientFactoryWithHttpClient() + { + using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") }; + OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: null, httpClient: httpClient); + OpenAIClient client = OpenAIClientFactory.CreateClient(config); + Assert.NotNull(client); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs new file mode 100644 index 000000000000..dd6734e8cb71 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI.VectorStores; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public sealed class OpenAIVectorStoreBuilderTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreBuilderEmptyAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateStore); + + VectorStore store = + await new OpenAIVectorStoreBuilder(this.CreateTestConfiguration()) + .CreateAsync(); + + Assert.NotNull(store); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreBuilderFluentAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateStore); + + Dictionary metadata = new() { { "key2", "value2" } }; + + VectorStore store = + await new OpenAIVectorStoreBuilder(this.CreateTestConfiguration()) + .WithName("my_vector_store") + .AddFile("#file_1") + .AddFiles(["#file_2", "#file_3"]) + .AddFiles(["#file_4", "#file_5"]) + .WithChunkingStrategy(1000, 400) + .WithExpiration(TimeSpan.FromDays(30)) + .WithMetadata("key1", "value1") + .WithMetadata(metadata) + .CreateAsync(); + + Assert.NotNull(store); + } + + /// + public void Dispose() + { + this._messageHandlerStub.Dispose(); + this._httpClient.Dispose(); + } + + /// + /// Initializes a new instance of the class. + /// + public OpenAIVectorStoreBuilderTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); + } + + private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) + => targetAzure ? + OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); + + private void SetupResponse(HttpStatusCode statusCode, string content) + { + this._messageHandlerStub.ResponseToReturn = + new(statusCode) + { + Content = new StringContent(content) + }; + } + + private void SetupResponses(HttpStatusCode statusCode, params string[] content) + { + foreach (var item in content) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + this._messageHandlerStub.ResponseQueue.Enqueue( + new(statusCode) + { + Content = new StringContent(item) + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + } + + private static class ResponseContent + { + public const string CreateStore = + """ + { + "id": "vs_123", + "object": "vector_store", + "created_at": 1698107661, + "usage_bytes": 123456, + "last_active_at": 1698107661, + "name": "my_vector_store", + "status": "completed", + "file_counts": { + "in_progress": 0, + "completed": 5, + "cancelled": 0, + "failed": 0, + "total": 5 + }, + "metadata": {}, + "last_used_at": 1698107661 + } + """; + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs new file mode 100644 index 000000000000..4016a7770fd9 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs @@ -0,0 +1,546 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public sealed class OpenAIVectorStoreTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + + /// + /// %%% + /// + [Fact] + public void VerifyOpenAIVectorStoreInitialization() + { + //this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); + + OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); + Assert.Equal("#vs1", store.VectorStoreId); + } + + /// + public void Dispose() + { + this._messageHandlerStub.Dispose(); + this._httpClient.Dispose(); + } + + /// + /// Initializes a new instance of the class. + /// + public OpenAIVectorStoreTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); + } + + private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) + => targetAzure ? + OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); + + private void SetupResponse(HttpStatusCode statusCode, string content) + { + this._messageHandlerStub.ResponseToReturn = + new(statusCode) + { + Content = new StringContent(content) + }; + } + + private void SetupResponses(HttpStatusCode statusCode, params string[] content) + { + foreach (var item in content) + { +#pragma warning disable CA2000 // Dispose objects before losing scope + this._messageHandlerStub.ResponseQueue.Enqueue( + new(statusCode) + { + Content = new StringContent(item) + }); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + } + + private static class ResponseContent + { + public const string CreateAgentSimple = + """ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": null, + "description": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [], + "file_ids": [], + "metadata": {} + } + """; + + public const string CreateAgentFull = + """ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": "testname", + "description": "testdescription", + "model": "gpt-4-turbo", + "instructions": "testinstructions", + "tools": [], + "file_ids": [], + "metadata": {} + } + """; + + public const string CreateAgentWithEverything = + """ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698984975, + "name": null, + "description": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [ + { + "type": "code_interpreter" + }, + { + "type": "file_search" + } + ], + "tool_resources": { + "file_search": { + "vector_store_ids": ["#vs"] + } + }, + "metadata": {"a": "1"} + } + """; + + public const string DeleteAgent = + """ + { + "id": "asst_abc123", + "object": "assistant.deleted", + "deleted": true + } + """; + + public const string CreateThread = + """ + { + "id": "thread_abc123", + "object": "thread", + "created_at": 1699012949, + "metadata": {} + } + """; + + public const string CreateRun = + """ + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699063290, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "queued", + "started_at": 1699063290, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699063291, + "last_error": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [], + "file_ids": [], + "metadata": {}, + "usage": null, + "temperature": 1 + } + """; + + public const string PendingRun = + """ + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699063290, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "requires_action", + "started_at": 1699063290, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699063291, + "last_error": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [], + "file_ids": [], + "metadata": {}, + "usage": null, + "temperature": 1 + } + """; + + public const string CompletedRun = + """ + { + "id": "run_abc123", + "object": "thread.run", + "created_at": 1699063290, + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "status": "completed", + "started_at": 1699063290, + "expires_at": null, + "cancelled_at": null, + "failed_at": null, + "completed_at": 1699063291, + "last_error": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [], + "file_ids": [], + "metadata": {}, + "usage": null, + "temperature": 1 + } + """; + + public const string MessageSteps = + """ + { + "object": "list", + "data": [ + { + "id": "step_abc123", + "object": "thread.run.step", + "created_at": 1699063291, + "run_id": "run_abc123", + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "type": "message_creation", + "status": "completed", + "cancelled_at": null, + "completed_at": 1699063291, + "expired_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "message_creation", + "message_creation": { + "message_id": "msg_abc123" + } + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + } + } + ], + "first_id": "step_abc123", + "last_id": "step_abc456", + "has_more": false + } + """; + + public const string ToolSteps = + """ + { + "object": "list", + "data": [ + { + "id": "step_abc987", + "object": "thread.run.step", + "created_at": 1699063291, + "run_id": "run_abc123", + "assistant_id": "asst_abc123", + "thread_id": "thread_abc123", + "type": "tool_calls", + "status": "in_progress", + "cancelled_at": null, + "completed_at": 1699063291, + "expired_at": null, + "failed_at": null, + "last_error": null, + "step_details": { + "type": "tool_calls", + "tool_calls": [ + { + "id": "tool_1", + "type": "function", + "function": { + "name": "MyPlugin-MyFunction", + "arguments": "{ \"index\": 3 }", + "output": "test" + } + } + ] + }, + "usage": { + "prompt_tokens": 123, + "completion_tokens": 456, + "total_tokens": 579 + } + } + ], + "first_id": "step_abc123", + "last_id": "step_abc456", + "has_more": false + } + """; + + public const string ToolResponse = "{ }"; + + public const string GetImageMessage = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "image_file", + "image_file": { + "file_id": "file_123" + } + } + ], + "assistant_id": "asst_abc123", + "run_id": "run_abc123" + } + """; + + public const string GetTextMessage = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "assistant_id": "asst_abc123", + "run_id": "run_abc123" + } + """; + + public const string GetTextMessageWithAnnotation = + """ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699017614, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.**f1", + "annotations": [ + { + "type": "file_citation", + "text": "**f1", + "file_citation": { + "file_id": "file_123", + "quote": "does" + }, + "start_index": 3, + "end_index": 6 + } + ] + } + } + ], + "assistant_id": "asst_abc123", + "run_id": "run_abc123" + } + """; + + public const string ListAgentsPageMore = + """ + { + "object": "list", + "data": [ + { + "id": "asst_abc123", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "model": "gpt-4-turbo", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "metadata": {} + }, + { + "id": "asst_abc456", + "object": "assistant", + "created_at": 1698982718, + "name": "My Assistant", + "description": null, + "model": "gpt-4-turbo", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "metadata": {} + }, + { + "id": "asst_abc789", + "object": "assistant", + "created_at": 1698982643, + "name": null, + "description": null, + "model": "gpt-4-turbo", + "instructions": null, + "tools": [], + "metadata": {} + } + ], + "first_id": "asst_abc123", + "last_id": "asst_abc789", + "has_more": true + } + """; + + public const string ListAgentsPageFinal = + """ + { + "object": "list", + "data": [ + { + "id": "asst_abc789", + "object": "assistant", + "created_at": 1698982736, + "name": "Coding Tutor", + "description": null, + "model": "gpt-4-turbo", + "instructions": "You are a helpful assistant designed to make me better at coding!", + "tools": [], + "metadata": {} + } + ], + "first_id": "asst_abc789", + "last_id": "asst_abc789", + "has_more": false + } + """; + + public const string ListMessagesPageMore = + """ + { + "object": "list", + "data": [ + { + "id": "msg_abc123", + "object": "thread.message", + "created_at": 1699016383, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + }, + { + "id": "msg_abc456", + "object": "thread.message", + "created_at": 1699016383, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "Hello, what is AI?", + "annotations": [] + } + } + ], + "file_ids": [ + "file-abc123" + ], + "assistant_id": null, + "run_id": null, + "metadata": {} + } + ], + "first_id": "msg_abc123", + "last_id": "msg_abc456", + "has_more": true + } + """; + + public const string ListMessagesPageFinal = + """ + { + "object": "list", + "data": [ + { + "id": "msg_abc789", + "object": "thread.message", + "created_at": 1699016383, + "thread_id": "thread_abc123", + "role": "user", + "content": [ + { + "type": "text", + "text": { + "value": "How does AI work? Explain it in simple terms.", + "annotations": [] + } + } + ], + "file_ids": [], + "assistant_id": null, + "run_id": null, + "metadata": {} + } + ], + "first_id": "msg_abc789", + "last_id": "msg_abc789", + "has_more": false + } + """; + } +} From 2f459f000470b178c148c4ca98345b1ef9c2d831 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 13:38:15 -0700 Subject: [PATCH 062/226] More coverage --- .../OpenAI/OpenAIVectorStoreTests.cs | 557 ++++-------------- 1 file changed, 126 insertions(+), 431 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs index 4016a7770fd9..dbcdef23f8b7 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Linq; using System.Net; using System.Net.Http; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI.VectorStores; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI; @@ -22,12 +24,79 @@ public sealed class OpenAIVectorStoreTests : IDisposable [Fact] public void VerifyOpenAIVectorStoreInitialization() { - //this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); Assert.Equal("#vs1", store.VectorStoreId); } + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreDeleteAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteStore); + + OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); + bool isDeleted = await store.DeleteAsync(); + + Assert.True(isDeleted); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreListAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.ListStores); + + VectorStore[] stores = await OpenAIVectorStore.GetVectorStoresAsync(this.CreateTestConfiguration()).ToArrayAsync(); + + Assert.Equal(2, stores.Length); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreAddFileAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.AddFile); + + OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); + await store.AddFileAsync("#file_1"); + + // %%% VERIFY + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreRemoveFileAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteFile); + + OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); + bool isDeleted = await store.RemoveFileAsync("#file_1"); + + Assert.True(isDeleted); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIVectorStoreGetFilesAsync() + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.ListFiles); + + OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); + string[] files = await store.GetFilesAsync().ToArrayAsync(); + + Assert.Equal(2, files.Length); + } + /// public void Dispose() { @@ -74,473 +143,99 @@ private void SetupResponses(HttpStatusCode statusCode, params string[] content) private static class ResponseContent { - public const string CreateAgentSimple = - """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {} - } - """; - - public const string CreateAgentFull = - """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": "testname", - "description": "testdescription", - "model": "gpt-4-turbo", - "instructions": "testinstructions", - "tools": [], - "file_ids": [], - "metadata": {} - } - """; - - public const string CreateAgentWithEverything = - """ - { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [ - { - "type": "code_interpreter" - }, - { - "type": "file_search" - } - ], - "tool_resources": { - "file_search": { - "vector_store_ids": ["#vs"] - } - }, - "metadata": {"a": "1"} - } - """; - - public const string DeleteAgent = - """ - { - "id": "asst_abc123", - "object": "assistant.deleted", - "deleted": true - } - """; - - public const string CreateThread = - """ - { - "id": "thread_abc123", - "object": "thread", - "created_at": 1699012949, - "metadata": {} - } - """; - - public const string CreateRun = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699063290, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "queued", - "started_at": 1699063290, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063291, - "last_error": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {}, - "usage": null, - "temperature": 1 - } - """; - - public const string PendingRun = - """ - { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699063290, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "status": "requires_action", - "started_at": 1699063290, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063291, - "last_error": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {}, - "usage": null, - "temperature": 1 - } - """; - - public const string CompletedRun = + public const string AddFile = """ { - "id": "run_abc123", - "object": "thread.run", - "created_at": 1699063290, - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", + "id": "#file_1", + "object": "vector_store.file", + "created_at": 1699061776, + "usage_bytes": 1234, + "vector_store_id": "vs_abcd", "status": "completed", - "started_at": 1699063290, - "expires_at": null, - "cancelled_at": null, - "failed_at": null, - "completed_at": 1699063291, - "last_error": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {}, - "usage": null, - "temperature": 1 - } - """; - - public const string MessageSteps = - """ - { - "object": "list", - "data": [ - { - "id": "step_abc123", - "object": "thread.run.step", - "created_at": 1699063291, - "run_id": "run_abc123", - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "type": "message_creation", - "status": "completed", - "cancelled_at": null, - "completed_at": 1699063291, - "expired_at": null, - "failed_at": null, - "last_error": null, - "step_details": { - "type": "message_creation", - "message_creation": { - "message_id": "msg_abc123" - } - }, - "usage": { - "prompt_tokens": 123, - "completion_tokens": 456, - "total_tokens": 579 - } - } - ], - "first_id": "step_abc123", - "last_id": "step_abc456", - "has_more": false - } - """; - - public const string ToolSteps = - """ - { - "object": "list", - "data": [ - { - "id": "step_abc987", - "object": "thread.run.step", - "created_at": 1699063291, - "run_id": "run_abc123", - "assistant_id": "asst_abc123", - "thread_id": "thread_abc123", - "type": "tool_calls", - "status": "in_progress", - "cancelled_at": null, - "completed_at": 1699063291, - "expired_at": null, - "failed_at": null, - "last_error": null, - "step_details": { - "type": "tool_calls", - "tool_calls": [ - { - "id": "tool_1", - "type": "function", - "function": { - "name": "MyPlugin-MyFunction", - "arguments": "{ \"index\": 3 }", - "output": "test" - } - } - ] - }, - "usage": { - "prompt_tokens": 123, - "completion_tokens": 456, - "total_tokens": 579 - } - } - ], - "first_id": "step_abc123", - "last_id": "step_abc456", - "has_more": false - } - """; - - public const string ToolResponse = "{ }"; - - public const string GetImageMessage = - """ - { - "id": "msg_abc123", - "object": "thread.message", - "created_at": 1699017614, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "image_file", - "image_file": { - "file_id": "file_123" - } - } - ], - "assistant_id": "asst_abc123", - "run_id": "run_abc123" + "last_error": null } """; - public const string GetTextMessage = + public const string DeleteFile = """ { - "id": "msg_abc123", - "object": "thread.message", - "created_at": 1699017614, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "How does AI work? Explain it in simple terms.", - "annotations": [] - } - } - ], - "assistant_id": "asst_abc123", - "run_id": "run_abc123" + "id": "#file_1", + "object": "vector_store.file.deleted", + "deleted": true } """; - public const string GetTextMessageWithAnnotation = + public const string DeleteStore = """ { - "id": "msg_abc123", - "object": "thread.message", - "created_at": 1699017614, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "How does AI work? Explain it in simple terms.**f1", - "annotations": [ - { - "type": "file_citation", - "text": "**f1", - "file_citation": { - "file_id": "file_123", - "quote": "does" - }, - "start_index": 3, - "end_index": 6 - } - ] - } - } - ], - "assistant_id": "asst_abc123", - "run_id": "run_abc123" + "id": "vs_abc123", + "object": "vector_store.deleted", + "deleted": true } """; - public const string ListAgentsPageMore = + public const string ListFiles = """ { "object": "list", "data": [ { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698982736, - "name": "Coding Tutor", - "description": null, - "model": "gpt-4-turbo", - "instructions": "You are a helpful assistant designed to make me better at coding!", - "tools": [], - "metadata": {} + "id": "file-abc123", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" }, { - "id": "asst_abc456", - "object": "assistant", - "created_at": 1698982718, - "name": "My Assistant", - "description": null, - "model": "gpt-4-turbo", - "instructions": "You are a helpful assistant designed to make me better at coding!", - "tools": [], - "metadata": {} - }, - { - "id": "asst_abc789", - "object": "assistant", - "created_at": 1698982643, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "metadata": {} + "id": "file-abc456", + "object": "vector_store.file", + "created_at": 1699061776, + "vector_store_id": "vs_abc123" } ], - "first_id": "asst_abc123", - "last_id": "asst_abc789", - "has_more": true - } - """; - - public const string ListAgentsPageFinal = - """ - { - "object": "list", - "data": [ - { - "id": "asst_abc789", - "object": "assistant", - "created_at": 1698982736, - "name": "Coding Tutor", - "description": null, - "model": "gpt-4-turbo", - "instructions": "You are a helpful assistant designed to make me better at coding!", - "tools": [], - "metadata": {} - } - ], - "first_id": "asst_abc789", - "last_id": "asst_abc789", + "first_id": "file-abc123", + "last_id": "file-abc456", "has_more": false - } + } """; - public const string ListMessagesPageMore = + public const string ListStores = """ { "object": "list", "data": [ { - "id": "msg_abc123", - "object": "thread.message", - "created_at": 1699016383, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "How does AI work? Explain it in simple terms.", - "annotations": [] - } - } - ], - "file_ids": [], - "assistant_id": null, - "run_id": null, - "metadata": {} + "id": "vs_abc123", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } }, { - "id": "msg_abc456", - "object": "thread.message", - "created_at": 1699016383, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "Hello, what is AI?", - "annotations": [] - } - } - ], - "file_ids": [ - "file-abc123" - ], - "assistant_id": null, - "run_id": null, - "metadata": {} - } - ], - "first_id": "msg_abc123", - "last_id": "msg_abc456", - "has_more": true - } - """; - - public const string ListMessagesPageFinal = - """ - { - "object": "list", - "data": [ - { - "id": "msg_abc789", - "object": "thread.message", - "created_at": 1699016383, - "thread_id": "thread_abc123", - "role": "user", - "content": [ - { - "type": "text", - "text": { - "value": "How does AI work? Explain it in simple terms.", - "annotations": [] - } - } - ], - "file_ids": [], - "assistant_id": null, - "run_id": null, - "metadata": {} + "id": "vs_abc456", + "object": "vector_store", + "created_at": 1699061776, + "name": "Support FAQ v2", + "bytes": 139920, + "file_counts": { + "in_progress": 0, + "completed": 3, + "failed": 0, + "cancelled": 0, + "total": 3 + } } ], - "first_id": "msg_abc789", - "last_id": "msg_abc789", + "first_id": "vs_abc123", + "last_id": "vs_abc456", "has_more": false - } + } """; } } From 4d2914be632880036b81b8d1962b69e3fd37166e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 8 Jul 2024 14:01:34 -0700 Subject: [PATCH 063/226] Partial arc optimization --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 10 ++- .../OpenAI/OpenAIAssistantAgentTests.cs | 74 ++++++++++++++++++- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 8c7ee432246e..98d6768c6a42 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -306,6 +306,10 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod settings = JsonSerializer.Deserialize(settingsJson); } + IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds; + string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(); + bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject; + return new() { @@ -313,14 +317,14 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod Name = model.Name, Description = model.Description, Instructions = model.Instructions, - CodeInterpterFileIds = (IReadOnlyList?)(model.ToolResources?.CodeInterpreter?.FileIds), + CodeInterpterFileIds = fileIds, EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), Metadata = model.Metadata, ModelName = model.Model, - EnableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject, + EnableJsonResponse = enableJsonResponse, TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, - VectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(), + VectorStoreId = vectorStoreId, ExecutionSettings = settings, }; } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 01285e2372a2..3d67537cb590 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -85,11 +85,10 @@ await OpenAIAssistantAgent.CreateAsync( } /// - /// Verify the invocation and response of - /// for an agent that has all properties defined.. + /// %%% /// [Fact] - public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() + public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() // %%% NAME { OpenAIAssistantDefinition definition = new() @@ -113,6 +112,75 @@ await OpenAIAssistantAgent.CreateAsync( Assert.True(agent.Tools.OfType().Any()); Assert.True(agent.Tools.OfType().Any()); Assert.Equal("#vs", agent.Definition.VectorStoreId); + Assert.Null(agent.Definition.CodeInterpterFileIds); + Assert.NotNull(agent.Definition.Metadata); + Assert.NotEmpty(agent.Definition.Metadata); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationEverything2Async() // %%% NAME + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + EnableCodeInterpreter = true, + CodeInterpterFileIds = ["file1", "file2"], + Metadata = new Dictionary() { { "a", "1" } }, + }; + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + this._emptyKernel, + this.CreateTestConfiguration(), + definition); + + Assert.NotNull(agent); + Assert.Equal(2, agent.Tools.Count); + Assert.True(agent.Tools.OfType().Any()); + Assert.True(agent.Tools.OfType().Any()); + //Assert.Null(agent.Definition.VectorStoreId); // %%% SETUP + //Assert.Null(agent.Definition.CodeInterpterFileIds); // %%% SETUP + Assert.NotNull(agent.Definition.Metadata); + Assert.NotEmpty(agent.Definition.Metadata); + } + + /// + /// %%% + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationEverything3Async() // %%% NAME + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + EnableCodeInterpreter = false, + EnableJsonResponse = true, + CodeInterpterFileIds = ["file1", "file2"], + Metadata = new Dictionary() { { "a", "1" } }, + ExecutionSettings = new(), + }; + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + this._emptyKernel, + this.CreateTestConfiguration(), + definition); + + Assert.NotNull(agent); + Assert.Equal(2, agent.Tools.Count); + Assert.True(agent.Tools.OfType().Any()); + Assert.True(agent.Tools.OfType().Any()); + //Assert.Null(agent.Definition.VectorStoreId); // %%% SETUP + //Assert.Null(agent.Definition.CodeInterpterFileIds); // %%% SETUP Assert.NotNull(agent.Definition.Metadata); Assert.NotEmpty(agent.Definition.Metadata); } From f5b9bdc8d539818a910928a72133cb4808b1e5a2 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 9 Jul 2024 15:01:26 +0100 Subject: [PATCH 064/226] .Net: OpenAI V2 Connector - ChatCompletion + FC - Phase 06 (#7138) ### Motivation and Context This PR does the following: 1. Migrates OpenAIChatCompletionService, ClientCore, and other model classes both use, to OpenAI SDK v2. 2. Updates ToolCallBehavior classes to return a list of functions and function choice. This change is required because the new SDK model requires both of those for the CompletionsOptions class creation and does not allow setting them after the class is already created, as it used to allow. 3. Adapts related unit tests to the API changes. 4. Added netstandard2.0 as target for the UnitTest project. 5. Rename `TryGetFunctionAndArguments` to `TryGetOpenAIFunctionAndArguments` to avoid clashing with AzureOpenAI --- .../Connectors.OpenAIV2.UnitTests.csproj | 45 +- .../Core/AutoFunctionInvocationFilterTests.cs | 632 +++++++++ .../Core/OpenAIChatMessageContentTests.cs | 117 ++ .../Core/OpenAIFunctionTests.cs | 189 +++ .../Core/OpenAIFunctionToolCallTests.cs | 82 ++ ...ithDataStreamingChatMessageContentTests.cs | 138 ++ .../Extensions/ChatHistoryExtensionsTests.cs | 46 + .../KernelBuilderExtensionsTests.cs | 44 + .../KernelFunctionMetadataExtensionsTests.cs | 257 ++++ .../OpenAIPluginCollectionExtensionsTests.cs | 76 ++ .../ServiceCollectionExtensionsTests.cs | 43 + .../Models/OpenAIFileReferenceTests.cs | 24 + .../Services/OpenAIAudioToTextServiceTests.cs | 21 + .../OpenAIChatCompletionServiceTests.cs | 1050 +++++++++++++++ .../Services/OpenAIFileServiceTests.cs | 28 +- .../Services/OpenAITextToAudioServiceTests.cs | 9 +- .../OpenAIPromptExecutionSettingsTests.cs | 271 ++++ ...letion_invalid_streaming_test_response.txt | 5 + ...multiple_function_calls_test_response.json | 64 + ...on_single_function_call_test_response.json | 32 + ..._multiple_function_calls_test_response.txt | 9 + ...ing_single_function_call_test_response.txt | 3 + ...hat_completion_streaming_test_response.txt | 5 + .../chat_completion_test_response.json | 22 + ...tion_with_data_streaming_test_response.txt | 1 + ...at_completion_with_data_test_response.json | 28 + ...multiple_function_calls_test_response.json | 40 + ..._multiple_function_calls_test_response.txt | 5 + .../ToolCallBehaviorTests.cs | 242 ++++ .../Connectors.OpenAIV2.csproj | 2 +- .../Core/ClientCore.ChatCompletion.cs | 1189 +++++++++++++++++ .../Connectors.OpenAIV2/Core/ClientCore.cs | 18 + .../Core/OpenAIChatMessageContent.cs | 134 ++ .../Core/OpenAIFunction.cs | 178 +++ .../Core/OpenAIFunctionToolCall.cs | 170 +++ .../Core/OpenAIStreamingChatMessageContent.cs | 104 ++ .../Extensions/ChatHistoryExtensions.cs | 70 + .../OpenAIKernelBuilderExtensions.cs | 105 ++ .../OpenAIKernelFunctionMetadataExtensions.cs | 54 + .../OpenAIPluginCollectionExtensions.cs | 62 + .../OpenAIServiceCollectionExtensions.cs | 100 ++ .../Services/OpenAIChatCompletionService.cs | 152 +++ .../Settings/OpenAIPromptExecutionSettings.cs | 372 ++++++ .../Connectors.OpenAIV2/ToolCallBehavior.cs | 281 ++++ .../OpenAI/OpenAIChatCompletionTests.cs | 270 ++++ ...enAIChatCompletion_FunctionCallingTests.cs | 777 +++++++++++ .../OpenAIChatCompletion_NonStreamingTests.cs | 169 +++ .../OpenAIChatCompletion_StreamingTests.cs | 177 +++ .../src/Diagnostics/ModelDiagnostics.cs | 2 + 49 files changed, 7890 insertions(+), 24 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj index 80e71aa16760..8ac5c7716e98 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj @@ -3,7 +3,7 @@ SemanticKernel.Connectors.OpenAI.UnitTests $(AssemblyName) - net8.0 + net8.0 true enable false @@ -14,10 +14,6 @@ - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -25,8 +21,12 @@ + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + - + @@ -41,6 +41,39 @@ + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + + + Always + Always diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs new file mode 100644 index 000000000000..5df2fb54cdb5 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs @@ -0,0 +1,632 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +public sealed class AutoFunctionInvocationFilterTests : IDisposable +{ + private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; + private readonly HttpClient _httpClient; + + public AutoFunctionInvocationFilterTests() + { + this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); + + this._httpClient = new HttpClient(this._messageHandlerStub, false); + } + + [Fact] + public async Task FiltersAreExecutedCorrectlyAsync() + { + // Arrange + int filterInvocations = 0; + int functionInvocations = 0; + int[] expectedRequestSequenceNumbers = [0, 0, 1, 1]; + int[] expectedFunctionSequenceNumbers = [0, 1, 0, 1]; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + Kernel? contextKernel = null; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + contextKernel = context.Kernel; + + if (context.ChatHistory.Last() is OpenAIChatMessageContent content) + { + Assert.Equal(2, content.ToolCalls.Count); + } + + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + filterInvocations++; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(4, filterInvocations); + Assert.Equal(4, functionInvocations); + Assert.Equal(expectedRequestSequenceNumbers, requestSequenceNumbers); + Assert.Equal(expectedFunctionSequenceNumbers, functionSequenceNumbers); + Assert.Same(kernel, contextKernel); + Assert.Equal("Test chat response", result.ToString()); + } + + [Fact] + public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() + { + // Arrange + int filterInvocations = 0; + int functionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + if (context.ChatHistory.Last() is OpenAIChatMessageContent content) + { + Assert.Equal(2, content.ToolCalls.Count); + } + + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + filterInvocations++; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { } + + // Assert + Assert.Equal(4, filterInvocations); + Assert.Equal(4, functionInvocations); + Assert.Equal([0, 0, 1, 1], requestSequenceNumbers); + Assert.Equal([0, 1, 0, 1], functionSequenceNumbers); + } + + [Fact] + public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); + var executionOrder = new List(); + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var filter1 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter1-Invoking"); + await next(context); + }); + + var filter2 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter2-Invoking"); + await next(context); + }); + + var builder = Kernel.CreateBuilder(); + + builder.Plugins.Add(plugin); + + builder.Services.AddSingleton((serviceProvider) => + { + return new OpenAIChatCompletionService("model-id", "test-api-key", "organization-id", this._httpClient); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + + // Case #1 - Add filter to services + builder.Services.AddSingleton(filter1); + + var kernel = builder.Build(); + + // Case #2 - Add filter to kernel + kernel.AutoFunctionInvocationFilters.Add(filter2); + + var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal("Filter1-Invoking", executionOrder[0]); + Assert.Equal("Filter2-Invoking", executionOrder[1]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); + var executionOrder = new List(); + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var filter1 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter1-Invoking"); + await next(context); + executionOrder.Add("Filter1-Invoked"); + }); + + var filter2 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter2-Invoking"); + await next(context); + executionOrder.Add("Filter2-Invoked"); + }); + + var filter3 = new AutoFunctionInvocationFilter(async (context, next) => + { + executionOrder.Add("Filter3-Invoking"); + await next(context); + executionOrder.Add("Filter3-Invoked"); + }); + + var builder = Kernel.CreateBuilder(); + + builder.Plugins.Add(plugin); + + builder.Services.AddSingleton((serviceProvider) => + { + return new OpenAIChatCompletionService("model-id", "test-api-key", "organization-id", this._httpClient); + }); + + builder.Services.AddSingleton(filter1); + builder.Services.AddSingleton(filter2); + builder.Services.AddSingleton(filter3); + + var kernel = builder.Build(); + + var arguments = new KernelArguments(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + }); + + // Act + if (isStreaming) + { + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", arguments)) + { } + } + else + { + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + await kernel.InvokePromptAsync("Test prompt", arguments); + } + + // Assert + Assert.Equal("Filter1-Invoking", executionOrder[0]); + Assert.Equal("Filter2-Invoking", executionOrder[1]); + Assert.Equal("Filter3-Invoking", executionOrder[2]); + Assert.Equal("Filter3-Invoked", executionOrder[3]); + Assert.Equal("Filter2-Invoked", executionOrder[4]); + Assert.Equal("Filter1-Invoked", executionOrder[5]); + } + + [Fact] + public async Task FilterCanOverrideArgumentsAsync() + { + // Arrange + const string NewValue = "NewValue"; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + context.Arguments!["parameter"] = NewValue; + await next(context); + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal("NewValue", result.ToString()); + } + + [Fact] + public async Task FilterCanHandleExceptionAsync() + { + // Arrange + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + try + { + await next(context); + } + catch (KernelException exception) + { + Assert.Equal("Exception from Function1", exception.Message); + context.Result = new FunctionResult(context.Result, "Result from filter"); + } + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + var chatCompletion = new OpenAIChatCompletionService("model-id", "test-api-key", "organization-id", this._httpClient); + + var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("System message"); + + // Act + var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); + + var firstFunctionResult = chatHistory[^2].Content; + var secondFunctionResult = chatHistory[^1].Content; + + // Assert + Assert.Equal("Result from filter", firstFunctionResult); + Assert.Equal("Result from Function2", secondFunctionResult); + } + + [Fact] + public async Task FilterCanHandleExceptionOnStreamingAsync() + { + // Arrange + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + try + { + await next(context); + } + catch (KernelException) + { + context.Result = new FunctionResult(context.Result, "Result from filter"); + } + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var chatCompletion = new OpenAIChatCompletionService("model-id", "test-api-key", "organization-id", this._httpClient); + + var chatHistory = new ChatHistory(); + var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) + { } + + var firstFunctionResult = chatHistory[^2].Content; + var secondFunctionResult = chatHistory[^1].Content; + + // Assert + Assert.Equal("Result from filter", firstFunctionResult); + Assert.Equal("Result from Function2", secondFunctionResult); + } + + [Fact] + public async Task FiltersCanSkipFunctionExecutionAsync() + { + // Arrange + int filterInvocations = 0; + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Filter delegate is invoked only for second function, the first one should be skipped. + if (context.Function.Name == "Function2") + { + await next(context); + } + + filterInvocations++; + }); + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(File.ReadAllText("TestData/filters_multiple_function_calls_test_response.json")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) }; + + this._messageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(2, filterInvocations); + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(1, secondFunctionInvocations); + } + + [Fact] + public async Task PreFilterCanTerminateOperationAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Terminating before first function, so all functions won't be invoked. + context.Terminate = true; + + await next(context); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + } + + [Fact] + public async Task PreFilterCanTerminateOperationOnStreamingAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + // Terminating before first function, so all functions won't be invoked. + context.Terminate = true; + + await next(context); + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { } + + // Assert + Assert.Equal(0, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + } + + [Fact] + public async Task PostFilterCanTerminateOperationAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + // Terminating after first function, so second function won't be invoked. + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); + + // Act + var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings + { + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions + })); + + // Assert + Assert.Equal(1, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + Assert.Equal([0], requestSequenceNumbers); + Assert.Equal([0], functionSequenceNumbers); + + // Results of function invoked before termination should be returned + var lastMessageContent = result.GetValue(); + Assert.NotNull(lastMessageContent); + + Assert.Equal("function1-value", lastMessageContent.Content); + Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); + } + + [Fact] + public async Task PostFilterCanTerminateOperationOnStreamingAsync() + { + // Arrange + int firstFunctionInvocations = 0; + int secondFunctionInvocations = 0; + List requestSequenceNumbers = []; + List functionSequenceNumbers = []; + + var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); + var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); + + var kernel = this.GetKernelWithFilter(plugin, async (context, next) => + { + requestSequenceNumbers.Add(context.RequestSequenceIndex); + functionSequenceNumbers.Add(context.FunctionSequenceIndex); + + await next(context); + + // Terminating after first function, so second function won't be invoked. + context.Terminate = true; + }); + + this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); + + var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + List streamingContent = []; + + // Act + await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) + { + streamingContent.Add(item); + } + + // Assert + Assert.Equal(1, firstFunctionInvocations); + Assert.Equal(0, secondFunctionInvocations); + Assert.Equal([0], requestSequenceNumbers); + Assert.Equal([0], functionSequenceNumbers); + + // Results of function invoked before termination should be returned + Assert.Equal(3, streamingContent.Count); + + var lastMessageContent = streamingContent[^1] as StreamingChatMessageContent; + Assert.NotNull(lastMessageContent); + + Assert.Equal("function1-value", lastMessageContent.Content); + Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + } + + #region private + +#pragma warning disable CA2000 // Dispose objects before losing scope + private static List GetFunctionCallingResponses() + { + return [ + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/filters_multiple_function_calls_test_response.json")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/filters_multiple_function_calls_test_response.json")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_test_response.json")) } + ]; + } + + private static List GetFunctionCallingStreamingResponses() + { + return [ + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/filters_streaming_multiple_function_calls_test_response.txt")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/filters_streaming_multiple_function_calls_test_response.txt")) }, + new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) } + ]; + } +#pragma warning restore CA2000 + + private Kernel GetKernelWithFilter( + KernelPlugin plugin, + Func, Task>? onAutoFunctionInvocation) + { + var builder = Kernel.CreateBuilder(); + var filter = new AutoFunctionInvocationFilter(onAutoFunctionInvocation); + + builder.Plugins.Add(plugin); + builder.Services.AddSingleton(filter); + + builder.Services.AddSingleton((serviceProvider) => + { + return new OpenAIChatCompletionService("model-id", "test-api-key", "organization-id", this._httpClient); + }); + + return builder.Build(); + } + + private sealed class AutoFunctionInvocationFilter( + Func, Task>? onAutoFunctionInvocation) : IAutoFunctionInvocationFilter + { + private readonly Func, Task>? _onAutoFunctionInvocation = onAutoFunctionInvocation; + + public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) => + this._onAutoFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs new file mode 100644 index 000000000000..7860c375e9cb --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections; +using System.Collections.Generic; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIChatMessageContentTests +{ + [Fact] + public void ConstructorsWorkCorrectly() + { + // Arrange + List toolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", "args")]; + + // Act + var content1 = new OpenAIChatMessageContent(ChatMessageRole.User, "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); + + // Assert + this.AssertChatMessageContent(AuthorRole.User, "content1", "model-id1", toolCalls, content1, "Fred"); + this.AssertChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, content2); + } + + [Fact] + public void GetOpenAIFunctionToolCallsReturnsCorrectList() + { + // Arrange + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; + + var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); + + // Act + var actualToolCalls1 = content1.GetFunctionToolCalls(); + var actualToolCalls2 = content2.GetFunctionToolCalls(); + + // Assert + Assert.Equal(2, actualToolCalls1.Count); + Assert.Equal("id1", actualToolCalls1[0].Id); + Assert.Equal("id2", actualToolCalls1[1].Id); + + Assert.Empty(actualToolCalls2); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) + { + // Arrange + IReadOnlyDictionary metadata = readOnlyMetadata ? + new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : + new Dictionary { { "key", "value" } }; + + List toolCalls = [ + ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), + ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; + + // Act + var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); + var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, metadata); + + // Assert + Assert.NotNull(content1.Metadata); + Assert.Single(content1.Metadata); + + Assert.NotNull(content2.Metadata); + Assert.Equal(2, content2.Metadata.Count); + Assert.Equal("value", content2.Metadata["key"]); + + Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); + + var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; + Assert.NotNull(actualToolCalls); + + Assert.Equal(2, actualToolCalls.Count); + Assert.Equal("id1", actualToolCalls[0].Id); + Assert.Equal("id2", actualToolCalls[1].Id); + } + + private void AssertChatMessageContent( + AuthorRole expectedRole, + string expectedContent, + string expectedModelId, + IReadOnlyList expectedToolCalls, + OpenAIChatMessageContent actualContent, + string? expectedName = null) + { + Assert.Equal(expectedRole, actualContent.Role); + Assert.Equal(expectedContent, actualContent.Content); + Assert.Equal(expectedName, actualContent.AuthorName); + Assert.Equal(expectedModelId, actualContent.ModelId); + Assert.Same(expectedToolCalls, actualContent.ToolCalls); + } + + private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> + { + public TValue this[TKey key] => dictionary[key]; + public IEnumerable Keys => dictionary.Keys; + public IEnumerable Values => dictionary.Values; + public int Count => dictionary.Count; + public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); + public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); + public bool TryGetValue(TKey key, out TValue value) => dictionary.TryGetValue(key, out value!); + IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs new file mode 100644 index 000000000000..1967ee882ec8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs @@ -0,0 +1,189 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +public sealed class OpenAIFunctionTests +{ + [Theory] + [InlineData(null, null, "", "")] + [InlineData("name", "description", "name", "description")] + public void ItInitializesOpenAIFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new OpenAIFunctionParameter(name, description, true, typeof(string), schema); + + // Assert + Assert.Equal(expectedName, functionParameter.Name); + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.True(functionParameter.IsRequired); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + + [Theory] + [InlineData(null, "")] + [InlineData("description", "description")] + public void ItInitializesOpenAIFunctionReturnParameterCorrectly(string? description, string expectedDescription) + { + // Arrange & Act + var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); + var functionParameter = new OpenAIFunctionReturnParameter(description, typeof(string), schema); + + // Assert + Assert.Equal(expectedDescription, functionParameter.Description); + Assert.Equal(typeof(string), functionParameter.ParameterType); + Assert.Same(schema, functionParameter.Schema); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithNoPluginName() + { + // Arrange + OpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToOpenAIFunction(); + + // Act + ChatTool result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal(sut.FunctionName, result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithNullParameters() + { + // Arrange + OpenAIFunction sut = new("plugin", "function", "description", null, null); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.FunctionParameters.ToString()); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionWithPluginName() + { + // Arrange + OpenAIFunction sut = KernelPluginFactory.CreateFromFunctions("myplugin", new[] + { + KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.") + }).GetFunctionsMetadata()[0].ToOpenAIFunction(); + + // Act + ChatTool result = sut.ToFunctionDefinition(); + + // Assert + Assert.Equal("myplugin-myfunc", result.FunctionName); + Assert.Equal(sut.Description, result.FunctionDescription); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() + { + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + + KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] + { + KernelFunctionFactory.CreateFromMethod( + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", + "TestFunction", + "My test function") + }); + + OpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); + + ChatTool functionDefinition = sut.ToFunctionDefinition(); + + var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); + var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters)); + + Assert.NotNull(functionDefinition); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() + { + string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; + + KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] + { + KernelFunctionFactory.CreateFromMethod( + [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, + "TestFunction", + "My test function") + }); + + OpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); + + ChatTool functionDefinition = sut.ToFunctionDefinition(); + + Assert.NotNull(functionDefinition); + Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); + Assert.Equal("My test function", functionDefinition.FunctionDescription); + Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() + { + // Arrange + OpenAIFunction f = KernelFunctionFactory.CreateFromMethod( + () => { }, + parameters: [new KernelParameterMetadata("param1")]).Metadata.ToOpenAIFunction(); + + // Act + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; + + // Assert + Assert.NotNull(pd.properties); + Assert.Single(pd.properties); + Assert.Equal( + JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string" }""")), + JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); + } + + [Fact] + public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescriptions() + { + // Arrange + OpenAIFunction f = KernelFunctionFactory.CreateFromMethod( + () => { }, + parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToOpenAIFunction(); + + // Act + ChatTool result = f.ToFunctionDefinition(); + ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; + + // Assert + Assert.NotNull(pd.properties); + Assert.Single(pd.properties); + Assert.Equal( + JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")), + JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); + } + +#pragma warning disable CA1812 // uninstantiated internal class + private sealed class ParametersData + { + public string? type { get; set; } + public string[]? required { get; set; } + public Dictionary? properties { get; set; } + } +#pragma warning restore CA1812 +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs new file mode 100644 index 000000000000..0c3f6bfa2c4b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs @@ -0,0 +1,82 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIFunctionToolCallTests +{ + [Theory] + [InlineData("MyFunction", "MyFunction")] + [InlineData("MyPlugin_MyFunction", "MyPlugin_MyFunction")] + public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) + { + // Arrange + var toolCall = ChatToolCall.CreateFunctionToolCall("id", toolCallName, string.Empty); + var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal(expectedName, openAIFunctionToolCall.FullyQualifiedName); + Assert.Same(openAIFunctionToolCall.FullyQualifiedName, openAIFunctionToolCall.FullyQualifiedName); + } + + [Fact] + public void ToStringReturnsCorrectValue() + { + // Arrange + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); + var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); + + // Act & Assert + Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", openAIFunctionToolCall.ToString()); + } + + [Fact] + public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary(); + var functionNamesByIndex = new Dictionary(); + var functionArgumentBuildersByIndex = new Dictionary(); + + // Act + var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Empty(toolCalls); + } + + [Fact] + public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() + { + // Arrange + var toolCallIdsByIndex = new Dictionary { { 3, "test-id" } }; + var functionNamesByIndex = new Dictionary { { 3, "test-function" } }; + var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; + + // Act + var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( + ref toolCallIdsByIndex, + ref functionNamesByIndex, + ref functionArgumentBuildersByIndex); + + // Assert + Assert.Single(toolCalls); + + var toolCall = toolCalls[0]; + + Assert.Equal("test-id", toolCall.Id); + Assert.Equal("test-function", toolCall.FunctionName); + Assert.Equal("test-argument", toolCall.FunctionArguments); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs new file mode 100644 index 000000000000..0b005900a53b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.IO; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions + +/// +/// Unit tests for class. +/// +public sealed class OpenAIStreamingChatMessageContentTests +{ + [Fact] + public async Task ConstructorWithStreamingUpdateAsync() + { + // Arrange + using var stream = File.OpenRead("TestData/chat_completion_streaming_test_response.txt"); + + using var messageHandlerStub = new HttpMessageHandlerStub(); + messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + using var httpClient = new HttpClient(messageHandlerStub); + var openAIClient = new OpenAIClient("key", new() { Transport = new HttpClientPipelineTransport(httpClient) }); + + // Act & Assert + var enumerator = openAIClient.GetChatClient("modelId").CompleteChatStreamingAsync("Test message").GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + var update = enumerator.Current; + + // Act + var content = new OpenAIStreamingChatMessageContent(update!, 0, "model-id"); + + // Assert + Assert.Equal("Test chat streaming response", content.Content); + } + + [Fact] + public void ConstructorWithParameters() + { + // Act + var content = new OpenAIStreamingChatMessageContent( + authorRole: AuthorRole.User, + content: "test message", + choiceIndex: 0, + modelId: "testModel", + toolCallUpdates: [], + metadata: new Dictionary() { ["test-index"] = "test-value" }); + + // Assert + Assert.Equal("test message", content.Content); + Assert.Equal(AuthorRole.User, content.Role); + Assert.Equal(0, content.ChoiceIndex); + Assert.Equal("testModel", content.ModelId); + Assert.Empty(content.ToolCallUpdates!); + Assert.Equal("test-value", content.Metadata!["test-index"]); + Assert.Equal(Encoding.UTF8, content.Encoding); + } + + [Fact] + public void ToStringReturnsAsExpected() + { + // Act + var content = new OpenAIStreamingChatMessageContent( + authorRole: AuthorRole.User, + content: "test message", + choiceIndex: 0, + modelId: "testModel", + toolCallUpdates: [], + metadata: new Dictionary() { ["test-index"] = "test-value" }); + + // Assert + Assert.Equal("test message", content.ToString()); + } + + [Fact] + public void ToByteArrayReturnsAsExpected() + { + // Act + var content = new OpenAIStreamingChatMessageContent( + authorRole: AuthorRole.User, + content: "test message", + choiceIndex: 0, + modelId: "testModel", + toolCallUpdates: [], + metadata: new Dictionary() { ["test-index"] = "test-value" }); + + // Assert + Assert.Equal("test message", Encoding.UTF8.GetString(content.ToByteArray())); + } + + /* + [Theory] + [MemberData(nameof(InvalidChoices))] + public void ConstructorWithInvalidChoiceSetsNullContent(object choice) + { + // Arrange + var streamingChoice = choice as ChatWithDataStreamingChoice; + + // Act + var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id"); + + // Assert + Assert.Null(content.Content); + } + + public static IEnumerable ValidChoices + { + get + { + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 1" } }] }, "Content 1" }; + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 2", Role = "Assistant" } }] }, "Content 2" }; + } + } + + public static IEnumerable InvalidChoices + { + get + { + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { EndTurn = true }] } }; + yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content", Role = "tool" } }] } }; + } + }*/ +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs new file mode 100644 index 000000000000..1010adbab869 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; +public class ChatHistoryExtensionsTests +{ + [Fact] + public async Task ItCanAddMessageFromStreamingChatContentsAsync() + { + var metadata = new Dictionary() + { + { "message", "something" }, + }; + + var chatHistoryStreamingContents = new List + { + new(AuthorRole.User, "Hello ", metadata: metadata), + new(null, ", ", metadata: metadata), + new(null, "I ", metadata: metadata), + new(null, "am ", metadata : metadata), + new(null, "a ", metadata : metadata), + new(null, "test ", metadata : metadata), + }.ToAsyncEnumerable(); + + var chatHistory = new ChatHistory(); + var finalContent = "Hello , I am a test "; + string processedContent = string.Empty; + await foreach (var chatMessageChunk in chatHistory.AddStreamingMessageAsync(chatHistoryStreamingContents)) + { + processedContent += chatMessageChunk.Content; + } + + Assert.Single(chatHistory); + Assert.Equal(finalContent, processedContent); + Assert.Equal(finalContent, chatHistory[0].Content); + Assert.Equal(AuthorRole.User, chatHistory[0].Role); + Assert.Equal(metadata["message"], chatHistory[0].Metadata!["message"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs index 6068dbe558da..869e82362282 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -1,10 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextGeneration; using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -144,4 +147,45 @@ public void ItCanAddFileService() var service = sut.AddOpenAIFiles("key").Build() .GetRequiredService(); } + + #region Chat completion + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.OpenAIClientInline)] + [InlineData(InitializationType.OpenAIClientInServiceProvider)] + public void KernelBuilderAddOpenAIChatCompletionAddsValidService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + builder = type switch + { + InitializationType.ApiKey => builder.AddOpenAIChatCompletion("model-id", "api-key"), + InitializationType.OpenAIClientInline => builder.AddOpenAIChatCompletion("model-id", client), + InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAIChatCompletion("model-id"), + _ => builder + }; + + // Assert + var chatCompletionService = builder.Build().GetRequiredService(); + Assert.True(chatCompletionService is OpenAIChatCompletionService); + + var textGenerationService = builder.Build().GetRequiredService(); + Assert.True(textGenerationService is OpenAIChatCompletionService); + } + + #endregion + + public enum InitializationType + { + ApiKey, + OpenAIClientInline, + OpenAIClientInServiceProvider, + OpenAIClientEndpoint, + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs new file mode 100644 index 000000000000..e817d559aeaa --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ComponentModel; +using System.Linq; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +#pragma warning disable CA1812 // Uninstantiated internal types + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; + +public sealed class KernelFunctionMetadataExtensionsTests +{ + [Fact] + public void ItCanConvertToAzureOpenAIFunctionNoParameters() + { + // Arrange + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToOpenAIFunction(); + + // Assert + Assert.Equal(sut.Name, result.FunctionName); + Assert.Equal(sut.PluginName, result.PluginName); + Assert.Equal(sut.Description, result.Description); + Assert.Equal($"{sut.PluginName}-{sut.Name}", result.FullyQualifiedName); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionNoPluginName() + { + // Arrange + var sut = new KernelFunctionMetadata("foo") + { + PluginName = string.Empty, + Description = "baz", + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToOpenAIFunction(); + + // Assert + Assert.Equal(sut.Name, result.FunctionName); + Assert.Equal(sut.PluginName, result.PluginName); + Assert.Equal(sut.Description, result.Description); + Assert.Equal(sut.Name, result.FullyQualifiedName); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ItCanConvertToAzureOpenAIFunctionWithParameter(bool withSchema) + { + // Arrange + var param1 = new KernelParameterMetadata("param1") + { + Description = "This is param1", + DefaultValue = "1", + ParameterType = typeof(int), + IsRequired = false, + Schema = withSchema ? KernelJsonSchema.Parse("""{"type":"integer"}""") : null, + }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal("This is param1 (default value: 1)", outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + Assert.NotNull(outputParam.Schema); + Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionWithParameterNoType() + { + // Arrange + var param1 = new KernelParameterMetadata("param1") { Description = "This is param1" }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + ReturnParameter = new KernelReturnParameterMetadata + { + Description = "retDesc", + Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), + } + }; + + // Act + var result = sut.ToOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal(param1.Description, outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + + Assert.NotNull(result.ReturnParameter); + Assert.Equal("retDesc", result.ReturnParameter.Description); + Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); + Assert.Null(result.ReturnParameter.ParameterType); + } + + [Fact] + public void ItCanConvertToAzureOpenAIFunctionWithNoReturnParameterType() + { + // Arrange + var param1 = new KernelParameterMetadata("param1") + { + Description = "This is param1", + ParameterType = typeof(int), + }; + + var sut = new KernelFunctionMetadata("foo") + { + PluginName = "bar", + Description = "baz", + Parameters = [param1], + }; + + // Act + var result = sut.ToOpenAIFunction(); + var outputParam = result.Parameters![0]; + + // Assert + Assert.Equal(param1.Name, outputParam.Name); + Assert.Equal(param1.Description, outputParam.Description); + Assert.Equal(param1.IsRequired, outputParam.IsRequired); + Assert.NotNull(outputParam.Schema); + Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); + } + + [Fact] + public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() + { + // Arrange + var kernel = new Kernel(); + kernel.Plugins.AddFromType("MyPlugin"); + + var functionMetadata = kernel.Plugins["MyPlugin"].First().Metadata; + + var sut = functionMetadata.ToOpenAIFunction(); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Equal( + """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", + result.FunctionParameters.ToString() + ); + } + + [Fact] + public void ItCanCreateValidAzureOpenAIFunctionManualForPrompt() + { + // Arrange + var promptTemplateConfig = new PromptTemplateConfig("Hello AI") + { + Description = "My sample function." + }; + promptTemplateConfig.InputVariables.Add(new InputVariable + { + Name = "parameter1", + Description = "String parameter", + JsonSchema = """{"type":"string","description":"String parameter"}""" + }); + promptTemplateConfig.InputVariables.Add(new InputVariable + { + Name = "parameter2", + Description = "Enum parameter", + JsonSchema = """{"enum":["Value1","Value2"],"description":"Enum parameter"}""" + }); + var function = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig); + var functionMetadata = function.Metadata; + var sut = functionMetadata.ToOpenAIFunction(); + + // Act + var result = sut.ToFunctionDefinition(); + + // Assert + Assert.NotNull(result); + Assert.Equal( + """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", + result.FunctionParameters.ToString() + ); + } + + private enum MyEnum + { + Value1, + Value2 + } + + private sealed class MyPlugin + { + [KernelFunction, Description("My sample function.")] + public string MyFunction( + [Description("String parameter")] string parameter1, + [Description("Enum parameter")] MyEnum parameter2, + [Description("DateTime parameter")] DateTime parameter3 + ) + { + return "return"; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs new file mode 100644 index 000000000000..d46884600d8e --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft. All rights reserved. + +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Core; + +/// +/// Unit tests for class. +/// +public sealed class OpenAIPluginCollectionExtensionsTests +{ + [Fact] + public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() + { + // Arrange + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); + var plugins = new KernelPluginCollection([plugin]); + + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); + + // Act + var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.False(result); + Assert.Null(actualFunction); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); + + // Act + var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Equal(function.Name, actualFunction?.Name); + Assert.Null(actualArguments); + } + + [Fact] + public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() + { + // Arrange + var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + + var plugins = new KernelPluginCollection([plugin]); + var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); + + // Act + var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + + // Assert + Assert.True(result); + Assert.Equal(function.Name, actualFunction?.Name); + + Assert.NotNull(actualArguments); + + Assert.Equal("San Diego", actualArguments["location"]); + Assert.Equal("300", actualArguments["max_price"]); + + Assert.Null(actualArguments["null_argument"]); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 19c030b820fb..3e7767d33e24 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -3,9 +3,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Services; +using Microsoft.SemanticKernel.TextGeneration; using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -15,6 +17,39 @@ namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; public class ServiceCollectionExtensionsTests { + #region Chat completion + + [Theory] + [InlineData(InitializationType.ApiKey)] + [InlineData(InitializationType.ClientInline)] + [InlineData(InitializationType.ClientInServiceProvider)] + public void ItCanAddChatCompletionService(InitializationType type) + { + // Arrange + var client = new OpenAIClient("key"); + var builder = Kernel.CreateBuilder(); + + builder.Services.AddSingleton(client); + + // Act + IServiceCollection collection = type switch + { + InitializationType.ApiKey => builder.Services.AddOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), + InitializationType.ClientInline => builder.Services.AddOpenAIChatCompletion("deployment-name", client), + InitializationType.ClientInServiceProvider => builder.Services.AddOpenAIChatCompletion("deployment-name"), + _ => builder.Services + }; + + // Assert + var chatCompletionService = builder.Build().GetRequiredService(); + Assert.True(chatCompletionService is OpenAIChatCompletionService); + + var textGenerationService = builder.Build().GetRequiredService(); + Assert.True(textGenerationService is OpenAIChatCompletionService); + } + + #endregion + [Fact] public void ItCanAddTextEmbeddingGenerationService() { @@ -146,4 +181,12 @@ public void ItCanAddFileService() .BuildServiceProvider() .GetRequiredService(); } + + public enum InitializationType + { + ApiKey, + ClientInline, + ClientInServiceProvider, + ClientEndpoint, + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs new file mode 100644 index 000000000000..26dd596fa49b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Models; + +public sealed class OpenAIFileReferenceTests +{ + [Fact] + public void CanBeInstantiated() + { + // Arrange + var fileReference = new OpenAIFileReference + { + CreatedTimestamp = DateTime.UtcNow, + FileName = "test.txt", + Id = "123", + Purpose = OpenAIFilePurpose.Assistants, + SizeInBytes = 100 + }; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs index 5627803bfab1..65be0cd4f384 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -43,6 +43,7 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) // Assert Assert.NotNull(service); Assert.Equal("model-id", service.Attributes["ModelId"]); + Assert.Equal("Organization", OpenAIAudioToTextService.OrganizationKey); } [Fact] @@ -128,6 +129,26 @@ public async Task GetTextContentByDefaultWorksCorrectlyAsync() Assert.Equal("Test audio-to-text response", result[0].Text); } + [Fact] + public async Task GetTextContentThrowsIfAudioCantBeReadAsync() + { + // Arrange + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => { await service.GetTextContentsAsync(new AudioContent(new Uri("http://remote-audio")), new OpenAIAudioToTextExecutionSettings("file.mp3")); }); + } + + [Fact] + public async Task GetTextContentThrowsIfFileNameIsInvalidAsync() + { + // Arrange + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + + // Act & Assert + await Assert.ThrowsAsync(async () => { await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("invalid")); }); + } + [Fact] public async Task GetTextContentsDoesLogActionAsync() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs new file mode 100644 index 000000000000..e10bbd941b38 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -0,0 +1,1050 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Diagnostics; +using Microsoft.SemanticKernel.TextGeneration; +using Moq; +using OpenAI; +using OpenAI.Chat; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; + +/// +/// Unit tests for +/// +public sealed class OpenAIChatCompletionServiceTests : IDisposable +{ + private readonly HttpMessageHandlerStub _messageHandlerStub; + private readonly MultipleHttpMessageHandlerStub _multiMessageHandlerStub; + private readonly HttpClient _httpClient; + private readonly OpenAIFunction _timepluginDate, _timepluginNow; + private readonly OpenAIPromptExecutionSettings _executionSettings; + private readonly Mock _mockLoggerFactory; + private readonly ChatHistory _chatHistoryForTest = [new ChatMessageContent(AuthorRole.User, "test")]; + + public OpenAIChatCompletionServiceTests() + { + this._messageHandlerStub = new HttpMessageHandlerStub(); + this._multiMessageHandlerStub = new MultipleHttpMessageHandlerStub(); + this._httpClient = new HttpClient(this._messageHandlerStub, false); + this._mockLoggerFactory = new Mock(); + + IList functions = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[] + { + KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.Date.ToString(format, CultureInfo.InvariantCulture), "Date", "TimePlugin.Date"), + KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.ToString(format, CultureInfo.InvariantCulture), "Now", "TimePlugin.Now"), + }).GetFunctionsMetadata(); + + this._timepluginDate = functions[0].ToOpenAIFunction(); + this._timepluginNow = functions[1].ToOpenAIFunction(); + + this._executionSettings = new() + { + ToolCallBehavior = ToolCallBehavior.EnableFunctions([this._timepluginDate, this._timepluginNow]) + }; + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var service = includeLoggerFactory ? + new OpenAIChatCompletionService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIChatCompletionService("model-id", "api-key", "organization"); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Theory] + [InlineData("http://localhost:1234/chat/completions", "http://localhost:1234/chat/completions")] // Uses full path when provided + [InlineData("http://localhost:1234/v2/chat/completions", "http://localhost:1234/v2/chat/completions")] // Uses full path when provided + [InlineData("http://localhost:1234", "http://localhost:1234/v1/chat/completions")] + [InlineData("http://localhost:8080", "http://localhost:8080/v1/chat/completions")] + [InlineData("https://something:8080", "https://something:8080/v1/chat/completions")] // Accepts TLS Secured endpoints + [InlineData("http://localhost:1234/v2", "http://localhost:1234/v2/chat/completions")] + [InlineData("http://localhost:8080/v2", "http://localhost:8080/v2/chat/completions")] + public async Task ItUsesCustomEndpointsWhenProvidedDirectlyAsync(string endpointProvided, string expectedEndpoint) + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: null, httpClient: this._httpClient, endpoint: new Uri(endpointProvided)); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + Assert.Equal(expectedEndpoint, this._messageHandlerStub.RequestUri!.ToString()); + } + + [Theory] + [InlineData("http://localhost:1234/chat/completions", "http://localhost:1234/chat/completions")] // Uses full path when provided + [InlineData("http://localhost:1234/v2/chat/completions", "http://localhost:1234/v2/chat/completions")] // Uses full path when provided + [InlineData("http://localhost:1234", "http://localhost:1234/v1/chat/completions")] + [InlineData("http://localhost:8080", "http://localhost:8080/v1/chat/completions")] + [InlineData("https://something:8080", "https://something:8080/v1/chat/completions")] // Accepts TLS Secured endpoints + [InlineData("http://localhost:1234/v2", "http://localhost:1234/v2/chat/completions")] + [InlineData("http://localhost:8080/v2", "http://localhost:8080/v2/chat/completions")] + public async Task ItUsesCustomEndpointsWhenProvidedAsBaseAddressAsync(string endpointProvided, string expectedEndpoint) + { + // Arrange + this._httpClient.BaseAddress = new Uri(endpointProvided); + var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: null, httpClient: this._httpClient, endpoint: new Uri(endpointProvided)); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + Assert.Equal(expectedEndpoint, this._messageHandlerStub.RequestUri!.ToString()); + } + + [Fact] + public async Task ItUsesHttpClientEndpointIfProvidedEndpointIsMissingAsync() + { + // Arrange + this._httpClient.BaseAddress = new Uri("http://localhost:12312"); + var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: null, httpClient: this._httpClient, endpoint: null!); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + Assert.Equal("http://localhost:12312/v1/chat/completions", this._messageHandlerStub.RequestUri!.ToString()); + } + + [Fact] + public async Task ItUsesDefaultEndpointIfProvidedEndpointIsMissingAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: "abc", httpClient: this._httpClient, endpoint: null!); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + Assert.Equal("https://api.openai.com/v1/chat/completions", this._messageHandlerStub.RequestUri!.ToString()); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) + { + // Arrange & Act + var client = new OpenAIClient("key"); + var service = includeLoggerFactory ? + new OpenAIChatCompletionService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : + new OpenAIChatCompletionService("model-id", client); + + // Assert + Assert.NotNull(service); + Assert.Equal("model-id", service.Attributes["ModelId"]); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + // Act + await chatCompletion.GetChatMessageContentsAsync([new ChatMessageContent(AuthorRole.User, "test")], this._executionSettings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); + } + + [Fact] + public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNowAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + this._executionSettings.ToolCallBehavior = ToolCallBehavior.RequireFunction(this._timepluginNow); + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); + } + + [Fact] + public async Task ItCreatesNoFunctionsWhenUsingNoneAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + this._executionSettings.ToolCallBehavior = null; + + // Act + await chatCompletion.GetChatMessageContentsAsync(this._chatHistoryForTest, this._executionSettings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.False(optionsJson.TryGetProperty("functions", out var _)); + } + + [Fact] + public async Task ItAddsIdToChatMessageAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + var chatHistory = new ChatHistory(); + chatHistory.AddMessage(AuthorRole.Tool, "Hello", metadata: new Dictionary() { { OpenAIChatMessageContent.ToolIdProperty, "John Doe" } }); + + // Act + await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + Assert.Equal(1, optionsJson.GetProperty("messages").GetArrayLength()); + Assert.Equal("John Doe", optionsJson.GetProperty("messages")[0].GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task ItGetChatMessageContentsShouldHaveModelIdDefinedAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse, Encoding.UTF8, "application/json") }; + + var chatHistory = new ChatHistory(); + chatHistory.AddMessage(AuthorRole.User, "Hello"); + + // Act + var chatMessage = await chatCompletion.GetChatMessageContentAsync(chatHistory, this._executionSettings); + + // Assert + Assert.NotNull(chatMessage.ModelId); + Assert.Equal("gpt-3.5-turbo", chatMessage.ModelId); + } + + [Fact] + public async Task ItGetTextContentsShouldHaveModelIdDefinedAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse, Encoding.UTF8, "application/json") }; + + var chatHistory = new ChatHistory(); + chatHistory.AddMessage(AuthorRole.User, "Hello"); + + // Act + var textContent = await chatCompletion.GetTextContentAsync("hello", this._executionSettings); + + // Assert + Assert.NotNull(textContent.ModelId); + Assert.Equal("gpt-3.5-turbo", textContent.ModelId); + } + + [Fact] + public async Task GetStreamingTextContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); + using var stream = File.OpenRead("TestData/chat_completion_streaming_test_response.txt"); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + var enumerator = service.GetStreamingTextContentsAsync("Prompt").GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Text); + + await enumerator.MoveNextAsync(); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() + { + // Arrange + var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); + using var stream = File.OpenRead("TestData/chat_completion_streaming_test_response.txt"); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(stream) + }; + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([]).GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + + await enumerator.MoveNextAsync(); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); + } + + [Fact] + public async Task ItAddsSystemMessageAsync() + { + // Arrange + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + var chatHistory = new ChatHistory(); + chatHistory.AddMessage(AuthorRole.User, "Hello"); + + // Act + await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(1, messages.GetArrayLength()); + + Assert.Equal("Hello", messages[0].GetProperty("content").GetString()); + Assert.Equal("user", messages[0].GetProperty("role").GetString()); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function1 = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => + { + functionCallCount++; + throw new ArgumentException("Some exception"); + }, "FunctionWithException"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); + + using var multiHttpClient = new HttpClient(this._multiMessageHandlerStub, false); + var service = new OpenAIChatCompletionService("model-id", "api-key", "organization-id", multiHttpClient, this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_multiple_function_calls_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) }; + + this._multiMessageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); + + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); + + await enumerator.MoveNextAsync(); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); + + // Keep looping until the end of stream + while (await enumerator.MoveNextAsync()) + { + } + + Assert.Equal(2, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() + { + // Arrange + const int DefaultMaximumAutoInvokeAttempts = 128; + const int ModelResponsesCount = 129; + + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); + using var multiHttpClient = new HttpClient(this._multiMessageHandlerStub, false); + var service = new OpenAIChatCompletionService("model-id", "api-key", httpClient: multiHttpClient, loggerFactory: this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var responses = new List(); + + for (var i = 0; i < ModelResponsesCount; i++) + { + responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_single_function_call_test_response.txt")) }); + } + + this._multiMessageHandlerStub.ResponsesToReturn = responses; + + // Act & Assert + await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) + { + Assert.Equal("Test chat streaming response", chunk.Content); + } + + Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); + } + + [Fact] + public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() + { + // Arrange + int functionCallCount = 0; + + var kernel = Kernel.CreateBuilder().Build(); + var function = KernelFunctionFactory.CreateFromMethod((string location) => + { + functionCallCount++; + return "Some weather"; + }, "GetCurrentWeather"); + + var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); + + kernel.Plugins.Add(plugin); + using var multiHttpClient = new HttpClient(this._multiMessageHandlerStub, false); + var service = new OpenAIChatCompletionService("model-id", "api-key", httpClient: multiHttpClient, loggerFactory: this._mockLoggerFactory.Object); + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + + using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_single_function_call_test_response.txt")) }; + using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) }; + + this._multiMessageHandlerStub.ResponsesToReturn = [response1, response2]; + + // Act & Assert + var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); + + // Function Tool Call Streaming (One Chunk) + await enumerator.MoveNextAsync(); + Assert.Equal("Test chat streaming response", enumerator.Current.Content); + Assert.Equal("ToolCalls", enumerator.Current.Metadata?["FinishReason"]); + + // Chat Completion Streaming (1st Chunk) + await enumerator.MoveNextAsync(); + Assert.Null(enumerator.Current.Metadata?["FinishReason"]); + + // Chat Completion Streaming (2nd Chunk) + await enumerator.MoveNextAsync(); + Assert.Equal("Stop", enumerator.Current.Metadata?["FinishReason"]); + + Assert.Equal(1, functionCallCount); + + var requestContents = this._multiMessageHandlerStub.RequestContents; + + Assert.Equal(2, requestContents.Count); + + requestContents.ForEach(Assert.NotNull); + + var firstContent = Encoding.UTF8.GetString(requestContents[0]!); + var secondContent = Encoding.UTF8.GetString(requestContents[1]!); + + var firstContentJson = JsonSerializer.Deserialize(firstContent); + var secondContentJson = JsonSerializer.Deserialize(secondContent); + + Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); + Assert.Equal("MyPlugin-GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); + + Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); + } + + [Fact] + public async Task GetChatMessageContentsUsesPromptAndSettingsCorrectlyAsync() + { + // Arrange + const string Prompt = "This is test prompt"; + const string SystemMessage = "This is test system message"; + + var service = new OpenAIChatCompletionService("model-id", "api-key", httpClient: this._httpClient); + var settings = new OpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) + }; + + IKernelBuilder builder = Kernel.CreateBuilder(); + builder.Services.AddTransient((sp) => service); + Kernel kernel = builder.Build(); + + // Act + var result = await kernel.InvokePromptAsync(Prompt, new(settings)); + + // Assert + Assert.Equal("Test chat response", result.ToString()); + + var requestContentByteArray = this._messageHandlerStub.RequestContent; + + Assert.NotNull(requestContentByteArray); + + var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray)); + + var messages = requestContent.GetProperty("messages"); + + Assert.Equal(2, messages.GetArrayLength()); + + Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); + Assert.Equal("system", messages[0].GetProperty("role").GetString()); + + Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); + Assert.Equal("user", messages[1].GetProperty("role").GetString()); + } + + [Fact] + public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync() + { + // Arrange + const string Prompt = "This is test prompt"; + const string SystemMessage = "This is test system message"; + const string AssistantMessage = "This is assistant message"; + const string CollectionItemPrompt = "This is collection item prompt"; + + var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + var settings = new OpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent(ChatCompletionResponse) }; + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage(Prompt); + chatHistory.AddAssistantMessage(AssistantMessage); + chatHistory.AddUserMessage( + [ + new TextContent(CollectionItemPrompt), + new ImageContent(new Uri("https://image")) + ]); + + // Act + await chatCompletion.GetChatMessageContentsAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + + Assert.Equal(4, messages.GetArrayLength()); + + Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); + Assert.Equal("system", messages[0].GetProperty("role").GetString()); + + Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); + Assert.Equal("user", messages[1].GetProperty("role").GetString()); + + Assert.Equal(AssistantMessage, messages[2].GetProperty("content").GetString()); + Assert.Equal("assistant", messages[2].GetProperty("role").GetString()); + + var contentItems = messages[3].GetProperty("content"); + Assert.Equal(2, contentItems.GetArrayLength()); + Assert.Equal(CollectionItemPrompt, contentItems[0].GetProperty("text").GetString()); + Assert.Equal("text", contentItems[0].GetProperty("type").GetString()); + Assert.Equal("https://image/", contentItems[1].GetProperty("image_url").GetProperty("url").GetString()); + Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); + } + + [Fact] + public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("TestData/chat_completion_multiple_function_calls_test_response.json")) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Fake prompt"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + Assert.NotNull(result); + Assert.Equal(5, result.Items.Count); + + var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; + Assert.NotNull(getCurrentWeatherFunctionCall); + Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); + Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); + Assert.Equal("1", getCurrentWeatherFunctionCall.Id); + Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); + + var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; + Assert.NotNull(functionWithExceptionFunctionCall); + Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); + Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); + Assert.Equal("2", functionWithExceptionFunctionCall.Id); + Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); + + var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; + Assert.NotNull(nonExistentFunctionCall); + Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); + Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); + Assert.Equal("3", nonExistentFunctionCall.Id); + Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); + + var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; + Assert.NotNull(invalidArgumentsFunctionCall); + Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); + Assert.Equal("4", invalidArgumentsFunctionCall.Id); + Assert.Null(invalidArgumentsFunctionCall.Arguments); + Assert.NotNull(invalidArgumentsFunctionCall.Exception); + Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); + Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); + + var intArgumentsFunctionCall = result.Items[4] as FunctionCallContent; + Assert.NotNull(intArgumentsFunctionCall); + Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); + Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); + Assert.Equal("5", intArgumentsFunctionCall.Id); + Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); + } + + [Fact] + public async Task FunctionCallsShouldBeReturnedToLLMAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var items = new ChatMessageContentItemCollection + { + new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), + new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) + }; + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Assistant, items) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(1, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); + + Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); + + var tool1 = assistantMessage.GetProperty("tool_calls")[0]; + Assert.Equal("1", tool1.GetProperty("id").GetString()); + Assert.Equal("function", tool1.GetProperty("type").GetString()); + + var function1 = tool1.GetProperty("function"); + Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); + + var tool2 = assistantMessage.GetProperty("tool_calls")[1]; + Assert.Equal("2", tool2.GetProperty("id").GetString()); + Assert.Equal("function", tool2.GetProperty("type").GetString()); + + var function2 = tool2.GetProperty("function"); + Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); + Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + ]), + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + ]) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[1]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() + { + // Arrange + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { + Content = new StringContent(ChatCompletionResponse) + }; + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.Tool, + [ + new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), + new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") + ]) + }; + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(2, messages.GetArrayLength()); + + var assistantMessage = messages[0]; + Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); + Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); + Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); + + var assistantMessage2 = messages[1]; + Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); + Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); + Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); + } + + [Fact] + public async Task GetAllContentsDoesLogActionAsync() + { + // Assert + var modelId = "gpt-4o"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + // Arrange + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => { await sut.GetChatMessageContentsAsync(this._chatHistoryForTest); }); + await Assert.ThrowsAnyAsync(async () => { await sut.GetStreamingChatMessageContentsAsync(this._chatHistoryForTest).GetAsyncEnumerator().MoveNextAsync(); }); + await Assert.ThrowsAnyAsync(async () => { await sut.GetTextContentsAsync("test"); }); + await Assert.ThrowsAnyAsync(async () => { await sut.GetStreamingTextContentsAsync("test").GetAsyncEnumerator().MoveNextAsync(); }); + + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + [Theory] + [InlineData("string", "json_object")] + [InlineData("string", "text")] + [InlineData("string", "random")] + [InlineData("JsonElement.String", "\"json_object\"")] + [InlineData("JsonElement.String", "\"text\"")] + [InlineData("JsonElement.String", "\"random\"")] + [InlineData("ChatResponseFormat", "json_object")] + [InlineData("ChatResponseFormat", "text")] + public async Task GetChatMessageInResponseFormatsAsync(string formatType, string formatValue) + { + // Assert + object? format = null; + switch (formatType) + { + case "string": + format = formatValue; + break; + case "JsonElement.String": + format = JsonSerializer.Deserialize(formatValue); + break; + case "ChatResponseFormat": + format = formatValue == "text" ? ChatResponseFormat.Text : ChatResponseFormat.JsonObject; + break; + } + + var modelId = "gpt-4o"; + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient); + OpenAIPromptExecutionSettings executionSettings = new() { ResponseFormat = format }; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) + }; + + // Act + var result = await sut.GetChatMessageContentAsync(this._chatHistoryForTest, executionSettings); + + // Assert + Assert.NotNull(result); + } + + [Fact] + public async Task GetChatMessageContentsLogsAsExpected() + { + // Assert + var modelId = "gpt-4o"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) + }; + + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetChatMessageContentsAsync(this._chatHistoryForTest); + + // Arrange + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + [Fact] + public async Task GetStreamingChatMessageContentsLogsAsExpected() + { + // Assert + var modelId = "gpt-4o"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) + }; + + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetStreamingChatMessageContentsAsync(this._chatHistoryForTest).GetAsyncEnumerator().MoveNextAsync(); + + // Arrange + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + [Fact] + public async Task GetTextContentsLogsAsExpected() + { + // Assert + var modelId = "gpt-4o"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) + }; + + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetTextContentAsync("test"); + + // Arrange + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + [Fact] + public async Task GetStreamingTextContentsLogsAsExpected() + { + // Assert + var modelId = "gpt-4o"; + var logger = new Mock>(); + logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); + + this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) + }; + + var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + + // Act + await sut.GetStreamingTextContentsAsync("test").GetAsyncEnumerator().MoveNextAsync(); + + // Arrange + logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); + } + + [Fact(Skip = "Not working running in the console")] + public async Task GetInvalidResponseThrowsExceptionAndIsCapturedByDiagnosticsAsync() + { + // Arrange + bool startedChatCompletionsActivity = false; + + this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) + { Content = new StringContent("Invalid JSON") }; + + var sut = new OpenAIChatCompletionService("model-id", "api-key", httpClient: this._httpClient); + + // Enable ModelDiagnostics + using var listener = new ActivityListener() + { + ShouldListenTo = (activitySource) => true, //activitySource.Name == typeof(ModelDiagnostics).Namespace!, + ActivityStarted = (activity) => + { + if (activity.OperationName == "chat.completions model-id") + { + startedChatCompletionsActivity = true; + } + }, + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllData, + }; + + ActivitySource.AddActivityListener(listener); + + Environment.SetEnvironmentVariable("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS", "true"); + Environment.SetEnvironmentVariable("SEMANTICKERNEL_EXPERIMENTAL_GENAI_ENABLE_OTEL_DIAGNOSTICS_SENSITIVE", "true"); + + // Act & Assert + await Assert.ThrowsAnyAsync(async () => { await sut.GetChatMessageContentsAsync(this._chatHistoryForTest); }); + + Assert.True(ModelDiagnostics.HasListeners()); + Assert.True(ModelDiagnostics.IsSensitiveEventsEnabled()); + Assert.True(ModelDiagnostics.IsModelDiagnosticsEnabled()); + Assert.True(startedChatCompletionsActivity); + } + + public void Dispose() + { + this._httpClient.Dispose(); + this._messageHandlerStub.Dispose(); + this._multiMessageHandlerStub.Dispose(); + } + + private const string ChatCompletionResponse = """ + { + "id": "chatcmpl-8IlRBQU929ym1EqAY2J4T7GGkW5Om", + "object": "chat.completion", + "created": 1699482945, + "model": "gpt-3.5-turbo", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "function_call": { + "name": "TimePlugin_Date", + "arguments": "{}" + } + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 52, + "completion_tokens": 1, + "total_tokens": 53 + } + } + """; +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs index 85ac2f2bf8d4..4cf27cd9ee2a 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs @@ -44,7 +44,7 @@ public void ConstructorWorksCorrectlyForOpenAI(bool includeLoggerFactory) [Theory] [InlineData(true)] [InlineData(false)] - public void ConstructorWorksCorrectlyForAzure(bool includeLoggerFactory) + public void ConstructorWorksCorrectlyForCustomEndpoint(bool includeLoggerFactory) { // Arrange & Act var service = includeLoggerFactory ? @@ -227,12 +227,10 @@ public async Task UploadContentWorksCorrectlyAsync(bool isCustomEndpoint) var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); - await using var stream = new MemoryStream(); - await using (var writer = new StreamWriter(stream, leaveOpen: true)) - { - await writer.WriteLineAsync("test"); - await writer.FlushAsync(); - } + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + await writer.WriteLineAsync("test"); + await writer.FlushAsync(); stream.Position = 0; @@ -245,6 +243,9 @@ public async Task UploadContentWorksCorrectlyAsync(bool isCustomEndpoint) Assert.NotEqual(string.Empty, file.FileName); Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); Assert.NotEqual(0, file.SizeInBytes); + + writer.Dispose(); + stream.Dispose(); } [Theory] @@ -260,12 +261,10 @@ public async Task UploadContentFailsAsExpectedAsync(bool isCustomEndpoint) var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); - await using var stream = new MemoryStream(); - await using (var writer = new StreamWriter(stream, leaveOpen: true)) - { - await writer.WriteLineAsync("test"); - await writer.FlushAsync(); - } + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + await writer.WriteLineAsync("test"); + await writer.FlushAsync(); stream.Position = 0; @@ -273,6 +272,9 @@ public async Task UploadContentFailsAsExpectedAsync(bool isCustomEndpoint) // Act & Assert await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings)); + + writer.Dispose(); + stream.Dispose(); } private OpenAIFileService CreateFileService(bool isCustomEndpoint = false) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs index 9c7de44d8a83..0eca148eae8e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs @@ -43,6 +43,7 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) // Assert Assert.NotNull(service); Assert.Equal("model-id", service.Attributes["ModelId"]); + Assert.Equal("Organization", OpenAITextToAudioService.OrganizationKey); } [Fact] @@ -63,7 +64,7 @@ public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAIT { // Arrange var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); - await using var stream = new MemoryStream([0x00, 0x00, 0xFF, 0x7F]); + using var stream = new MemoryStream([0x00, 0x00, 0xFF, 0x7F]); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) { @@ -85,7 +86,7 @@ public async Task GetAudioContentByDefaultWorksCorrectlyAsync() byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); + using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) { @@ -113,7 +114,7 @@ public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); + using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) { @@ -170,7 +171,7 @@ public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAdd } var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); + using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs new file mode 100644 index 000000000000..4e272320eee3 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs @@ -0,0 +1,271 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Xunit; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Settings; + +/// +/// Unit tests of OpenAIPromptExecutionSettingsTests +/// +public class OpenAIPromptExecutionSettingsTests +{ + [Fact] + public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() + { + // Arrange + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(1, executionSettings.Temperature); + Assert.Equal(1, executionSettings.TopP); + Assert.Equal(0, executionSettings.FrequencyPenalty); + Assert.Equal(0, executionSettings.PresencePenalty); + Assert.Null(executionSettings.StopSequences); + Assert.Null(executionSettings.TokenSelectionBiases); + Assert.Null(executionSettings.TopLogprobs); + Assert.Null(executionSettings.Logprobs); + Assert.Equal(128, executionSettings.MaxTokens); + } + + [Fact] + public void ItUsesExistingOpenAIExecutionSettings() + { + // Arrange + OpenAIPromptExecutionSettings actualSettings = new() + { + Temperature = 0.7, + TopP = 0.7, + FrequencyPenalty = 0.7, + PresencePenalty = 0.7, + StopSequences = new string[] { "foo", "bar" }, + ChatSystemPrompt = "chat system prompt", + MaxTokens = 128, + Logprobs = true, + TopLogprobs = 5, + TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, + }; + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(actualSettings, executionSettings); + Assert.Equal(256, OpenAIPromptExecutionSettings.DefaultTextMaxTokens); + } + + [Fact] + public void ItCanUseOpenAIExecutionSettings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() { + { "max_tokens", 1000 }, + { "temperature", 0 } + } + }; + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + Assert.NotNull(executionSettings); + Assert.Equal(1000, executionSettings.MaxTokens); + Assert.Equal(0, executionSettings.Temperature); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", 0.7 }, + { "top_p", 0.7 }, + { "frequency_penalty", 0.7 }, + { "presence_penalty", 0.7 }, + { "results_per_prompt", 2 }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", 128 }, + { "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 }, + } + }; + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() + { + // Arrange + PromptExecutionSettings actualSettings = new() + { + ExtensionData = new Dictionary() + { + { "temperature", "0.7" }, + { "top_p", "0.7" }, + { "frequency_penalty", "0.7" }, + { "presence_penalty", "0.7" }, + { "results_per_prompt", "2" }, + { "stop_sequences", new [] { "foo", "bar" } }, + { "chat_system_prompt", "chat system prompt" }, + { "max_tokens", "128" }, + { "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } }, + { "seed", 123456 }, + { "logprobs", true }, + { "top_logprobs", 5 } + } + }; + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Fact] + public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase() + { + // Arrange + var json = """ + { + "temperature": 0.7, + "top_p": 0.7, + "frequency_penalty": 0.7, + "presence_penalty": 0.7, + "results_per_prompt": 2, + "stop_sequences": [ "foo", "bar" ], + "chat_system_prompt": "chat system prompt", + "token_selection_biases": { "1": 2, "3": 4 }, + "max_tokens": 128, + "seed": 123456, + "logprobs": true, + "top_logprobs": 5 + } + """; + var actualSettings = JsonSerializer.Deserialize(json); + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); + + // Assert + AssertExecutionSettings(executionSettings); + } + + [Theory] + [InlineData("", "")] + [InlineData("System prompt", "System prompt")] + public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) + { + // Arrange & Act + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; + + // Assert + Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); + } + + [Fact] + public void PromptExecutionSettingsCloneWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0 + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + var clone = executionSettings!.Clone(); + + // Assert + Assert.NotNull(clone); + Assert.Equal(executionSettings.ModelId, clone.ModelId); + Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); + } + + [Fact] + public void PromptExecutionSettingsFreezeWorksAsExpected() + { + // Arrange + string configPayload = """ + { + "max_tokens": 60, + "temperature": 0.5, + "top_p": 0.0, + "presence_penalty": 0.0, + "frequency_penalty": 0.0, + "stop_sequences": [ "DONE" ], + "token_selection_biases": { "1": 2, "3": 4 } + } + """; + var executionSettings = JsonSerializer.Deserialize(configPayload); + + // Act + executionSettings!.Freeze(); + + // Assert + Assert.True(executionSettings.IsFrozen); + Assert.Throws(() => executionSettings.ModelId = "gpt-4"); + Assert.Throws(() => executionSettings.Temperature = 1); + Assert.Throws(() => executionSettings.TopP = 1); + Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); + Assert.Throws(() => executionSettings.TokenSelectionBiases?.Add(5, 6)); + + executionSettings!.Freeze(); // idempotent + Assert.True(executionSettings.IsFrozen); + } + + [Fact] + public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() + { + // Arrange + PromptExecutionSettings settings = new OpenAIPromptExecutionSettings { StopSequences = [] }; + + // Act + var executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(settings); + + // Assert + Assert.NotNull(executionSettings.StopSequences); + Assert.Empty(executionSettings.StopSequences); + } + + private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings) + { + Assert.NotNull(executionSettings); + Assert.Equal(0.7, executionSettings.Temperature); + Assert.Equal(0.7, executionSettings.TopP); + Assert.Equal(0.7, executionSettings.FrequencyPenalty); + Assert.Equal(0.7, executionSettings.PresencePenalty); + Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); + Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); + Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); + Assert.Equal(128, executionSettings.MaxTokens); + Assert.Equal(123456, executionSettings.Seed); + Assert.Equal(true, executionSettings.Logprobs); + Assert.Equal(5, executionSettings.TopLogprobs); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt new file mode 100644 index 000000000000..be41c2eaf843 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt @@ -0,0 +1,5 @@ +data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{"content":"Test chat streaming response"},"logprobs":null,"finish_reason":null}]} + +data: {"id":}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json new file mode 100644 index 000000000000..737b972309ba --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json @@ -0,0 +1,64 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + }, + { + "id": "2", + "type": "function", + "function": { + "name": "MyPlugin-FunctionWithException", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "3", + "type": "function", + "function": { + "name": "MyPlugin-NonExistentFunction", + "arguments": "{\n\"argument\": \"value\"\n}" + } + }, + { + "id": "4", + "type": "function", + "function": { + "name": "MyPlugin-InvalidArguments", + "arguments": "invalid_arguments_format" + } + }, + { + "id": "5", + "type": "function", + "function": { + "name": "MyPlugin-IntArguments", + "arguments": "{\n\"age\": 36\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json new file mode 100644 index 000000000000..6c93e434f259 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json @@ -0,0 +1,32 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-GetCurrentWeather", + "arguments": "{\n\"location\": \"Boston, MA\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt new file mode 100644 index 000000000000..ceb8f3e8b44b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt @@ -0,0 +1,9 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin-FunctionWithException","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":2,"id":"3","type":"function","function":{"name":"MyPlugin-NonExistentFunction","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":3,"id":"4","type":"function","function":{"name":"MyPlugin-InvalidArguments","arguments":"invalid_arguments_format"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt new file mode 100644 index 000000000000..6835039941ce --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt @@ -0,0 +1,3 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt new file mode 100644 index 000000000000..e5e8d1b19afd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt @@ -0,0 +1,5 @@ +data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{"content":"Test chat streaming response"},"logprobs":null,"finish_reason":null}]} + +data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json new file mode 100644 index 000000000000..b601bac8b55b --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json @@ -0,0 +1,22 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1704208954, + "model": "gpt-4", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "Test chat response" + }, + "finish_reason": "stop" + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + }, + "system_fingerprint": null +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt new file mode 100644 index 000000000000..5e17403da9fc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt @@ -0,0 +1 @@ +data: {"id":"response-id","model":"","created":1684304924,"object":"chat.completion","choices":[{"index":0,"messages":[{"delta":{"role":"assistant","content":"Test chat with data streaming response"},"end_turn":false}]}]} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json new file mode 100644 index 000000000000..1d1d4e78b5bd --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json @@ -0,0 +1,28 @@ +{ + "id": "response-id", + "model": "", + "created": 1684304924, + "object": "chat.completion", + "choices": [ + { + "index": 0, + "messages": [ + { + "role": "tool", + "content": "{\"citations\": [{\"content\": \"\\OpenAI AI services are cloud-based artificial intelligence (AI) services...\", \"id\": null, \"title\": \"What is OpenAI AI services\", \"filepath\": null, \"url\": null, \"metadata\": {\"chunking\": \"original document size=250. Scores=0.4314117431640625 and 1.72564697265625.Org Highlight count=4.\"}, \"chunk_id\": \"0\"}], \"intent\": \"[\\\"Learn about OpenAI AI services.\\\"]\"}", + "end_turn": false + }, + { + "role": "assistant", + "content": "Test chat with data response", + "end_turn": true + } + ] + } + ], + "usage": { + "prompt_tokens": 55, + "completion_tokens": 100, + "total_tokens": 155 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json new file mode 100644 index 000000000000..3ffa6b00cc3f --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json @@ -0,0 +1,40 @@ +{ + "id": "response-id", + "object": "chat.completion", + "created": 1699896916, + "model": "gpt-3.5-turbo-0613", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": null, + "tool_calls": [ + { + "id": "1", + "type": "function", + "function": { + "name": "MyPlugin-Function1", + "arguments": "{\n\"parameter\": \"function1-value\"\n}" + } + }, + { + "id": "2", + "type": "function", + "function": { + "name": "MyPlugin-Function2", + "arguments": "{\n\"parameter\": \"function2-value\"\n}" + } + } + ] + }, + "logprobs": null, + "finish_reason": "tool_calls" + } + ], + "usage": { + "prompt_tokens": 82, + "completion_tokens": 17, + "total_tokens": 99 + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt new file mode 100644 index 000000000000..c8aeb98e8b82 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt @@ -0,0 +1,5 @@ +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-Function1","arguments":"{\n\"parameter\": \"function1-value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin-Function2","arguments":"{\n\"parameter\": \"function2-value\"\n}"}}]},"finish_reason":"tool_calls"}]} + +data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs new file mode 100644 index 000000000000..76b6c47360b6 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs @@ -0,0 +1,242 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using Xunit; +using static Microsoft.SemanticKernel.Connectors.OpenAI.ToolCallBehavior; + +namespace SemanticKernel.Connectors.OpenAI.UnitTests; + +/// +/// Unit tests for +/// +public sealed class ToolCallBehaviorTests +{ + [Fact] + public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + var behavior = ToolCallBehavior.EnableKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); + Assert.Equal($"{nameof(KernelFunctions)}(autoInvoke:{behavior.MaximumAutoInvokeAttempts != 0})", behavior.ToString()); + } + + [Fact] + public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() + { + // Arrange & Act + const int DefaultMaximumAutoInvokeAttempts = 128; + var behavior = ToolCallBehavior.AutoInvokeKernelFunctions; + + // Assert + Assert.IsType(behavior); + Assert.Equal(DefaultMaximumAutoInvokeAttempts, behavior.MaximumAutoInvokeAttempts); + } + + [Fact] + public void EnableFunctionsReturnsEnabledFunctionsInstance() + { + // Arrange & Act + List functions = [new("Plugin", "Function", "description", [], null)]; + var behavior = ToolCallBehavior.EnableFunctions(functions); + + // Assert + Assert.IsType(behavior); + Assert.Contains($"{nameof(EnabledFunctions)}(autoInvoke:{behavior.MaximumAutoInvokeAttempts != 0})", behavior.ToString()); + } + + [Fact] + public void RequireFunctionReturnsRequiredFunctionInstance() + { + // Arrange & Act + var behavior = ToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); + + // Assert + Assert.IsType(behavior); + Assert.Contains($"{nameof(RequiredFunction)}(autoInvoke:{behavior.MaximumAutoInvokeAttempts != 0})", behavior.ToString()); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + + // Act + var options = kernelFunctions.ConfigureOptions(null); + + // Assert + Assert.Null(options.Choice); + Assert.Null(options.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var kernel = Kernel.CreateBuilder().Build(); + + // Act + var options = kernelFunctions.ConfigureOptions(kernel); + + // Assert + Assert.Null(options.Choice); + Assert.Null(options.Tools); + } + + [Fact] + public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() + { + // Arrange + var kernelFunctions = new KernelFunctions(autoInvoke: false); + var kernel = Kernel.CreateBuilder().Build(); + + var plugin = this.GetTestPlugin(); + + kernel.Plugins.Add(plugin); + + // Act + var options = kernelFunctions.ConfigureOptions(kernel); + + // Assert + Assert.Equal(ChatToolChoice.Auto, options.Choice); + + this.AssertTools(options.Tools); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() + { + // Arrange + var enabledFunctions = new EnabledFunctions([], autoInvoke: false); + + // Act + var options = enabledFunctions.ConfigureOptions(null); + + // Assert + Assert.Null(options.Choice); + Assert.Null(options.Tools); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null)); + Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel)); + Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool autoInvoke) + { + // Arrange + var plugin = this.GetTestPlugin(); + var functions = plugin.GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); + var enabledFunctions = new EnabledFunctions(functions, autoInvoke); + var kernel = Kernel.CreateBuilder().Build(); + + kernel.Plugins.Add(plugin); + + // Act + var options = enabledFunctions.ConfigureOptions(kernel); + + // Assert + Assert.Equal(ChatToolChoice.Auto, options.Choice); + + this.AssertTools(options.Tools); + } + + [Fact] + public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null)); + Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); + } + + [Fact] + public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() + { + // Arrange + var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var kernel = Kernel.CreateBuilder().Build(); + + // Act & Assert + var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel)); + Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); + } + + [Fact] + public void RequiredFunctionConfigureOptionsAddsTools() + { + // Arrange + var plugin = this.GetTestPlugin(); + var function = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); + var requiredFunction = new RequiredFunction(function, autoInvoke: true); + var kernel = new Kernel(); + kernel.Plugins.Add(plugin); + + // Act + var options = requiredFunction.ConfigureOptions(kernel); + + // Assert + Assert.NotNull(options.Choice); + + this.AssertTools(options.Tools); + } + + private KernelPlugin GetTestPlugin() + { + var function = KernelFunctionFactory.CreateFromMethod( + (string parameter1, string parameter2) => "Result1", + "MyFunction", + "Test Function", + [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], + new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); + + return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); + } + + private void AssertTools(IList? tools) + { + Assert.NotNull(tools); + var tool = Assert.Single(tools); + + Assert.NotNull(tool); + + Assert.Equal("MyPlugin-MyFunction", tool.FunctionName); + Assert.Equal("Test Function", tool.FunctionDescription); + Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.FunctionParameters.ToString()); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index bab4ac2c2e15..668b26204f88 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -2,7 +2,7 @@ - Microsoft.SemanticKernel.Connectors.OpenAI + Microsoft.SemanticKernel.Connectors.OpenAIV2 $(AssemblyName) net8.0;netstandard2.0 true diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs new file mode 100644 index 000000000000..effff740d2ed --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -0,0 +1,1189 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.ClientModel; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Diagnostics; +using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with OpenAI services. +/// +internal partial class ClientCore +{ + private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; + private const string ModelProvider = "openai"; + private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); + + /// + /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current + /// asynchronous chain of execution. + /// + /// + /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that + /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, + /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close + /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. + /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in + /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that + /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, + /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent + /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made + /// configurable should need arise. + /// + private const int MaxInflightAutoInvokes = 128; + + /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. + private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); + + /// Tracking for . + private static readonly AsyncLocal s_inflightAutoInvokes = new(); + + /// + /// Instance of for metrics. + /// + private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); + + /// + /// Instance of to keep track of the number of prompt tokens used. + /// + private static readonly Counter s_promptTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.prompt", + unit: "{token}", + description: "Number of prompt tokens used"); + + /// + /// Instance of to keep track of the number of completion tokens used. + /// + private static readonly Counter s_completionTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.completion", + unit: "{token}", + description: "Number of completion tokens used"); + + /// + /// Instance of to keep track of the total number of tokens used. + /// + private static readonly Counter s_totalTokensCounter = + s_meter.CreateCounter( + name: "semantic_kernel.connectors.openai.tokens.total", + unit: "{token}", + description: "Number of tokens used"); + + private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) + { + return new Dictionary(8) + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.CreatedAt), completions.CreatedAt }, + { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completions.Usage), completions.Usage }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completions.FinishReason), completions.FinishReason.ToString() }, + { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, + }; + } + + private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) + { + return new Dictionary(4) + { + { nameof(completionUpdate.Id), completionUpdate.Id }, + { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, + { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, + }; + } + + /// + /// Generate a new chat message + /// + /// Chat history + /// Execution settings for the completion API. + /// The containing services, plugins, and other state for use throughout the operation. + /// Async cancellation token + /// Generated chat message in string format + internal async Task> GetChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + + // Convert the incoming execution settings to OpenAI settings. + OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) + { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + + // Make the request. + OpenAIChatCompletion? chatCompletion = null; + OpenAIChatMessageContent chatMessageContent; + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.ModelId, ModelProvider, chat, chatExecutionSettings)) + { + try + { + chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.ModelId).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + + this.LogUsage(chatCompletion.Usage); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + if (chatCompletion != null) + { + // Capture available metadata even if the operation failed. + activity + .SetResponseId(chatCompletion.Id) + .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) + .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); + } + throw; + } + + chatMessageContent = this.CreateChatMessageContent(chatCompletion); + activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); + } + + // If we don't want to attempt to invoke any functions, just return the result. + if (!toolCallingConfig.AutoInvoke) + { + return [chatMessageContent]; + } + + Debug.Assert(kernel is not null); + + // Get our single result and extract the function call information. If this isn't a function call, or if it is + // but we're unable to find the function or extract the relevant information, just return the single result. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + if (chatCompletion.ToolCalls.Count == 0) + { + return [chatMessageContent]; + } + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); + } + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); + } + + // Add the original assistant message to the chat messages; this is required for the service + // to understand the tool call responses. Also add the result message to the caller's chat + // history: if they don't want it, they can remove it, but this makes the data available, + // including metadata like usage. + chatForRequest.Add(CreateRequestMessage(chatCompletion)); + chat.Add(chatMessageContent); + + // We must send back a response for every tool call, regardless of whether we successfully executed it or not. + // If we successfully execute it, we'll add the result. If we don't, we'll add an error. + for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) + { + ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (functionToolCall.Kind != ChatToolCallKind.Function) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + OpenAIFunctionToolCall? openAIFunctionToolCall; + try + { + openAIFunctionToolCall = new(functionToolCall); + } + catch (JsonException) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetOpenAIFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = chatMessageContent.ToolCalls.Count + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + + // If filter requested termination, returning latest function result. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + return [chat.Last()]; + } + } + } + } + + internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chat, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + Verify.NotNull(chat); + + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", + JsonSerializer.Serialize(chat), + JsonSerializer.Serialize(executionSettings)); + } + + OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ValidateMaxTokens(chatExecutionSettings.MaxTokens); + + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + + for (int requestIndex = 0; ; requestIndex++) + { + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); + + var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); + + // Reset state + contentBuilder?.Clear(); + toolCallIdsByIndex?.Clear(); + functionNamesByIndex?.Clear(); + functionArgumentBuildersByIndex?.Clear(); + + // Stream the response. + IReadOnlyDictionary? metadata = null; + string? streamedName = null; + ChatMessageRole? streamedRole = default; + ChatFinishReason finishReason = default; + ChatToolCall[]? toolCalls = null; + FunctionCallContent[]? functionCallContents = null; + + using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.ModelId, ModelProvider, chat, chatExecutionSettings)) + { + // Make the request. + AsyncResultCollection response; + try + { + response = RunRequest(() => this.Client.GetChatClient(this.ModelId).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); + List? streamedContents = activity is not null ? [] : null; + try + { + while (true) + { + try + { + if (!await responseEnumerator.MoveNextAsync()) + { + break; + } + } + catch (Exception ex) when (activity is not null) + { + activity.SetError(ex); + throw; + } + + StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; + metadata = GetChatCompletionMetadata(chatCompletionUpdate); + streamedRole ??= chatCompletionUpdate.Role; + //streamedName ??= update.AuthorName; + finishReason = chatCompletionUpdate.FinishReason ?? default; + + // If we're intending to invoke function calls, we need to consume that function call information. + if (toolCallingConfig.AutoInvoke) + { + foreach (var contentPart in chatCompletionUpdate.ContentUpdate) + { + if (contentPart.Kind == ChatMessageContentPartKind.Text) + { + (contentBuilder ??= new()).Append(contentPart.Text); + } + } + + OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + } + + var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.ModelId, metadata); + + foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) + { + // Using the code below to distinguish and skip non - function call related updates. + // The Kind property of updates can't be reliably used because it's only initialized for the first update. + if (string.IsNullOrEmpty(functionCallUpdate.Id) && + string.IsNullOrEmpty(functionCallUpdate.FunctionName) && + string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) + { + continue; + } + + openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( + callId: functionCallUpdate.Id, + name: functionCallUpdate.FunctionName, + arguments: functionCallUpdate.FunctionArgumentsUpdate, + functionCallIndex: functionCallUpdate.Index)); + } + + streamedContents?.Add(openAIStreamingChatMessageContent); + yield return openAIStreamingChatMessageContent; + } + + // Translate all entries into ChatCompletionsFunctionToolCall instances. + toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( + ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + // Translate all entries into FunctionCallContent instances for diagnostics purposes. + functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); + } + finally + { + activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); + await responseEnumerator.DisposeAsync(); + } + } + + // If we don't have a function to invoke, we're done. + // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service + // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool + // is specified. + if (!toolCallingConfig.AutoInvoke || + toolCallIdsByIndex is not { Count: > 0 }) + { + yield break; + } + + // Get any response content that was streamed. + string content = contentBuilder?.ToString() ?? string.Empty; + + // Log the requests + if (this.Logger.IsEnabled(LogLevel.Trace)) + { + this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); + } + else if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); + } + + // Add the original assistant message to the chat messages; this is required for the service + // to understand the tool call responses. + chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + + // Respond to each tooling request. + for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) + { + ChatToolCall toolCall = toolCalls[toolCallIndex]; + + // We currently only know about function tool calls. If it's anything else, we'll respond with an error. + if (string.IsNullOrEmpty(toolCall.FunctionName)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + continue; + } + + // Parse the function call arguments. + OpenAIFunctionToolCall? openAIFunctionToolCall; + try + { + openAIFunctionToolCall = new(toolCall); + } + catch (JsonException) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + continue; + } + + // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, + // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able + // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. + if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + continue; + } + + // Find the function in the kernel and populate the arguments. + if (!kernel!.Plugins.TryGetOpenAIFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + { + AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + continue; + } + + // Now, invoke the function, and add the resulting tool call message to the chat options. + FunctionResult functionResult = new(function) { Culture = kernel.Culture }; + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + { + Arguments = functionArgs, + RequestSequenceIndex = requestIndex, + FunctionSequenceIndex = toolCallIndex, + FunctionCount = toolCalls.Length + }; + + s_inflightAutoInvokes.Value++; + try + { + invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => + { + // Check if filter requested termination. + if (context.Terminate) + { + return; + } + + // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any + // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, + // as the called function could in turn telling the model about itself as a possible candidate for invocation. + context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); + }).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception e) +#pragma warning restore CA1031 // Do not catch general exception types + { + AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + continue; + } + finally + { + s_inflightAutoInvokes.Value--; + } + + // Apply any changes from the auto function invocation filters context to final result. + functionResult = invocationContext.Result; + + object functionResultValue = functionResult.GetValue() ?? string.Empty; + var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); + + AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); + + // If filter requested termination, returning latest function result and breaking request iteration loop. + if (invocationContext.Terminate) + { + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Filter requested termination of automatic function invocation."); + } + + var lastChatMessage = chat.Last(); + + yield return new OpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); + yield break; + } + } + } + } + + internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + ChatHistory chat = CreateNewChat(prompt, chatSettings); + + await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) + { + yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); + } + } + + internal async Task> GetChatAsTextContentsAsync( + string text, + PromptExecutionSettings? executionSettings, + Kernel? kernel, + CancellationToken cancellationToken = default) + { + OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + + ChatHistory chat = CreateNewChat(text, chatSettings); + return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) + .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) + .ToList(); + } + + /// Checks if a tool call is for a function that was defined. + private static bool IsRequestableTool(ChatCompletionOptions options, OpenAIFunctionToolCall ftc) + { + IList tools = options.Tools; + for (int i = 0; i < tools.Count; i++) + { + if (tools[i].Kind == ChatToolKind.Function && + string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Create a new empty chat instance + /// + /// Optional chat instructions for the AI service + /// Execution settings + /// Chat object + private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null) + { + var chat = new ChatHistory(); + + // If settings is not provided, create a new chat with the text as the system prompt + AuthorRole textRole = AuthorRole.System; + + if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) + { + chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); + textRole = AuthorRole.User; + } + + if (!string.IsNullOrWhiteSpace(text)) + { + chat.AddMessage(textRole, text!); + } + + return chat; + } + + private ChatCompletionOptions CreateChatCompletionOptions( + OpenAIPromptExecutionSettings executionSettings, + ChatHistory chatHistory, + ToolCallingConfig toolCallingConfig, + Kernel? kernel) + { + var options = new ChatCompletionOptions + { + MaxTokens = executionSettings.MaxTokens, + Temperature = (float?)executionSettings.Temperature, + TopP = (float?)executionSettings.TopP, + FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + PresencePenalty = (float?)executionSettings.PresencePenalty, + Seed = executionSettings.Seed, + User = executionSettings.User, + TopLogProbabilityCount = executionSettings.TopLogprobs, + IncludeLogProbabilities = executionSettings.Logprobs, + ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, + ToolChoice = toolCallingConfig.Choice, + }; + + if (toolCallingConfig.Tools is { Count: > 0 } tools) + { + options.Tools.AddRange(tools); + } + + if (executionSettings.TokenSelectionBiases is not null) + { + foreach (var keyValue in executionSettings.TokenSelectionBiases) + { + options.LogitBiases.Add(keyValue.Key, keyValue.Value); + } + } + + if (executionSettings.StopSequences is { Count: > 0 }) + { + foreach (var s in executionSettings.StopSequences) + { + options.StopSequences.Add(s); + } + } + + return options; + } + + private static List CreateChatCompletionMessages(OpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) + { + List messages = []; + + if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) + { + messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); + } + + foreach (var message in chatHistory) + { + messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); + } + + return messages; + } + + private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) + { + if (chatRole == ChatMessageRole.User) + { + return new UserChatMessage(content) { ParticipantName = name }; + } + + if (chatRole == ChatMessageRole.System) + { + return new SystemChatMessage(content) { ParticipantName = name }; + } + + if (chatRole == ChatMessageRole.Assistant) + { + return new AssistantChatMessage(tools, content) { ParticipantName = name }; + } + + throw new NotImplementedException($"Role {chatRole} is not implemented"); + } + + private static List CreateRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) + { + if (message.Role == AuthorRole.System) + { + return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Tool) + { + // Handling function results represented by the TextContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) + if (message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && + toolId?.ToString() is string toolIdString) + { + return [new ToolChatMessage(toolIdString, message.Content)]; + } + + // Handling function results represented by the FunctionResultContent type. + // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) + List? toolMessages = null; + foreach (var item in message.Items) + { + if (item is not FunctionResultContent resultContent) + { + continue; + } + + toolMessages ??= []; + + if (resultContent.Result is Exception ex) + { + toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); + continue; + } + + var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); + + toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); + } + + if (toolMessages is not null) + { + return toolMessages; + } + + throw new NotSupportedException("No function result provided in the tool message."); + } + + if (message.Role == AuthorRole.User) + { + if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) + { + return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; + } + + return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch + { + TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), + ImageContent imageContent => GetImageContentItem(imageContent), + _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") + }))) + { ParticipantName = message.AuthorName }]; + } + + if (message.Role == AuthorRole.Assistant) + { + var toolCalls = new List(); + + // Handling function calls supplied via either: + // ChatCompletionsToolCall.ToolCalls collection items or + // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. + IEnumerable? tools = (message as OpenAIChatMessageContent)?.ToolCalls; + if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) + { + tools = toolCallsObject as IEnumerable; + if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) + { + int length = array.GetArrayLength(); + var ftcs = new List(length); + for (int i = 0; i < length; i++) + { + JsonElement e = array[i]; + if (e.TryGetProperty("Id", out JsonElement id) && + e.TryGetProperty("Name", out JsonElement name) && + e.TryGetProperty("Arguments", out JsonElement arguments) && + id.ValueKind == JsonValueKind.String && + name.ValueKind == JsonValueKind.String && + arguments.ValueKind == JsonValueKind.String) + { + ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); + } + } + tools = ftcs; + } + } + + if (tools is not null) + { + toolCalls.AddRange(tools); + } + + // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. + HashSet? functionCallIds = null; + foreach (var item in message.Items) + { + if (item is not FunctionCallContent callRequest) + { + continue; + } + + functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); + + if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) + { + continue; + } + + var argument = JsonSerializer.Serialize(callRequest.Arguments); + + toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); + } + + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; + } + + throw new NotSupportedException($"Role {message.Role} is not supported."); + } + + private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) + { + if (imageContent.Data is { IsEmpty: false } data) + { + return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); + } + + if (imageContent.Uri is not null) + { + return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); + } + + throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); + } + + private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) + { + if (completion.Role == ChatMessageRole.System) + { + return ChatMessage.CreateSystemMessage(completion.Content[0].Text); + } + + if (completion.Role == ChatMessageRole.Assistant) + { + return ChatMessage.CreateAssistantMessage(completion); + } + + if (completion.Role == ChatMessageRole.User) + { + return ChatMessage.CreateUserMessage(completion.Content); + } + + throw new NotSupportedException($"Role {completion.Role} is not supported."); + } + + private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) + { + var message = new OpenAIChatMessageContent(completion, this.ModelId, GetChatCompletionMetadata(completion)); + + message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); + + return message; + } + + private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + { + var message = new OpenAIChatMessageContent(chatRole, content, this.ModelId, toolCalls, metadata) + { + AuthorName = authorName, + }; + + if (functionCalls is not null) + { + message.Items.AddRange(functionCalls); + } + + return message; + } + + private List GetFunctionCallContents(IEnumerable toolCalls) + { + List result = []; + + foreach (var toolCall in toolCalls) + { + // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. + // This allows consumers to work with functions in an LLM-agnostic way. + if (toolCall.Kind == ChatToolCallKind.Function) + { + Exception? exception = null; + KernelArguments? arguments = null; + try + { + arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); + if (arguments is not null) + { + // Iterate over copy of the names to avoid mutating the dictionary while enumerating it + var names = arguments.Names.ToArray(); + foreach (var name in names) + { + arguments[name] = arguments[name]?.ToString(); + } + } + } + catch (JsonException ex) + { + exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); + + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", toolCall.FunctionName, toolCall.Id); + } + } + + var functionName = FunctionName.Parse(toolCall.FunctionName, OpenAIFunction.NameSeparator); + + var functionCallContent = new FunctionCallContent( + functionName: functionName.Name, + pluginName: functionName.PluginName, + id: toolCall.Id, + arguments: arguments) + { + InnerContent = toolCall, + Exception = exception + }; + + result.Add(functionCallContent); + } + } + + return result; + } + + private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) + { + // Log any error + if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) + { + Debug.Assert(result is null); + logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); + } + + // Add the tool response message to the chat messages + result ??= errorMessage ?? string.Empty; + chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); + + // Add the tool response message to the chat history. + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + + if (toolCall.Kind == ChatToolCallKind.Function) + { + // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. + // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. + var functionName = FunctionName.Parse(toolCall.FunctionName, OpenAIFunction.NameSeparator); + message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); + } + + chat.Add(message); + } + + private static void ValidateMaxTokens(int? maxTokens) + { + if (maxTokens.HasValue && maxTokens < 1) + { + throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); + } + } + + /// + /// Captures usage details, including token information. + /// + /// Instance of with token usage details. + private void LogUsage(ChatTokenUsage usage) + { + if (usage is null) + { + this.Logger.LogDebug("Token usage information unavailable."); + return; + } + + if (this.Logger.IsEnabled(LogLevel.Information)) + { + this.Logger.LogInformation( + "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", + usage.InputTokens, usage.OutputTokens, usage.TotalTokens); + } + + s_promptTokensCounter.Add(usage.InputTokens); + s_completionTokensCounter.Add(usage.OutputTokens); + s_totalTokensCounter.Add(usage.TotalTokens); + } + + /// + /// Processes the function result. + /// + /// The result of the function call. + /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. + /// A string representation of the function result. + private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) + { + if (functionResult is string stringResult) + { + return stringResult; + } + + // This is an optimization to use ChatMessageContent content directly + // without unnecessary serialization of the whole message content class. + if (functionResult is ChatMessageContent chatMessageContent) + { + return chatMessageContent.ToString(); + } + + // For polymorphic serialization of unknown in advance child classes of the KernelContent class, + // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. + // For more details about the polymorphic serialization, see the article at: + // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 +#pragma warning disable CS0618 // Type or member is obsolete + return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); +#pragma warning restore CS0618 // Type or member is obsolete + } + + /// + /// Executes auto function invocation filters and/or function itself. + /// This method can be moved to when auto function invocation logic will be extracted to common place. + /// + private static async Task OnAutoFunctionInvocationAsync( + Kernel kernel, + AutoFunctionInvocationContext context, + Func functionCallCallback) + { + await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); + + return context; + } + + /// + /// This method will execute auto function invocation filters and function recursively. + /// If there are no registered filters, just function will be executed. + /// If there are registered filters, filter on position will be executed. + /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. + /// Function will be always executed as last step after all filters. + /// + private static async Task InvokeFilterOrFunctionAsync( + IList? autoFunctionInvocationFilters, + Func functionCallCallback, + AutoFunctionInvocationContext context, + int index = 0) + { + if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) + { + await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, + (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); + } + else + { + await functionCallCallback(context).ConfigureAwait(false); + } + } + + private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, OpenAIPromptExecutionSettings executionSettings, int requestIndex) + { + if (executionSettings.ToolCallBehavior is null) + { + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) + { + // Don't add any tools as we've reached the maximum attempts limit. + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); + } + + return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); + } + + var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); + + bool autoInvoke = kernel is not null && + executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && + s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; + + // Disable auto invocation if we've exceeded the allowed limit. + if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) + { + autoInvoke = false; + if (this.Logger.IsEnabled(LogLevel.Debug)) + { + this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); + } + } + + return new ToolCallingConfig( + Tools: tools ?? [s_nonInvocableFunctionTool], + Choice: choice ?? ChatToolChoice.None, + AutoInvoke: autoInvoke); + } + + private static ChatResponseFormat? GetResponseFormat(OpenAIPromptExecutionSettings executionSettings) + { + switch (executionSettings.ResponseFormat) + { + case ChatResponseFormat formatObject: + // If the response format is an OpenAI SDK ChatCompletionsResponseFormat, just pass it along. + return formatObject; + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + } + break; + } + + return null; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs index 695f23579ad1..08c617bf2e8b 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -222,6 +222,24 @@ private static async Task RunRequestAsync(Func> request) } } + /// + /// Invokes the specified request and handles exceptions. + /// + /// Type of the response. + /// Request to invoke. + /// Returns the response. + private static T RunRequest(Func request) + { + try + { + return request.Invoke(); + } + catch (ClientResultException e) + { + throw e.ToHttpOperationException(); + } + } + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) { return new GenericActionPipelinePolicy((message) => diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs new file mode 100644 index 000000000000..0bc00fdf81b2 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI specialized chat message content +/// +public sealed class OpenAIChatMessageContent : ChatMessageContent +{ + /// + /// Gets the metadata key for the tool id. + /// + public static string ToolIdProperty => "ChatCompletionsToolCall.Id"; + + /// + /// Gets the metadata key for the list of . + /// + internal static string FunctionToolCallsProperty => "ChatResponseMessage.FunctionToolCalls"; + + /// + /// Initializes a new instance of the class. + /// + internal OpenAIChatMessageContent(OpenAIChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(completion.Role.ToString()), CreateContentItems(completion.Content), modelId, completion, System.Text.Encoding.UTF8, CreateMetadataDictionary(completion.ToolCalls, metadata)) + { + this.ToolCalls = completion.ToolCalls; + } + + /// + /// Initializes a new instance of the class. + /// + internal OpenAIChatMessageContent(ChatMessageRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) + { + this.ToolCalls = toolCalls; + } + + /// + /// Initializes a new instance of the class. + /// + internal OpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) + : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) + { + this.ToolCalls = toolCalls; + } + + private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + ChatMessageContentItemCollection collection = []; + + foreach (var part in contentUpdate) + { + // We only support text content for now. + if (part.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new TextContent(part.Text)); + } + } + + return collection; + } + + /// + /// A list of the tools called by the model. + /// + public IReadOnlyList ToolCalls { get; } + + /// + /// Retrieve the resulting function from the chat result. + /// + /// The , or null if no function was returned by the model. + public IReadOnlyList GetFunctionToolCalls() + { + List? functionToolCallList = null; + + foreach (var toolCall in this.ToolCalls) + { + if (toolCall.Kind == ChatToolCallKind.Function) + { + (functionToolCallList ??= []).Add(new OpenAIFunctionToolCall(toolCall)); + } + } + + if (functionToolCallList is not null) + { + return functionToolCallList; + } + + return []; + } + + private static IReadOnlyDictionary? CreateMetadataDictionary( + IReadOnlyList toolCalls, + IReadOnlyDictionary? original) + { + // We only need to augment the metadata if there are any tool calls. + if (toolCalls.Count > 0) + { + Dictionary newDictionary; + if (original is null) + { + // There's no existing metadata to clone; just allocate a new dictionary. + newDictionary = new Dictionary(1); + } + else if (original is IDictionary origIDictionary) + { + // Efficiently clone the old dictionary to a new one. + newDictionary = new Dictionary(origIDictionary); + } + else + { + // There's metadata to clone but we have to do so one item at a time. + newDictionary = new Dictionary(original.Count + 1); + foreach (var kvp in original) + { + newDictionary[kvp.Key] = kvp.Value; + } + } + + // Add the additional entry. + newDictionary.Add(FunctionToolCallsProperty, toolCalls.Where(ctc => ctc.Kind == ChatToolCallKind.Function).ToList()); + + return newDictionary; + } + + return original; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs new file mode 100644 index 000000000000..512277245fec --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Represents a function parameter that can be passed to an OpenAI function tool call. +/// +public sealed class OpenAIFunctionParameter +{ + internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) + { + this.Name = name ?? string.Empty; + this.Description = description ?? string.Empty; + this.IsRequired = isRequired; + this.ParameterType = parameterType; + this.Schema = schema; + } + + /// Gets the name of the parameter. + public string Name { get; } + + /// Gets a description of the parameter. + public string Description { get; } + + /// Gets whether the parameter is required vs optional. + public bool IsRequired { get; } + + /// Gets the of the parameter, if known. + public Type? ParameterType { get; } + + /// Gets a JSON schema for the parameter, if known. + public KernelJsonSchema? Schema { get; } +} + +/// +/// Represents a function return parameter that can be returned by a tool call to OpenAI. +/// +public sealed class OpenAIFunctionReturnParameter +{ + internal OpenAIFunctionReturnParameter(string? description, Type? parameterType, KernelJsonSchema? schema) + { + this.Description = description ?? string.Empty; + this.Schema = schema; + this.ParameterType = parameterType; + } + + /// Gets a description of the return parameter. + public string Description { get; } + + /// Gets the of the return parameter, if known. + public Type? ParameterType { get; } + + /// Gets a JSON schema for the return parameter, if known. + public KernelJsonSchema? Schema { get; } +} + +/// +/// Represents a function that can be passed to the OpenAI API +/// +public sealed class OpenAIFunction +{ + /// + /// Cached storing the JSON for a function with no parameters. + /// + /// + /// This is an optimization to avoid serializing the same JSON Schema over and over again + /// for this relatively common case. + /// + private static readonly BinaryData s_zeroFunctionParametersSchema = new("""{"type":"object","required":[],"properties":{}}"""); + /// + /// Cached schema for a descriptionless string. + /// + private static readonly KernelJsonSchema s_stringNoDescriptionSchema = KernelJsonSchema.Parse("""{"type":"string"}"""); + + /// Initializes the OpenAIFunction. + internal OpenAIFunction( + string? pluginName, + string functionName, + string? description, + IReadOnlyList? parameters, + OpenAIFunctionReturnParameter? returnParameter) + { + Verify.NotNullOrWhiteSpace(functionName); + + this.PluginName = pluginName; + this.FunctionName = functionName; + this.Description = description; + this.Parameters = parameters; + this.ReturnParameter = returnParameter; + } + + /// Gets the separator used between the plugin name and the function name, if a plugin name is present. + /// This separator was previously _, but has been changed to - to better align to the behavior elsewhere in SK and in response + /// to developers who want to use underscores in their function or plugin names. We plan to make this setting configurable in the future. + public static string NameSeparator { get; set; } = "-"; + + /// Gets the name of the plugin with which the function is associated, if any. + public string? PluginName { get; } + + /// Gets the name of the function. + public string FunctionName { get; } + + /// Gets the fully-qualified name of the function. + /// + /// This is the concatenation of the and the , + /// separated by . If there is no , this is + /// the same as . + /// + public string FullyQualifiedName => + string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{NameSeparator}{this.FunctionName}"; + + /// Gets a description of the function. + public string? Description { get; } + + /// Gets a list of parameters to the function, if any. + public IReadOnlyList? Parameters { get; } + + /// Gets the return parameter of the function, if any. + public OpenAIFunctionReturnParameter? ReturnParameter { get; } + + /// + /// Converts the representation to the OpenAI SDK's + /// representation. + /// + /// A containing all the function information. + public ChatTool ToFunctionDefinition() + { + BinaryData resultParameters = s_zeroFunctionParametersSchema; + + IReadOnlyList? parameters = this.Parameters; + if (parameters is { Count: > 0 }) + { + var properties = new Dictionary(); + var required = new List(); + + for (int i = 0; i < parameters.Count; i++) + { + var parameter = parameters[i]; + properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); + if (parameter.IsRequired) + { + required.Add(parameter.Name); + } + } + + resultParameters = BinaryData.FromObjectAsJson(new + { + type = "object", + required, + properties, + }); + } + + return ChatTool.CreateFunctionTool + ( + functionName: this.FullyQualifiedName, + functionDescription: this.Description, + functionParameters: resultParameters + ); + } + + /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) + private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) + { + // If there's a description, incorporate it. + if (!string.IsNullOrWhiteSpace(description)) + { + return KernelJsonSchemaBuilder.Build(null, typeof(string), description); + } + + // Otherwise, we can use a cached schema for a string with no description. + return s_stringNoDescriptionSchema; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs new file mode 100644 index 000000000000..822862b24d87 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using System.Text.Json; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Represents an OpenAI function tool call with deserialized function name and arguments. +/// +public sealed class OpenAIFunctionToolCall +{ + private string? _fullyQualifiedFunctionName; + + /// Initialize the from a . + internal OpenAIFunctionToolCall(ChatToolCall functionToolCall) + { + Verify.NotNull(functionToolCall); + Verify.NotNull(functionToolCall.FunctionName); + + string fullyQualifiedFunctionName = functionToolCall.FunctionName; + string functionName = fullyQualifiedFunctionName; + string? arguments = functionToolCall.FunctionArguments; + string? pluginName = null; + + int separatorPos = fullyQualifiedFunctionName.IndexOf(OpenAIFunction.NameSeparator, StringComparison.Ordinal); + if (separatorPos >= 0) + { + pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString(); + functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + OpenAIFunction.NameSeparator.Length).Trim().ToString(); + } + + this.Id = functionToolCall.Id; + this._fullyQualifiedFunctionName = fullyQualifiedFunctionName; + this.PluginName = pluginName; + this.FunctionName = functionName; + if (!string.IsNullOrWhiteSpace(arguments)) + { + this.Arguments = JsonSerializer.Deserialize>(arguments!); + } + } + + /// Gets the ID of the tool call. + public string? Id { get; } + + /// Gets the name of the plugin with which this function is associated, if any. + public string? PluginName { get; } + + /// Gets the name of the function. + public string FunctionName { get; } + + /// Gets a name/value collection of the arguments to the function, if any. + public Dictionary? Arguments { get; } + + /// Gets the fully-qualified name of the function. + /// + /// This is the concatenation of the and the , + /// separated by . If there is no , + /// this is the same as . + /// + public string FullyQualifiedName => + this._fullyQualifiedFunctionName ??= + string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{OpenAIFunction.NameSeparator}{this.FunctionName}"; + + /// + public override string ToString() + { + var sb = new StringBuilder(this.FullyQualifiedName); + + sb.Append('('); + if (this.Arguments is not null) + { + string separator = ""; + foreach (var arg in this.Arguments) + { + sb.Append(separator).Append(arg.Key).Append(':').Append(arg.Value); + separator = ", "; + } + } + sb.Append(')'); + + return sb.ToString(); + } + + /// + /// Tracks tooling updates from streaming responses. + /// + /// The tool call updates to incorporate. + /// Lazily-initialized dictionary mapping indices to IDs. + /// Lazily-initialized dictionary mapping indices to names. + /// Lazily-initialized dictionary mapping indices to arguments. + internal static void TrackStreamingToolingUpdate( + IReadOnlyList? updates, + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + if (updates is null) + { + // Nothing to track. + return; + } + + foreach (var update in updates) + { + // If we have an ID, ensure the index is being tracked. Even if it's not a function update, + // we want to keep track of it so we can send back an error. + if (update.Id is string id) + { + (toolCallIdsByIndex ??= [])[update.Index] = id; + } + + // Ensure we're tracking the function's name. + if (update.FunctionName is string name) + { + (functionNamesByIndex ??= [])[update.Index] = name; + } + + // Ensure we're tracking the function's arguments. + if (update.FunctionArgumentsUpdate is string argumentsUpdate) + { + if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) + { + functionArgumentBuildersByIndex[update.Index] = arguments = new(); + } + + arguments.Append(argumentsUpdate); + } + } + } + + /// + /// Converts the data built up by into an array of s. + /// + /// Dictionary mapping indices to IDs. + /// Dictionary mapping indices to names. + /// Dictionary mapping indices to arguments. + internal static ChatToolCall[] ConvertToolCallUpdatesToFunctionToolCalls( + ref Dictionary? toolCallIdsByIndex, + ref Dictionary? functionNamesByIndex, + ref Dictionary? functionArgumentBuildersByIndex) + { + ChatToolCall[] toolCalls = []; + if (toolCallIdsByIndex is { Count: > 0 }) + { + toolCalls = new ChatToolCall[toolCallIdsByIndex.Count]; + + int i = 0; + foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) + { + string? functionName = null; + StringBuilder? functionArguments = null; + + functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); + functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); + + toolCalls[i] = ChatToolCall.CreateFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); + i++; + } + + Debug.Assert(i == toolCalls.Length); + } + + return toolCalls; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs new file mode 100644 index 000000000000..bd9ae55ce888 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI specialized streaming chat message content. +/// +/// +/// Represents a chat message content chunk that was streamed from the remote model. +/// +public sealed class OpenAIStreamingChatMessageContent : StreamingChatMessageContent +{ + /// + /// The reason why the completion finished. + /// + public ChatFinishReason? FinishReason { get; set; } + + /// + /// Create a new instance of the class. + /// + /// Internal OpenAI SDK Message update representation + /// Index of the choice + /// The model ID used to generate the content + /// Additional metadata + internal OpenAIStreamingChatMessageContent( + StreamingChatCompletionUpdate chatUpdate, + int choiceIndex, + string modelId, + IReadOnlyDictionary? metadata = null) + : base( + chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, + null, + chatUpdate, + choiceIndex, + modelId, + Encoding.UTF8, + metadata) + { + this.ToolCallUpdates = chatUpdate.ToolCallUpdates; + this.FinishReason = chatUpdate.FinishReason; + this.Items = CreateContentItems(chatUpdate.ContentUpdate); + } + + /// + /// Create a new instance of the class. + /// + /// Author role of the message + /// Content of the message + /// Tool call updates + /// Completion finish reason + /// Index of the choice + /// The model ID used to generate the content + /// Additional metadata + internal OpenAIStreamingChatMessageContent( + AuthorRole? authorRole, + string? content, + IReadOnlyList? toolCallUpdates = null, + ChatFinishReason? completionsFinishReason = null, + int choiceIndex = 0, + string? modelId = null, + IReadOnlyDictionary? metadata = null) + : base( + authorRole, + content, + null, + choiceIndex, + modelId, + Encoding.UTF8, + metadata) + { + this.ToolCallUpdates = toolCallUpdates; + this.FinishReason = completionsFinishReason; + } + + /// Gets any update information in the message about a tool call. + public IReadOnlyList? ToolCallUpdates { get; } + + /// + public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); + + /// + public override string ToString() => this.Content ?? string.Empty; + + private static StreamingKernelContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) + { + StreamingKernelContentItemCollection collection = []; + + foreach (var content in contentUpdate) + { + // We only support text content for now. + if (content.Kind == ChatMessageContentPartKind.Text) + { + collection.Add(new StreamingTextContent(content.Text)); + } + } + + return collection; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs new file mode 100644 index 000000000000..47697609aebc --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Microsoft.SemanticKernel; + +/// +/// Chat history extensions. +/// +public static class OpenAIChatHistoryExtensions +{ + /// + /// Add a message to the chat history at the end of the streamed message + /// + /// Target chat history + /// list of streaming message contents + /// Returns the original streaming results with some message processing + [Experimental("SKEXP0010")] + public static async IAsyncEnumerable AddStreamingMessageAsync(this ChatHistory chatHistory, IAsyncEnumerable streamingMessageContents) + { + List messageContents = []; + + // Stream the response. + StringBuilder? contentBuilder = null; + Dictionary? toolCallIdsByIndex = null; + Dictionary? functionNamesByIndex = null; + Dictionary? functionArgumentBuildersByIndex = null; + Dictionary? metadata = null; + AuthorRole? streamedRole = null; + string? streamedName = null; + + await foreach (var chatMessage in streamingMessageContents.ConfigureAwait(false)) + { + metadata ??= (Dictionary?)chatMessage.Metadata; + + if (chatMessage.Content is { Length: > 0 } contentUpdate) + { + (contentBuilder ??= new()).Append(contentUpdate); + } + + OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + + // Is always expected to have at least one chunk with the role provided from a streaming message + streamedRole ??= chatMessage.Role; + streamedName ??= chatMessage.AuthorName; + + messageContents.Add(chatMessage); + yield return chatMessage; + } + + if (messageContents.Count != 0) + { + var role = streamedRole ?? AuthorRole.Assistant; + + chatHistory.Add( + new OpenAIChatMessageContent( + role, + contentBuilder?.ToString() ?? string.Empty, + messageContents[0].ModelId!, + OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), + metadata) + { AuthorName = streamedName }); + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index 795f75a5d977..9f7472f2eb51 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -10,9 +10,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextGeneration; using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -321,4 +323,107 @@ public static IKernelBuilder AddOpenAIFiles( } #endregion + + #region Chat Completion + + /// + /// Adds the OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The HttpClient to use with this service. + /// The same instance as . + public static IKernelBuilder AddOpenAIChatCompletion( + this IKernelBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + apiKey, + orgId, + HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + + return builder; + } + + /// + /// Adds the OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model id + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + public static IKernelBuilder AddOpenAIChatCompletion( + this IKernelBuilder builder, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + + return builder; + } + + /// + /// Adds the Custom Endpoint OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// Custom OpenAI Compatible Message API endpoint + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The HttpClient to use with this service. + /// The same instance as . + [Experimental("SKEXP0010")] + public static IKernelBuilder AddOpenAIChatCompletion( + this IKernelBuilder builder, + string modelId, + Uri endpoint, + string? apiKey, + string? orgId = null, + string? serviceId = null, + HttpClient? httpClient = null) + { + Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId: modelId, + apiKey: apiKey, + endpoint: endpoint, + organization: orgId, + httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), + loggerFactory: serviceProvider.GetService()); + + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + + return builder; + } + + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs new file mode 100644 index 000000000000..a0982942b222 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Extensions for specific to the OpenAI connector. +/// +public static class OpenAIKernelFunctionMetadataExtensions +{ + /// + /// Convert a to an . + /// + /// The object to convert. + /// An object. + public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metadata) + { + IReadOnlyList metadataParams = metadata.Parameters; + + var openAIParams = new OpenAIFunctionParameter[metadataParams.Count]; + for (int i = 0; i < openAIParams.Length; i++) + { + var param = metadataParams[i]; + + openAIParams[i] = new OpenAIFunctionParameter( + param.Name, + GetDescription(param), + param.IsRequired, + param.ParameterType, + param.Schema); + } + + return new OpenAIFunction( + metadata.PluginName, + metadata.Name, + metadata.Description, + openAIParams, + new OpenAIFunctionReturnParameter( + metadata.ReturnParameter.Description, + metadata.ReturnParameter.ParameterType, + metadata.ReturnParameter.Schema)); + + static string GetDescription(KernelParameterMetadata param) + { + if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) + { + return $"{param.Description} (default value: {stringValue})"; + } + + return param.Description; + } + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs new file mode 100644 index 000000000000..2451cab7d399 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Extension methods for . +/// +public static class OpenAIPluginCollectionExtensions +{ + /// + /// Given an object, tries to retrieve the corresponding and populate with its parameters. + /// + /// The plugins. + /// The object. + /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, + /// When this method returns, the arguments for the function; otherwise, + /// if the function was found; otherwise, . + public static bool TryGetOpenAIFunctionAndArguments( + this IReadOnlyKernelPluginCollection plugins, + ChatToolCall functionToolCall, + [NotNullWhen(true)] out KernelFunction? function, + out KernelArguments? arguments) => + plugins.TryGetOpenAIFunctionAndArguments(new OpenAIFunctionToolCall(functionToolCall), out function, out arguments); + + /// + /// Given an object, tries to retrieve the corresponding and populate with its parameters. + /// + /// The plugins. + /// The object. + /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, + /// When this method returns, the arguments for the function; otherwise, + /// if the function was found; otherwise, . + public static bool TryGetOpenAIFunctionAndArguments( + this IReadOnlyKernelPluginCollection plugins, + OpenAIFunctionToolCall functionToolCall, + [NotNullWhen(true)] out KernelFunction? function, + out KernelArguments? arguments) + { + if (plugins.TryGetFunction(functionToolCall.PluginName, functionToolCall.FunctionName, out function)) + { + // Add parameters to arguments + arguments = null; + if (functionToolCall.Arguments is not null) + { + arguments = []; + foreach (var parameter in functionToolCall.Arguments) + { + arguments[parameter.Key] = parameter.Value?.ToString(); + } + } + + return true; + } + + // Function not found in collection + arguments = null; + return false; + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index eff0b551876e..9227a2d484e0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -5,9 +5,11 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AudioToText; +using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Embeddings; using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.TextGeneration; using Microsoft.SemanticKernel.TextToAudio; using Microsoft.SemanticKernel.TextToImage; using OpenAI; @@ -298,4 +300,102 @@ public static IServiceCollection AddOpenAIFiles( } #endregion + + #region Chat Completion + + /// + /// Adds the OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The same instance as . + public static IServiceCollection AddOpenAIChatCompletion( + this IServiceCollection services, + string modelId, + string apiKey, + string? orgId = null, + string? serviceId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + apiKey, + orgId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, (Func)Factory); + services.AddKeyedSingleton(serviceId, (Func)Factory); + + return services; + } + + /// + /// Adds the OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model id + /// to use for the service. If null, one must be available in the service provider when this service is resolved. + /// A local identifier for the given AI service + /// The same instance as . + public static IServiceCollection AddOpenAIChatCompletion(this IServiceCollection services, + string modelId, + OpenAIClient? openAIClient = null, + string? serviceId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, (Func)Factory); + services.AddKeyedSingleton(serviceId, (Func)Factory); + + return services; + } + + /// + /// Adds the Custom OpenAI chat completion service to the list. + /// + /// The instance to augment. + /// OpenAI model name, see https://platform.openai.com/docs/models + /// A Custom Message API compatible endpoint. + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// A local identifier for the given AI service + /// The same instance as . + [Experimental("SKEXP0010")] + public static IServiceCollection AddOpenAIChatCompletion( + this IServiceCollection services, + string modelId, + Uri endpoint, + string? apiKey = null, + string? orgId = null, + string? serviceId = null) + { + Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + + OpenAIChatCompletionService Factory(IServiceProvider serviceProvider, object? _) => + new(modelId, + endpoint, + apiKey, + orgId, + HttpClientProvider.GetHttpClient(serviceProvider), + serviceProvider.GetService()); + + services.AddKeyedSingleton(serviceId, (Func)Factory); + services.AddKeyedSingleton(serviceId, (Func)Factory); + + return services; + } + + #endregion } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs new file mode 100644 index 000000000000..4d87999cdf12 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.TextGeneration; +using OpenAI; + +#pragma warning disable CA1862 // Use the 'StringComparison' method overloads to perform case-insensitive string comparisons +#pragma warning disable RCS1155 // Use StringComparison when comparing strings + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// OpenAI chat completion service. +/// +public sealed class OpenAIChatCompletionService : IChatCompletionService, ITextGenerationService +{ + /// Core implementation shared by OpenAI clients. + private readonly ClientCore _client; + + /// + /// Create an instance of the OpenAI chat completion connector + /// + /// Model name + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIChatCompletionService( + string modelId, + string apiKey, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null +) + { + this._client = new( + modelId, + apiKey, + organization, + endpoint: null, + httpClient, + loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); + } + + /// + /// Create an instance of the Custom Message API OpenAI chat completion connector + /// + /// Model name + /// Custom Message API compatible endpoint + /// OpenAI API Key + /// OpenAI Organization Id (usually optional) + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + [Experimental("SKEXP0010")] + public OpenAIChatCompletionService( + string modelId, + Uri endpoint, + string? apiKey = null, + string? organization = null, + HttpClient? httpClient = null, + ILoggerFactory? loggerFactory = null) + { + Uri? internalClientEndpoint = null; + var providedEndpoint = endpoint ?? httpClient?.BaseAddress; + if (providedEndpoint is not null) + { + // If the provided endpoint does not provide a path, we add a version to the base path for compatibility + if (providedEndpoint.PathAndQuery.Length == 0 || providedEndpoint.PathAndQuery == "/") + { + internalClientEndpoint = new Uri(providedEndpoint, "/v1/"); + } + else + { + // As OpenAI Client automatically adds the chatcompletions endpoint, we remove it to avoid duplication. + const string PathAndQueryPattern = "/chat/completions"; + var providedEndpointText = providedEndpoint.ToString(); + int index = providedEndpointText.IndexOf(PathAndQueryPattern, StringComparison.OrdinalIgnoreCase); + if (index >= 0) + { + internalClientEndpoint = new Uri($"{providedEndpointText.Substring(0, index)}{providedEndpointText.Substring(index + PathAndQueryPattern.Length)}"); + } + else + { + internalClientEndpoint = providedEndpoint; + } + } + } + + this._client = new( + modelId, + apiKey, + organization, + internalClientEndpoint, + httpClient, + loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); + } + + /// + /// Create an instance of the OpenAI chat completion connector + /// + /// Model name + /// Custom for HTTP requests. + /// The to use for logging. If null, no logging will be performed. + public OpenAIChatCompletionService( + string modelId, + OpenAIClient openAIClient, + ILoggerFactory? loggerFactory = null) + { + this._client = new( + modelId, + openAIClient, + loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); + } + + /// + public IReadOnlyDictionary Attributes => this._client.Attributes; + + /// + public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + } + + /// + public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + } + + /// + public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); + } + + /// + public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) + { + this._client.LogActionDetails(); + return this._client.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); + } +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs new file mode 100644 index 000000000000..fe911f32d627 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs @@ -0,0 +1,372 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Text; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/* Phase 06 +- Drop FromExecutionSettingsWithData Azure specific method +*/ + +/// +/// Execution settings for an OpenAI completion request. +/// +[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] +public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings +{ + /// + /// Temperature controls the randomness of the completion. + /// The higher the temperature, the more random the completion. + /// Default is 1.0. + /// + [JsonPropertyName("temperature")] + public double Temperature + { + get => this._temperature; + + set + { + this.ThrowIfFrozen(); + this._temperature = value; + } + } + + /// + /// TopP controls the diversity of the completion. + /// The higher the TopP, the more diverse the completion. + /// Default is 1.0. + /// + [JsonPropertyName("top_p")] + public double TopP + { + get => this._topP; + + set + { + this.ThrowIfFrozen(); + this._topP = value; + } + } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens + /// based on whether they appear in the text so far, increasing the + /// model's likelihood to talk about new topics. + /// + [JsonPropertyName("presence_penalty")] + public double PresencePenalty + { + get => this._presencePenalty; + + set + { + this.ThrowIfFrozen(); + this._presencePenalty = value; + } + } + + /// + /// Number between -2.0 and 2.0. Positive values penalize new tokens + /// based on their existing frequency in the text so far, decreasing + /// the model's likelihood to repeat the same line verbatim. + /// + [JsonPropertyName("frequency_penalty")] + public double FrequencyPenalty + { + get => this._frequencyPenalty; + + set + { + this.ThrowIfFrozen(); + this._frequencyPenalty = value; + } + } + + /// + /// The maximum number of tokens to generate in the completion. + /// + [JsonPropertyName("max_tokens")] + public int? MaxTokens + { + get => this._maxTokens; + + set + { + this.ThrowIfFrozen(); + this._maxTokens = value; + } + } + + /// + /// Sequences where the completion will stop generating further tokens. + /// + [JsonPropertyName("stop_sequences")] + public IList? StopSequences + { + get => this._stopSequences; + + set + { + this.ThrowIfFrozen(); + this._stopSequences = value; + } + } + + /// + /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the + /// same seed and parameters should return the same result. Determinism is not guaranteed. + /// + [JsonPropertyName("seed")] + public long? Seed + { + get => this._seed; + + set + { + this.ThrowIfFrozen(); + this._seed = value; + } + } + + /// + /// Gets or sets the response format to use for the completion. + /// + /// + /// Possible values are: "json_object", "text", object. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("response_format")] + public object? ResponseFormat + { + get => this._responseFormat; + + set + { + this.ThrowIfFrozen(); + this._responseFormat = value; + } + } + + /// + /// The system prompt to use when generating text using a chat model. + /// Defaults to "Assistant is a large language model." + /// + [JsonPropertyName("chat_system_prompt")] + public string? ChatSystemPrompt + { + get => this._chatSystemPrompt; + + set + { + this.ThrowIfFrozen(); + this._chatSystemPrompt = value; + } + } + + /// + /// Modify the likelihood of specified tokens appearing in the completion. + /// + [JsonPropertyName("token_selection_biases")] + public IDictionary? TokenSelectionBiases + { + get => this._tokenSelectionBiases; + + set + { + this.ThrowIfFrozen(); + this._tokenSelectionBiases = value; + } + } + + /// + /// Gets or sets the behavior for how tool calls are handled. + /// + /// + /// + /// To disable all tool calling, set the property to null (the default). + /// + /// To request that the model use a specific function, set the property to an instance returned + /// from . + /// + /// + /// To allow the model to request one of any number of functions, set the property to an + /// instance returned from , called with + /// a list of the functions available. + /// + /// + /// To allow the model to request one of any of the functions in the supplied , + /// set the property to if the client should simply + /// send the information about the functions and not handle the response in any special manner, or + /// if the client should attempt to automatically + /// invoke the function and send the result back to the service. + /// + /// + /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service + /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to + /// resolve that function from the functions available in the , and if found, rather + /// than returning the response back to the caller, it will handle the request automatically, invoking + /// the function, and sending back the result. The intermediate messages will be retained in the + /// if an instance was provided. + /// + public ToolCallBehavior? ToolCallBehavior + { + get => this._toolCallBehavior; + + set + { + this.ThrowIfFrozen(); + this._toolCallBehavior = value; + } + } + + /// + /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse + /// + public string? User + { + get => this._user; + + set + { + this.ThrowIfFrozen(); + this._user = value; + } + } + + /// + /// Whether to return log probabilities of the output tokens or not. + /// If true, returns the log probabilities of each output token returned in the `content` of `message`. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("logprobs")] + public bool? Logprobs + { + get => this._logprobs; + + set + { + this.ThrowIfFrozen(); + this._logprobs = value; + } + } + + /// + /// An integer specifying the number of most likely tokens to return at each token position, each with an associated log probability. + /// + [Experimental("SKEXP0010")] + [JsonPropertyName("top_logprobs")] + public int? TopLogprobs + { + get => this._topLogprobs; + + set + { + this.ThrowIfFrozen(); + this._topLogprobs = value; + } + } + + /// + public override void Freeze() + { + if (this.IsFrozen) + { + return; + } + + base.Freeze(); + + if (this._stopSequences is not null) + { + this._stopSequences = new ReadOnlyCollection(this._stopSequences); + } + + if (this._tokenSelectionBiases is not null) + { + this._tokenSelectionBiases = new ReadOnlyDictionary(this._tokenSelectionBiases); + } + } + + /// + public override PromptExecutionSettings Clone() + { + return new OpenAIPromptExecutionSettings() + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + TopP = this.TopP, + PresencePenalty = this.PresencePenalty, + FrequencyPenalty = this.FrequencyPenalty, + MaxTokens = this.MaxTokens, + StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, + Seed = this.Seed, + ResponseFormat = this.ResponseFormat, + TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, + ToolCallBehavior = this.ToolCallBehavior, + User = this.User, + ChatSystemPrompt = this.ChatSystemPrompt, + Logprobs = this.Logprobs, + TopLogprobs = this.TopLogprobs + }; + } + + /// + /// Default max tokens for a text generation + /// + internal static int DefaultTextMaxTokens { get; } = 256; + + /// + /// Create a new settings object with the values from another settings object. + /// + /// Template configuration + /// Default max tokens + /// An instance of OpenAIPromptExecutionSettings + public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) + { + if (executionSettings is null) + { + return new OpenAIPromptExecutionSettings() + { + MaxTokens = defaultMaxTokens + }; + } + + if (executionSettings is OpenAIPromptExecutionSettings settings) + { + return settings; + } + + var json = JsonSerializer.Serialize(executionSettings); + + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + + return openAIExecutionSettings!; + } + + #region private ================================================================================ + + private double _temperature = 1; + private double _topP = 1; + private double _presencePenalty; + private double _frequencyPenalty; + private int? _maxTokens; + private IList? _stopSequences; + private long? _seed; + private object? _responseFormat; + private IDictionary? _tokenSelectionBiases; + private ToolCallBehavior? _toolCallBehavior; + private string? _user; + private string? _chatSystemPrompt; + private bool? _logprobs; + private int? _topLogprobs; + + #endregion +} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs new file mode 100644 index 000000000000..57bc8f391573 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs @@ -0,0 +1,281 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using System.Text.Json; +using OpenAI.Chat; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// Represents a behavior for OpenAI tool calls. +public abstract class ToolCallBehavior +{ + // NOTE: Right now, the only tools that are available are for function calling. In the future, + // this class can be extended to support additional kinds of tools, including composite ones: + // the OpenAIPromptExecutionSettings has a single ToolCallBehavior property, but we could + // expose a `public static ToolCallBehavior Composite(params ToolCallBehavior[] behaviors)` + // or the like to allow multiple distinct tools to be provided, should that be appropriate. + // We can also consider additional forms of tools, such as ones that dynamically examine + // the Kernel, KernelArguments, etc., and dynamically contribute tools to the ChatCompletionsOptions. + + /// + /// The default maximum number of tool-call auto-invokes that can be made in a single request. + /// + /// + /// After this number of iterations as part of a single user request is reached, auto-invocation + /// will be disabled (e.g. will behave like )). + /// This is a safeguard against possible runaway execution if the model routinely re-requests + /// the same function over and over. It is currently hardcoded, but in the future it could + /// be made configurable by the developer. Other configuration is also possible in the future, + /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure + /// to find the requested function, failure to invoke the function, etc.), with behaviors for + /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call + /// support, where the model can request multiple tools in a single response, it is significantly + /// less likely that this limit is reached, as most of the time only a single request is needed. + /// + private const int DefaultMaximumAutoInvokeAttempts = 128; + + /// + /// Gets an instance that will provide all of the 's plugins' function information. + /// Function call requests from the model will be propagated back to the caller. + /// + /// + /// If no is available, no function information will be provided to the model. + /// + public static ToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); + + /// + /// Gets an instance that will both provide all of the 's plugins' function information + /// to the model and attempt to automatically handle any function call requests. + /// + /// + /// When successful, tool call requests from the model become an implementation detail, with the service + /// handling invoking any requested functions and supplying the results back to the model. + /// If no is available, no function information will be provided to the model. + /// + public static ToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); + + /// Gets an instance that will provide the specified list of functions to the model. + /// The functions that should be made available to the model. + /// true to attempt to automatically handle function call requests; otherwise, false. + /// + /// The that may be set into + /// to indicate that the specified functions should be made available to the model. + /// + public static ToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) + { + Verify.NotNull(functions); + return new EnabledFunctions(functions, autoInvoke); + } + + /// Gets an instance that will request the model to use the specified function. + /// The function the model should request to use. + /// true to attempt to automatically handle function call requests; otherwise, false. + /// + /// The that may be set into + /// to indicate that the specified function should be requested by the model. + /// + public static ToolCallBehavior RequireFunction(OpenAIFunction function, bool autoInvoke = false) + { + Verify.NotNull(function); + return new RequiredFunction(function, autoInvoke); + } + + /// Initializes the instance; prevents external instantiation. + private ToolCallBehavior(bool autoInvoke) + { + this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; + } + + /// + /// Options to control tool call result serialization behavior. + /// + [Obsolete("This property is deprecated in favor of Kernel.SerializerOptions that will be introduced in one of the following releases.")] + [ExcludeFromCodeCoverage] + [EditorBrowsable(EditorBrowsableState.Never)] + public virtual JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } + + /// Gets how many requests are part of a single interaction should include this tool in the request. + /// + /// This should be greater than or equal to . It defaults to . + /// Once this limit is reached, the tools will no longer be included in subsequent retries as part of the operation, e.g. + /// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result + /// will not include the tools for further use. + /// + internal virtual int MaximumUseAttempts => int.MaxValue; + + /// Gets how many tool call request/response roundtrips are supported with auto-invocation. + /// + /// To disable auto invocation, this can be set to 0. + /// + internal int MaximumAutoInvokeAttempts { get; } + + /// + /// Gets whether validation against a specified list is required before allowing the model to request a function from the kernel. + /// + /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. + internal virtual bool AllowAnyRequestedKernelFunction => false; + + /// Returns list of available tools and the way model should use them. + /// The used for the operation. This can be queried to determine what tools to return. + internal abstract (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel); + + /// + /// Represents a that will provide to the model all available functions from a + /// provided by the client. Setting this will have no effect if no is provided. + /// + internal sealed class KernelFunctions : ToolCallBehavior + { + internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } + + public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; + + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) + { + ChatToolChoice? choice = null; + List? tools = null; + + // If no kernel is provided, we don't have any tools to provide. + if (kernel is not null) + { + // Provide all functions from the kernel. + IList functions = kernel.Plugins.GetFunctionsMetadata(); + if (functions.Count > 0) + { + choice = ChatToolChoice.Auto; + tools = []; + for (int i = 0; i < functions.Count; i++) + { + tools.Add(functions[i].ToOpenAIFunction().ToFunctionDefinition()); + } + } + } + + return (tools, choice); + } + + internal override bool AllowAnyRequestedKernelFunction => true; + } + + /// + /// Represents a that provides a specified list of functions to the model. + /// + internal sealed class EnabledFunctions : ToolCallBehavior + { + private readonly OpenAIFunction[] _openAIFunctions; + private readonly ChatTool[] _functions; + + public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) + { + this._openAIFunctions = functions.ToArray(); + + var defs = new ChatTool[this._openAIFunctions.Length]; + for (int i = 0; i < defs.Length; i++) + { + defs[i] = this._openAIFunctions[i].ToFunctionDefinition(); + } + this._functions = defs; + } + + public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.FunctionName))}"; + + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) + { + ChatToolChoice? choice = null; + List? tools = null; + + OpenAIFunction[] openAIFunctions = this._openAIFunctions; + ChatTool[] functions = this._functions; + Debug.Assert(openAIFunctions.Length == functions.Length); + + if (openAIFunctions.Length > 0) + { + bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && kernel is null) + { + throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); + } + + choice = ChatToolChoice.Auto; + tools = []; + for (int i = 0; i < openAIFunctions.Length; i++) + { + // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. + if (autoInvoke) + { + Debug.Assert(kernel is not null); + OpenAIFunction f = openAIFunctions[i]; + if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) + { + throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); + } + } + + // Add the function. + tools.Add(functions[i]); + } + } + + return (tools, choice); + } + } + + /// Represents a that requests the model use a specific function. + internal sealed class RequiredFunction : ToolCallBehavior + { + private readonly OpenAIFunction _function; + private readonly ChatTool _tool; + private readonly ChatToolChoice _choice; + + public RequiredFunction(OpenAIFunction function, bool autoInvoke) : base(autoInvoke) + { + this._function = function; + this._tool = function.ToFunctionDefinition(); + this._choice = new ChatToolChoice(this._tool); + } + + public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.FunctionName}"; + + internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) + { + bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; + + // If auto-invocation is specified, we need a kernel to be able to invoke the functions. + // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions + // and then fail to do so, so we fail before we get to that point. This is an error + // on the consumers behalf: if they specify auto-invocation with any functions, they must + // specify the kernel and the kernel must contain those functions. + if (autoInvoke && kernel is null) + { + throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); + } + + // Make sure that if auto-invocation is specified, the required function can be found in the kernel. + if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) + { + throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); + } + + return ([this._tool], this._choice); + } + + /// Gets how many requests are part of a single interaction should include this tool in the request. + /// + /// Unlike and , this must use 1 as the maximum + /// use attempts. Otherwise, every call back to the model _requires_ it to invoke the function (as opposed + /// to allows it), which means we end up doing the same work over and over and over until the maximum is reached. + /// Thus for "requires", we must send the tool information only once. + /// + internal override int MaximumUseAttempts => 1; + } +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs new file mode 100644 index 000000000000..e475682b8c13 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs @@ -0,0 +1,270 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Http.Resilience; +using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class OpenAIChatCompletionTests : BaseIntegrationTest +{ + [Fact] + //[Fact(Skip = "Skipping while we investigate issue with GitHub actions.")] + public async Task ItCanUseOpenAiChatForTextGenerationAsync() + { + // + var kernel = this.CreateAndInitializeKernel(); + + var func = kernel.CreateFunctionFromPrompt( + "List the two planets after '{{$input}}', excluding moons, using bullet points.", + new OpenAIPromptExecutionSettings()); + + // Act + var result = await func.InvokeAsync(kernel, new() { [InputParameterName] = "Jupiter" }); + + // Assert + Assert.NotNull(result); + Assert.Contains("Saturn", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + Assert.Contains("Uranus", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task OpenAIStreamingTestAsync() + { + // + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "ChatPlugin"); + + StringBuilder fullResult = new(); + + var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; + + // Act + await foreach (var content in kernel.InvokeStreamingAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt })) + { + fullResult.Append(content); + } + + // Assert + Assert.Contains("Pike Place", fullResult.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task OpenAIHttpRetryPolicyTestAsync() + { + // + List statusCodes = []; + + var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(openAIConfiguration); + Assert.NotNull(openAIConfiguration.ChatModelId); + + var kernelBuilder = Kernel.CreateBuilder(); + + kernelBuilder.AddOpenAIChatCompletion( + modelId: openAIConfiguration.ChatModelId, + apiKey: "INVALID_KEY"); + + kernelBuilder.Services.ConfigureHttpClientDefaults(c => + { + // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example + c.AddStandardResilienceHandler().Configure(o => + { + o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.Unauthorized); + o.Retry.OnRetry = args => + { + statusCodes.Add(args.Outcome.Result?.StatusCode); + return ValueTask.CompletedTask; + }; + }); + }); + + var target = kernelBuilder.Build(); + + var plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); + + var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; + + // Act + var exception = await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = prompt })); + + // Assert + Assert.All(statusCodes, s => Assert.Equal(HttpStatusCode.Unauthorized, s)); + Assert.Equal(HttpStatusCode.Unauthorized, ((HttpOperationException)exception).StatusCode); + } + + [Fact] + public async Task OpenAIShouldReturnMetadataAsync() + { + // + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); + + // Act + var result = await kernel.InvokeAsync(plugins["FunPlugin"]["Limerick"]); + + // Assert + Assert.NotNull(result.Metadata); + + // Usage + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + } + + [Theory(Skip = "This test is for manual verification.")] + [InlineData("\n")] + [InlineData("\r\n")] + public async Task CompletionWithDifferentLineEndingsAsync(string lineEnding) + { + // + var prompt = + "Given a json input and a request. Apply the request on the json input and return the result. " + + $"Put the result in between tags{lineEnding}" + + $$"""Input:{{lineEnding}}{"name": "John", "age": 30}{{lineEnding}}{{lineEnding}}Request:{{lineEnding}}name"""; + + var kernel = this.CreateAndInitializeKernel(); + + var plugins = TestHelpers.ImportSamplePlugins(kernel, "ChatPlugin"); + + // Act + FunctionResult actual = await kernel.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); + + // Assert + Assert.Contains("John", actual.GetValue(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task ChatSystemPromptIsNotIgnoredAsync() + { + // + var kernel = this.CreateAndInitializeKernel(); + + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await kernel.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?", new(settings)); + + // Assert + Assert.Contains("I don't know", result.ToString(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task SemanticKernelVersionHeaderIsSentAsync() + { + // + using var defaultHandler = new HttpClientHandler(); + using var httpHeaderHandler = new HttpHeaderHandler(defaultHandler); + using var httpClient = new HttpClient(httpHeaderHandler); + + var kernel = this.CreateAndInitializeKernel(httpClient); + + // Act + var result = await kernel.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?"); + + // Assert + Assert.NotNull(httpHeaderHandler.RequestHeaders); + Assert.True(httpHeaderHandler.RequestHeaders.TryGetValues("Semantic-Kernel-Version", out var values)); + } + + //[Theory(Skip = "This test is for manual verification.")] + [Theory] + [InlineData(null, null)] + [InlineData(false, null)] + [InlineData(true, 2)] + [InlineData(true, 5)] + public async Task LogProbsDataIsReturnedWhenRequestedAsync(bool? logprobs, int? topLogprobs) + { + // + var settings = new OpenAIPromptExecutionSettings { Logprobs = logprobs, TopLogprobs = topLogprobs }; + + var kernel = this.CreateAndInitializeKernel(); + + // Act + var result = await kernel.InvokePromptAsync("Hi, can you help me today?", new(settings)); + + var logProbabilityInfo = result.Metadata?["LogProbabilityInfo"] as IReadOnlyList; + + // Assert + Assert.NotNull(logProbabilityInfo); + + if (logprobs is true) + { + Assert.NotNull(logProbabilityInfo); + Assert.Equal(topLogprobs, logProbabilityInfo[0].TopLogProbabilities.Count); + } + else + { + Assert.Empty(logProbabilityInfo); + } + } + + #region internals + + private Kernel CreateAndInitializeKernel(HttpClient? httpClient = null) + { + var OpenAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(OpenAIConfiguration); + Assert.NotNull(OpenAIConfiguration.ChatModelId); + Assert.NotNull(OpenAIConfiguration.ApiKey); + Assert.NotNull(OpenAIConfiguration.ServiceId); + + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddOpenAIChatCompletion( + modelId: OpenAIConfiguration.ChatModelId, + apiKey: OpenAIConfiguration.ApiKey, + serviceId: OpenAIConfiguration.ServiceId, + httpClient: httpClient); + + return kernelBuilder.Build(); + } + + private const string InputParameterName = "input"; + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + private sealed class HttpHeaderHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) + { + public System.Net.Http.Headers.HttpRequestHeaders? RequestHeaders { get; private set; } + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + this.RequestHeaders = request.Headers; + return await base.SendAsync(request, cancellationToken); + } + } + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs new file mode 100644 index 000000000000..62267c6eb691 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs @@ -0,0 +1,777 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; + +public sealed class OpenAIChatCompletionFunctionCallingTests : BaseIntegrationTest +{ + [Fact] + public async Task CanAutoInvokeKernelFunctionsAsync() + { + // Arrange + var invokedFunctions = new List(); + + var filter = new FakeFunctionFilter(async (context, next) => + { + invokedFunctions.Add($"{context.Function.Name}({string.Join(", ", context.Arguments)})"); + await next(context); + }); + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.FunctionInvocationFilters.Add(filter); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); + + // Assert + Assert.Contains("rain", result.GetValue(), StringComparison.InvariantCulture); + Assert.Contains("GetCurrentUtcTime()", invokedFunctions); + Assert.Contains("Get_Weather_For_City([cityName, Boston])", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsStreamingAsync() + { + // Arrange + var invokedFunctions = new List(); + + var filter = new FakeFunctionFilter(async (context, next) => + { + invokedFunctions.Add($"{context.Function.Name}({string.Join(", ", context.Arguments)})"); + await next(context); + }); + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.FunctionInvocationFilters.Add(filter); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in kernel.InvokePromptStreamingAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings))) + { + stringBuilder.Append(update); + } + + // Assert + Assert.Contains("rain", stringBuilder.ToString(), StringComparison.InvariantCulture); + Assert.Contains("GetCurrentUtcTime()", invokedFunctions); + Assert.Contains("Get_Weather_For_City([cityName, Boston])", invokedFunctions); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("42.8", result.GetValue(), StringComparison.InvariantCulture); // The WeatherPlugin always returns 42.8 for Dublin, Ireland. + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("10", result.GetValue(), StringComparison.InvariantCulture); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionsWithEnumTypeParametersAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("rain", result.GetValue(), StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var promptFunction = KernelFunctionFactory.CreateFromPrompt( + "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", + functionName: "FindLatestNews", + description: "Searches for the latest news."); + + kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( + "NewsProvider", + "Delivers up-to-date news content.", + [promptFunction])); + + OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); + + var builder = new StringBuilder(); + + await foreach (var update in streamingResult) + { + builder.Append(update.ToString()); + } + + var result = builder.ToString(); + + // Assert + Assert.NotNull(result); + Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Current way of handling function calls manually using connector specific chat message content class. + var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + + while (toolCalls.Count > 0) + { + // Adding LLM function call request to chat history + chatHistory.Add(result); + + // Iterating over the requested function calls and invoking them + foreach (var toolCall in toolCalls) + { + string content = kernel.Plugins.TryGetOpenAIFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? + JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : + "Unable to find function. Please try again!"; + + // Adding the result of the function call to the chat history + chatHistory.Add(new ChatMessageContent( + AuthorRole.Tool, + content, + metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + } + + // Sending the functions invocation results back to the LLM to get the final response + result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + } + + // Assert + Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(result.ToChatMessage()); + } + + // Sending the functions invocation results to the LLM to get the final response + messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("Add the \"Error\" keyword to the response, if you are unable to answer a question or an error has happen."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length != 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); + } + + // Sending the functions execution results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.NotNull(messageContent.Content); + + Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + + while (functionCalls.Length > 0) + { + // Adding function call from LLM to chat history + chatHistory.Add(messageContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + var result = await functionCall.InvokeAsync(kernel); + + chatHistory.AddMessage(AuthorRole.Tool, [result]); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + messageContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + // Sending the functions invocation results back to the LLM to get the final response + messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); + } + + // Assert + Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ItFailsIfNoFunctionResultProvidedAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var completionService = kernel.GetRequiredService(); + + // Act + var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(result); + + var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); + + // Assert + Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + var functionResult = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(functionResult.ToChatMessage()); + } + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("rain", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var result = new StringBuilder(); + + // Act + await foreach (var contentUpdate in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + result.Append(contentUpdate.Content); + } + + // Assert + Assert.Equal(5, chatHistory.Count); + + var userMessage = chatHistory[0]; + Assert.Equal(AuthorRole.User, userMessage.Role); + + // LLM requested the current time. + var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + + var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); + Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); + Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); + Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); + Assert.NotNull(getCurrentTimeFunctionCallResult.Result); + + // LLM requested the weather for Boston. + var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); + + var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); + Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. + + var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); + Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); + Assert.NotNull(getWeatherForCityFunctionCallResult.Result); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("Add the \"Error\" keyword to the response, if you are unable to answer a question or an error has happen."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + // Simulating an exception + var exception = new OperationCanceledException("The operation was canceled due to timeout."); + + chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); + } + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("error", result, StringComparison.InvariantCultureIgnoreCase); + } + + [Fact] + public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsForStreamingAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + var chatHistory = new ChatHistory(); + chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + string? result = null; + + // Act + while (true) + { + AuthorRole? authorRole = null; + var fccBuilder = new FunctionCallContentBuilder(); + var textContent = new StringBuilder(); + + await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) + { + textContent.Append(streamingContent.Content); + authorRole ??= streamingContent.Role; + fccBuilder.Append(streamingContent); + } + + var functionCalls = fccBuilder.Build(); + if (functionCalls.Any()) + { + var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); + chatHistory.Add(fcContent); + + // Iterating over the requested function calls and invoking them + foreach (var functionCall in functionCalls) + { + fcContent.Items.Add(functionCall); + + var functionResult = await functionCall.InvokeAsync(kernel); + + chatHistory.Add(functionResult.ToChatMessage()); + } + + // Adding a simulated function call to the connector response message + var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); + fcContent.Items.Add(simulatedFunctionCall); + + // Adding a simulated function result to chat history + var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; + chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); + + continue; + } + + result = textContent.ToString(); + break; + } + + // Assert + Assert.Contains("tornado", result, StringComparison.InvariantCultureIgnoreCase); + } + + private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) + { + var OpenAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(OpenAIConfiguration); + Assert.NotNull(OpenAIConfiguration.ChatModelId!); + Assert.NotNull(OpenAIConfiguration.ApiKey); + + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddOpenAIChatCompletion( + modelId: OpenAIConfiguration.ChatModelId, + apiKey: OpenAIConfiguration.ApiKey); + + var kernel = kernelBuilder.Build(); + + if (importHelperPlugin) + { + kernel.ImportPluginFromFunctions("HelperFunctions", + [ + kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), + kernel.CreateFunctionFromMethod((string cityName) => + { + return cityName switch + { + "Boston" => "61 and rainy", + _ => "31 and snowing", + }; + }, "Get_Weather_For_City", "Gets the current weather for the specified city"), + kernel.CreateFunctionFromMethod((WeatherParameters parameters) => + { + if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) + { + return Task.FromResult(42.8); // 42.8 Fahrenheit. + } + + throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); + }, "Get_Current_Temperature", "Get current temperature."), + kernel.CreateFunctionFromMethod((double temperatureInFahrenheit) => + { + double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; + return Task.FromResult(temperatureInCelsius); + }, "Convert_Temperature_From_Fahrenheit_To_Celsius", "Convert temperature from Fahrenheit to Celsius.") + ]); + } + + return kernel; + } + + public record WeatherParameters(City City); + + public class City + { + public string Name { get; set; } = string.Empty; + public string Country { get; set; } = string.Empty; + } + + private sealed class FakeFunctionFilter : IFunctionInvocationFilter + { + private readonly Func, Task>? _onFunctionInvocation; + + public FakeFunctionFilter( + Func, Task>? onFunctionInvocation = null) + { + this._onFunctionInvocation = onFunctionInvocation; + } + + public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) => + this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs new file mode 100644 index 000000000000..3314ee944bbd --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.TextGeneration; +using OpenAI.Chat; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class OpenAIChatCompletionNonStreamingTests : BaseIntegrationTest +{ + [Fact] + public async Task ChatCompletionShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await chatCompletion.GetChatMessageContentAsync("What is the capital of France?", settings, kernel); + + // Assert + Assert.Contains("I don't know", result.Content); + } + + [Fact] + public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var chatHistory = new ChatHistory("Reply \"I don't know\" to every question."); + chatHistory.AddUserMessage("What is the capital of France?"); + + // Act + var result = await chatCompletion.GetChatMessageContentAsync(chatHistory, null, kernel); + + // Assert + Assert.Contains("I don't know", result.Content); + Assert.NotNull(result.Metadata); + + Assert.True(result.Metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); + + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + + Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + + Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.Empty((logProbabilityInfo as IReadOnlyList)!); + } + + [Fact] + public async Task TextGenerationShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + // Act + var result = await textGeneration.GetTextContentAsync("What is the capital of France?", settings, kernel); + + // Assert + Assert.Contains("I don't know", result.Text); + } + + [Fact] + public async Task TextGenerationShouldReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + // Act + var result = await textGeneration.GetTextContentAsync("Reply \"I don't know\" to every question. What is the capital of France?", null, kernel); + + // Assert + Assert.Contains("I don't know", result.Text); + Assert.NotNull(result.Metadata); + + Assert.True(result.Metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); + + Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); + Assert.NotNull(usageObject); + + var jsonObject = JsonSerializer.SerializeToElement(usageObject); + Assert.True(jsonObject.TryGetProperty("InputTokens", out JsonElement promptTokensJson)); + Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); + Assert.NotEqual(0, promptTokens); + + Assert.True(jsonObject.TryGetProperty("OutputTokens", out JsonElement completionTokensJson)); + Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); + Assert.NotEqual(0, completionTokens); + + Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + + Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.Empty((logProbabilityInfo as IReadOnlyList)!); + } + + #region internals + + private Kernel CreateAndInitializeKernel() + { + var OpenAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(OpenAIConfiguration); + Assert.NotNull(OpenAIConfiguration.ChatModelId!); + Assert.NotNull(OpenAIConfiguration.ApiKey); + + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddOpenAIChatCompletion( + modelId: OpenAIConfiguration.ChatModelId, + apiKey: OpenAIConfiguration.ApiKey); + + return kernelBuilder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs new file mode 100644 index 000000000000..5a3145b5881f --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs @@ -0,0 +1,177 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.TextGeneration; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +public sealed class OpenAIChatCompletionStreamingTests : BaseIntegrationTest +{ + [Fact] + public async Task ChatCompletionShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in chatCompletion.GetStreamingChatMessageContentsAsync("What is the capital of France?", settings, kernel)) + { + stringBuilder.Append(update.Content); + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + } + + [Fact] + public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var chatCompletion = kernel.Services.GetRequiredService(); + + var chatHistory = new ChatHistory("Reply \"I don't know\" to every question."); + chatHistory.AddUserMessage("What is the capital of France?"); + + var stringBuilder = new StringBuilder(); + var metadata = new Dictionary(); + + // Act + await foreach (var update in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, null, kernel)) + { + stringBuilder.Append(update.Content); + + foreach (var key in update.Metadata!.Keys) + { + if (!metadata.TryGetValue(key, out var value) || value is null) + { + metadata[key] = update.Metadata[key]; + } + } + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + Assert.NotNull(metadata); + + Assert.True(metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(metadata.ContainsKey("SystemFingerprint")); + + Assert.True(metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + } + + [Fact] + public async Task TextGenerationShouldUseChatSystemPromptAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; + + var stringBuilder = new StringBuilder(); + + // Act + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("What is the capital of France?", settings, kernel)) + { + stringBuilder.Append(update); + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + } + + [Fact] + public async Task TextGenerationShouldReturnMetadataAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(); + + var textGeneration = kernel.Services.GetRequiredService(); + + // Act + var stringBuilder = new StringBuilder(); + var metadata = new Dictionary(); + + // Act + await foreach (var update in textGeneration.GetStreamingTextContentsAsync("Reply \"I don't know\" to every question. What is the capital of France?", null, kernel)) + { + stringBuilder.Append(update); + + foreach (var key in update.Metadata!.Keys) + { + if (!metadata.TryGetValue(key, out var value) || value is null) + { + metadata[key] = update.Metadata[key]; + } + } + } + + // Assert + Assert.Contains("I don't know", stringBuilder.ToString()); + Assert.NotNull(metadata); + + Assert.True(metadata.TryGetValue("Id", out object? id)); + Assert.NotNull(id); + + Assert.True(metadata.TryGetValue("CreatedAt", out object? createdAt)); + Assert.NotNull(createdAt); + + Assert.True(metadata.ContainsKey("SystemFingerprint")); + + Assert.True(metadata.TryGetValue("FinishReason", out object? finishReason)); + Assert.Equal("Stop", finishReason); + } + + #region internals + + private Kernel CreateAndInitializeKernel() + { + var OpenAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + Assert.NotNull(OpenAIConfiguration); + Assert.NotNull(OpenAIConfiguration.ChatModelId!); + Assert.NotNull(OpenAIConfiguration.ApiKey); + + var kernelBuilder = base.CreateKernelBuilder(); + + kernelBuilder.AddOpenAIChatCompletion( + modelId: OpenAIConfiguration.ChatModelId, + apiKey: OpenAIConfiguration.ApiKey); + + return kernelBuilder.Build(); + } + + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + #endregion +} diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs b/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs index 3425d187e4fd..ecd4f18a0c90 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs @@ -131,6 +131,8 @@ public static bool IsModelDiagnosticsEnabled() /// public static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); + internal static bool HasListeners() => s_activitySource.HasListeners(); + #region Private private static void AddOptionalTags(Activity? activity, TPromptExecutionSettings? executionSettings) where TPromptExecutionSettings : PromptExecutionSettings From f7e7e29832da244ad4ed243d8f7543889e50d559 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 10 Jul 2024 19:39:11 +0100 Subject: [PATCH 065/226] .Net: OpenAI V2 - FileService Obsolescence (#7184) ### Motivation and Context - Obsolescence of FileService. --- dotnet/Directory.Packages.props | 2 +- .../KernelBuilderExtensionsTests.cs | 2 + .../OpenAIFileUploadExecutionSettingsTests.cs | 24 -- .../ServiceCollectionExtensionsTests.cs | 2 + .../Models/OpenAIFileReferenceTests.cs | 24 -- .../Services/OpenAIFileServiceTests.cs | 280 ++++++++---------- .../Core/ClientCore.File.cs | 124 -------- .../OpenAIKernelBuilderExtensions.cs | 2 + .../OpenAIServiceCollectionExtensions.cs | 2 + .../Models/OpenAIFilePurpose.cs | 91 +++++- .../Models/OpenAIFileReference.cs | 2 + .../Services/OpenAIFileService.cs | 250 ++++++++++++++-- .../OpenAIFileUploadExecutionSettings.cs | 5 +- .../OpenAI/OpenAIFileServiceTests.cs | 147 +++++++++ .../IntegrationTestsV2.csproj | 1 + .../TestData/test_content.txt | 9 + .../TestData/test_image_001.jpg | Bin 0 -> 61082 bytes 17 files changed, 615 insertions(+), 352 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs create mode 100644 dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs create mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_content.txt create mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index bb4233ad6ba9..69288dfcdf4b 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -36,7 +36,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs index 869e82362282..c57c7954f0b8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; @@ -138,6 +139,7 @@ public void ItCanAddAudioToTextServiceWithOpenAIClient() } [Fact] + [Obsolete("This test is deprecated and will be removed in a future version.")] public void ItCanAddFileService() { // Arrange diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs deleted file mode 100644 index 8e4ffa622ca8..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIFileUploadExecutionSettingsTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Extensions; - -public class OpenAIFileUploadExecutionSettingsTests -{ - [Fact] - public void ItCanCreateOpenAIFileUploadExecutionSettings() - { - // Arrange - var fileName = "file.txt"; - var purpose = OpenAIFilePurpose.FineTune; - - // Act - var settings = new OpenAIFileUploadExecutionSettings(fileName, purpose); - - // Assert - Assert.Equal(fileName, settings.FileName); - Assert.Equal(purpose, settings.Purpose); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 3e7767d33e24..524d6c1ce8a4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.AudioToText; @@ -171,6 +172,7 @@ public void ItCanAddAudioToTextServiceWithOpenAIClient() } [Fact] + [Obsolete("This test is deprecated and will be removed in a future version.")] public void ItCanAddFileService() { // Arrange diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs deleted file mode 100644 index 26dd596fa49b..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Models/OpenAIFileReferenceTests.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Models; - -public sealed class OpenAIFileReferenceTests -{ - [Fact] - public void CanBeInstantiated() - { - // Arrange - var fileReference = new OpenAIFileReference - { - CreatedTimestamp = DateTime.UtcNow, - FileName = "test.txt", - Id = "123", - Purpose = OpenAIFilePurpose.Assistants, - SizeInBytes = 100 - }; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs index 4cf27cd9ee2a..c763e729e381 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs @@ -12,11 +12,12 @@ using Moq; using Xunit; -namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; +namespace SemanticKernel.Connectors.OpenAI.UnitTests.Files; /// /// Unit tests for class. /// +[Obsolete("This class is deprecated and will be removed in a future version.")] public sealed class OpenAIFileServiceTests : IDisposable { private readonly HttpMessageHandlerStub _messageHandlerStub; @@ -39,109 +40,113 @@ public void ConstructorWorksCorrectlyForOpenAI(bool includeLoggerFactory) var service = includeLoggerFactory ? new OpenAIFileService("api-key", loggerFactory: this._mockLoggerFactory.Object) : new OpenAIFileService("api-key"); + + // Assert + Assert.NotNull(service); } [Theory] [InlineData(true)] [InlineData(false)] - public void ConstructorWorksCorrectlyForCustomEndpoint(bool includeLoggerFactory) + public void ConstructorWorksCorrectlyForAzure(bool includeLoggerFactory) { // Arrange & Act var service = includeLoggerFactory ? new OpenAIFileService(new Uri("http://localhost"), "api-key", loggerFactory: this._mockLoggerFactory.Object) : new OpenAIFileService(new Uri("http://localhost"), "api-key"); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task DeleteFileWorksCorrectlyAsync(bool isCustomEndpoint) - { - // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "test.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - - this._messageHandlerStub.ResponseToReturn = response; - // Act & Assert - await service.DeleteFileAsync("file-id"); + // Assert + Assert.NotNull(service); } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task DeleteFileFailsAsExpectedAsync(bool isCustomEndpoint) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task DeleteFileWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) { // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateFailedResponse(); - - this._messageHandlerStub.ResponseToReturn = response; - - // Act & Assert - await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id")); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetFileWorksCorrectlyAsync(bool isCustomEndpoint) - { - // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "file.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - + var service = this.CreateFileService(isAzure); + using var response = + isFailedRequest ? + this.CreateFailedResponse() : + this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "test.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); this._messageHandlerStub.ResponseToReturn = response; // Act & Assert - var file = await service.GetFileAsync("file-id"); - Assert.NotNull(file); - Assert.NotEqual(string.Empty, file.Id); - Assert.NotEqual(string.Empty, file.FileName); - Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); - Assert.NotEqual(0, file.SizeInBytes); + if (isFailedRequest) + { + await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id")); + } + else + { + await service.DeleteFileAsync("file-id"); + } } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetFileFailsAsExpectedAsync(bool isCustomEndpoint) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task GetFileWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) { // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateFailedResponse(); + var service = this.CreateFileService(isAzure); + using var response = + isFailedRequest ? + this.CreateFailedResponse() : + this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "file.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); this._messageHandlerStub.ResponseToReturn = response; // Act & Assert - await Assert.ThrowsAsync(() => service.GetFileAsync("file-id")); + if (isFailedRequest) + { + await Assert.ThrowsAsync(() => service.GetFileAsync("file-id")); + } + else + { + var file = await service.GetFileAsync("file-id"); + Assert.NotNull(file); + Assert.NotEqual(string.Empty, file.Id); + Assert.NotEqual(string.Empty, file.FileName); + Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); + Assert.NotEqual(0, file.SizeInBytes); + } } [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetFilesWorksCorrectlyAsync(bool isCustomEndpoint) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task GetFilesWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) { // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateSuccessResponse( + var service = this.CreateFileService(isAzure); + using var response = + isFailedRequest ? + this.CreateFailedResponse() : + this.CreateSuccessResponse( """ { "data": [ @@ -162,37 +167,29 @@ public async Task GetFilesWorksCorrectlyAsync(bool isCustomEndpoint) ] } """); - this._messageHandlerStub.ResponseToReturn = response; // Act & Assert - var files = (await service.GetFilesAsync()).ToArray(); - Assert.NotNull(files); - Assert.NotEmpty(files); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetFilesFailsAsExpectedAsync(bool isCustomEndpoint) - { - // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateFailedResponse(); - - this._messageHandlerStub.ResponseToReturn = response; - - await Assert.ThrowsAsync(() => service.GetFilesAsync()); + if (isFailedRequest) + { + await Assert.ThrowsAsync(() => service.GetFilesAsync()); + } + else + { + var files = (await service.GetFilesAsync()).ToArray(); + Assert.NotNull(files); + Assert.NotEmpty(files); + } } [Theory] [InlineData(true)] [InlineData(false)] - public async Task GetFileContentWorksCorrectlyAsync(bool isCustomEndpoint) + public async Task GetFileContentWorksCorrectlyAsync(bool isAzure) { // Arrange var data = BinaryData.FromString("Hello AI!"); - var service = this.CreateFileService(isCustomEndpoint); + var service = this.CreateFileService(isAzure); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { @@ -206,81 +203,62 @@ public async Task GetFileContentWorksCorrectlyAsync(bool isCustomEndpoint) } [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task UploadContentWorksCorrectlyAsync(bool isCustomEndpoint) + [InlineData(true, true)] + [InlineData(false, true)] + [InlineData(true, false)] + [InlineData(false, false)] + public async Task UploadContentWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) { // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "test.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - - this._messageHandlerStub.ResponseToReturn = response; - - var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); - - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - await writer.WriteLineAsync("test"); - await writer.FlushAsync(); - - stream.Position = 0; - - var content = new BinaryContent(stream.ToArray(), "text/plain"); - - // Act & Assert - var file = await service.UploadContentAsync(content, settings); - Assert.NotNull(file); - Assert.NotEqual(string.Empty, file.Id); - Assert.NotEqual(string.Empty, file.FileName); - Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); - Assert.NotEqual(0, file.SizeInBytes); - - writer.Dispose(); - stream.Dispose(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task UploadContentFailsAsExpectedAsync(bool isCustomEndpoint) - { - // Arrange - var service = this.CreateFileService(isCustomEndpoint); - using var response = this.CreateFailedResponse(); - + var service = this.CreateFileService(isAzure); + using var response = + isFailedRequest ? + this.CreateFailedResponse() : + this.CreateSuccessResponse( + """ + { + "id": "123", + "filename": "test.txt", + "purpose": "assistants", + "bytes": 120000, + "created_at": 1677610602 + } + """); this._messageHandlerStub.ResponseToReturn = response; var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); - var stream = new MemoryStream(); - var writer = new StreamWriter(stream); - await writer.WriteLineAsync("test"); - await writer.FlushAsync(); + await using var stream = new MemoryStream(); + await using (var writer = new StreamWriter(stream, leaveOpen: true)) + { + await writer.WriteLineAsync("test"); + await writer.FlushAsync(); + } stream.Position = 0; var content = new BinaryContent(stream.ToArray(), "text/plain"); // Act & Assert - await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings)); - - writer.Dispose(); - stream.Dispose(); + if (isFailedRequest) + { + await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings)); + } + else + { + var file = await service.UploadContentAsync(content, settings); + Assert.NotNull(file); + Assert.NotEqual(string.Empty, file.Id); + Assert.NotEqual(string.Empty, file.FileName); + Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); + Assert.NotEqual(0, file.SizeInBytes); + } } - private OpenAIFileService CreateFileService(bool isCustomEndpoint = false) + private OpenAIFileService CreateFileService(bool isAzure = false) { return - isCustomEndpoint ? + isAzure ? new OpenAIFileService(new Uri("http://localhost"), "api-key", httpClient: this._httpClient) : new OpenAIFileService("api-key", "organization", this._httpClient); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs deleted file mode 100644 index 41a9f470c4b0..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.File.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -/* -Phase 05 -- Ignoring the specific Purposes not implemented by current FileService. -*/ - -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using OpenAI.Files; - -using OAIFilePurpose = OpenAI.Files.OpenAIFilePurpose; -using SKFilePurpose = Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Uploads a file to OpenAI. - /// - /// File name - /// File content - /// Purpose of the file - /// Cancellation token - /// Uploaded file information - internal async Task UploadFileAsync( - string fileName, - Stream fileContent, - SKFilePurpose purpose, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().UploadFileAsync(fileContent, fileName, ConvertToOpenAIFilePurpose(purpose), cancellationToken)).ConfigureAwait(false); - return ConvertToFileReference(response.Value); - } - - /// - /// Delete a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - internal async Task DeleteFileAsync( - string fileId, - CancellationToken cancellationToken) - { - await RunRequestAsync(() => this.Client.GetFileClient().DeleteFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - } - - /// - /// Retrieve metadata for a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The metadata associated with the specified file identifier. - internal async Task GetFileAsync( - string fileId, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - return ConvertToFileReference(response.Value); - } - - /// - /// Retrieve metadata for all previously uploaded files. - /// - /// The to monitor for cancellation requests. The default is . - /// The metadata of all uploaded files. - internal async Task> GetFilesAsync(CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFilesAsync(cancellationToken: cancellationToken)).ConfigureAwait(false); - return response.Value.Select(ConvertToFileReference); - } - - /// - /// Retrieve the file content from a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The file content as - /// - /// Files uploaded with do not support content retrieval. - /// - internal async Task GetFileContentAsync( - string fileId, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().DownloadFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - return response.Value.ToArray(); - } - - private static OpenAIFileReference ConvertToFileReference(OpenAIFileInfo fileInfo) - => new() - { - Id = fileInfo.Id, - CreatedTimestamp = fileInfo.CreatedAt.DateTime, - FileName = fileInfo.Filename, - SizeInBytes = (int)(fileInfo.SizeInBytes ?? 0), - Purpose = ConvertToFilePurpose(fileInfo.Purpose), - }; - - private static FileUploadPurpose ConvertToOpenAIFilePurpose(SKFilePurpose purpose) - { - if (purpose == SKFilePurpose.Assistants) { return FileUploadPurpose.Assistants; } - if (purpose == SKFilePurpose.FineTune) { return FileUploadPurpose.FineTune; } - - throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); - } - - private static SKFilePurpose ConvertToFilePurpose(OAIFilePurpose purpose) - { - if (purpose == OAIFilePurpose.Assistants) { return SKFilePurpose.Assistants; } - if (purpose == OAIFilePurpose.FineTune) { return SKFilePurpose.FineTune; } - - throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index 9f7472f2eb51..309bebfb9cc5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -302,6 +302,8 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => /// The HttpClient to use with this service. /// The same instance as . [Experimental("SKEXP0010")] + [Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations.")] + [ExcludeFromCodeCoverage] public static IKernelBuilder AddOpenAIFiles( this IKernelBuilder builder, string apiKey, diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index 9227a2d484e0..d06dd65bba8d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -280,6 +280,8 @@ OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => /// A local identifier for the given AI service /// The same instance as . [Experimental("SKEXP0010")] + [Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations.")] + [ExcludeFromCodeCoverage] public static IServiceCollection AddOpenAIFiles( this IServiceCollection services, string apiKey, diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs index a01b2d08fa8d..523b84dbe333 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs @@ -1,22 +1,101 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// -/// Defines the purpose associated with the uploaded file. +/// Defines the purpose associated with the uploaded file: +/// https://platform.openai.com/docs/api-reference/files/object#files/object-purpose /// [Experimental("SKEXP0010")] -public enum OpenAIFilePurpose +[Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations. This class is deprecated and will be removed in a future version.")] +[ExcludeFromCodeCoverage] +public readonly struct OpenAIFilePurpose : IEquatable { /// - /// File to be used by assistants for model processing. + /// File to be used by assistants as input. /// - Assistants, + public static OpenAIFilePurpose Assistants { get; } = new("assistants"); /// - /// File to be used by fine-tuning jobs. + /// File produced as assistants output. /// - FineTune, + public static OpenAIFilePurpose AssistantsOutput { get; } = new("assistants_output"); + + /// + /// Files uploaded as a batch of API requests + /// + public static OpenAIFilePurpose Batch { get; } = new("batch"); + + /// + /// File produced as result of a file included as a batch request. + /// + public static OpenAIFilePurpose BatchOutput { get; } = new("batch_output"); + + /// + /// File to be used as input to fine-tune a model. + /// + public static OpenAIFilePurpose FineTune { get; } = new("fine-tune"); + + /// + /// File produced as result of fine-tuning a model. + /// + public static OpenAIFilePurpose FineTuneResults { get; } = new("fine-tune-results"); + + /// + /// File to be used for Assistants image file inputs. + /// + public static OpenAIFilePurpose Vision { get; } = new("vision"); + + /// + /// Gets the label associated with this . + /// + public string Label { get; } + + /// + /// Creates a new instance with the provided label. + /// + /// The label to associate with this . + public OpenAIFilePurpose(string label) + { + Verify.NotNullOrWhiteSpace(label, nameof(label)); + this.Label = label!; + } + + /// + /// Returns a value indicating whether two instances are equivalent, as determined by a + /// case-insensitive comparison of their labels. + /// + /// the first instance to compare + /// the second instance to compare + /// true if left and right are both null or have equivalent labels; false otherwise + public static bool operator ==(OpenAIFilePurpose left, OpenAIFilePurpose right) + => left.Equals(right); + + /// + /// Returns a value indicating whether two instances are not equivalent, as determined by a + /// case-insensitive comparison of their labels. + /// + /// the first instance to compare + /// the second instance to compare + /// false if left and right are both null or have equivalent labels; true otherwise + public static bool operator !=(OpenAIFilePurpose left, OpenAIFilePurpose right) + => !(left == right); + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + => obj is OpenAIFilePurpose otherPurpose && this == otherPurpose; + + /// + public bool Equals(OpenAIFilePurpose other) + => string.Equals(this.Label, other.Label, StringComparison.OrdinalIgnoreCase); + + /// + public override int GetHashCode() + => StringComparer.OrdinalIgnoreCase.GetHashCode(this.Label); + + /// + public override string ToString() => this.Label; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs index 371be0d93a33..e50a9185c20c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs @@ -9,6 +9,8 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// References an uploaded file by id. /// [Experimental("SKEXP0010")] +[Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations. This class is deprecated and will be removed in a future version.")] +[ExcludeFromCodeCoverage] public sealed class OpenAIFileReference { /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs index 4185f1237b15..2b7f1bde31d8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs @@ -4,10 +4,15 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Http; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -15,35 +20,57 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// File service access for OpenAI: https://api.openai.com/v1/files /// [Experimental("SKEXP0010")] +[Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations. This class is deprecated and will be removed in a future version.")] +[ExcludeFromCodeCoverage] public sealed class OpenAIFileService { - /// - /// OpenAI client for HTTP operations. - /// - private readonly ClientCore _client; + private const string OrganizationKey = "Organization"; + private const string HeaderNameAuthorization = "Authorization"; + private const string HeaderNameAzureApiKey = "api-key"; + private const string HeaderNameOpenAIAssistant = "OpenAI-Beta"; + private const string HeaderNameUserAgent = "User-Agent"; + private const string HeaderOpenAIValueAssistant = "assistants=v1"; + private const string OpenAIApiEndpoint = "https://api.openai.com/v1/"; + private const string OpenAIApiRouteFiles = "files"; + private const string AzureOpenAIApiRouteFiles = "openai/files"; + private const string AzureOpenAIDefaultVersion = "2024-02-15-preview"; + + private readonly string _apiKey; + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly Uri _serviceUri; + private readonly string? _version; + private readonly string? _organization; /// - /// Initializes a new instance of the class. + /// Create an instance of the Azure OpenAI chat completion connector /// - /// Non-default endpoint for the OpenAI API. - /// API Key + /// Azure Endpoint URL + /// Azure OpenAI API Key /// OpenAI Organization Id (usually optional) + /// The API version to target. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public OpenAIFileService( Uri endpoint, string apiKey, string? organization = null, + string? version = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { Verify.NotNull(apiKey, nameof(apiKey)); - this._client = new(null, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + this._apiKey = apiKey; + this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance; + this._httpClient = HttpClientProvider.GetHttpClient(httpClient); + this._serviceUri = new Uri(this._httpClient.BaseAddress ?? endpoint, AzureOpenAIApiRouteFiles); + this._version = version ?? AzureOpenAIDefaultVersion; + this._organization = organization; } /// - /// Initializes a new instance of the class. + /// Create an instance of the OpenAI chat completion connector /// /// OpenAI API Key /// OpenAI Organization Id (usually optional) @@ -57,7 +84,11 @@ public OpenAIFileService( { Verify.NotNull(apiKey, nameof(apiKey)); - this._client = new(null, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); + this._apiKey = apiKey; + this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance; + this._httpClient = HttpClientProvider.GetHttpClient(httpClient); + this._serviceUri = new Uri(this._httpClient.BaseAddress ?? new Uri(OpenAIApiEndpoint), OpenAIApiRouteFiles); + this._organization = organization; } /// @@ -65,11 +96,11 @@ public OpenAIFileService( /// /// The uploaded file identifier. /// The to monitor for cancellation requests. The default is . - public Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) + public async Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) { Verify.NotNull(id, nameof(id)); - return this._client.DeleteFileAsync(id, cancellationToken); + await this.ExecuteDeleteRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false); } /// @@ -84,10 +115,25 @@ public Task DeleteFileAsync(string id, CancellationToken cancellationToken = def public async Task GetFileContentAsync(string id, CancellationToken cancellationToken = default) { Verify.NotNull(id, nameof(id)); - var bytes = await this._client.GetFileContentAsync(id, cancellationToken).ConfigureAwait(false); + var contentUri = $"{this._serviceUri}/{id}/content"; + var (stream, mimetype) = await this.StreamGetRequestAsync(contentUri, cancellationToken).ConfigureAwait(false); - // The mime type of the downloaded file is not provided by the OpenAI API. - return new(bytes, null); + using (stream) + { + using var memoryStream = new MemoryStream(); +#if NETSTANDARD2_0 + const int DefaultCopyBufferSize = 81920; + await stream.CopyToAsync(memoryStream, DefaultCopyBufferSize, cancellationToken).ConfigureAwait(false); +#else + await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); +#endif + return + new(memoryStream.ToArray(), mimetype) + { + Metadata = new Dictionary() { { "id", id } }, + Uri = new Uri(contentUri), + }; + } } /// @@ -96,10 +142,13 @@ public async Task GetFileContentAsync(string id, CancellationToke /// The uploaded file identifier. /// The to monitor for cancellation requests. The default is . /// The metadata associated with the specified file identifier. - public Task GetFileAsync(string id, CancellationToken cancellationToken = default) + public async Task GetFileAsync(string id, CancellationToken cancellationToken = default) { Verify.NotNull(id, nameof(id)); - return this._client.GetFileAsync(id, cancellationToken); + + var result = await this.ExecuteGetRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false); + + return this.ConvertFileReference(result); } /// @@ -107,8 +156,22 @@ public Task GetFileAsync(string id, CancellationToken cance /// /// The to monitor for cancellation requests. The default is . /// The metadata of all uploaded files. - public async Task> GetFilesAsync(CancellationToken cancellationToken = default) - => await this._client.GetFilesAsync(cancellationToken).ConfigureAwait(false); + public Task> GetFilesAsync(CancellationToken cancellationToken = default) + => this.GetFilesAsync(null, cancellationToken); + + /// + /// Retrieve metadata for previously uploaded files + /// + /// The purpose of the files by which to filter. + /// The to monitor for cancellation requests. The default is . + /// The metadata of all uploaded files. + public async Task> GetFilesAsync(OpenAIFilePurpose? filePurpose, CancellationToken cancellationToken = default) + { + var serviceUri = filePurpose.HasValue && !string.IsNullOrEmpty(filePurpose.Value.Label) ? $"{this._serviceUri}?purpose={filePurpose}" : this._serviceUri.ToString(); + var result = await this.ExecuteGetRequestAsync(serviceUri, cancellationToken).ConfigureAwait(false); + + return result.Data.Select(this.ConvertFileReference).ToArray(); + } /// /// Upload a file. @@ -122,7 +185,152 @@ public async Task UploadContentAsync(BinaryContent fileCont Verify.NotNull(settings, nameof(settings)); Verify.NotNull(fileContent.Data, nameof(fileContent.Data)); - using var memoryStream = new MemoryStream(fileContent.Data.Value.ToArray()); - return await this._client.UploadFileAsync(settings.FileName, memoryStream, settings.Purpose, cancellationToken).ConfigureAwait(false); + using var formData = new MultipartFormDataContent(); + using var contentPurpose = new StringContent(settings.Purpose.Label); + using var contentFile = new ByteArrayContent(fileContent.Data.Value.ToArray()); + formData.Add(contentPurpose, "purpose"); + formData.Add(contentFile, "file", settings.FileName); + + var result = await this.ExecutePostRequestAsync(this._serviceUri.ToString(), formData, cancellationToken).ConfigureAwait(false); + + return this.ConvertFileReference(result); + } + + private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken) + { + using var request = HttpRequest.CreateDeleteRequest(this.PrepareUrl(url)); + this.AddRequestHeaders(request); + using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + } + + private async Task ExecuteGetRequestAsync(string url, CancellationToken cancellationToken) + { + using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url)); + this.AddRequestHeaders(request); + using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + + var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); + + var model = JsonSerializer.Deserialize(body); + + return + model ?? + throw new KernelException($"Unexpected response from {url}") + { + Data = { { "ResponseData", body } }, + }; + } + + private async Task<(Stream Stream, string? MimeType)> StreamGetRequestAsync(string url, CancellationToken cancellationToken) + { + using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url)); + this.AddRequestHeaders(request); + var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + try + { + return + (new HttpResponseStream( + await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false), + response), + response.Content.Headers.ContentType?.MediaType); + } + catch + { + response.Dispose(); + throw; + } + } + + private async Task ExecutePostRequestAsync(string url, HttpContent payload, CancellationToken cancellationToken) + { + using var request = new HttpRequestMessage(HttpMethod.Post, this.PrepareUrl(url)) { Content = payload }; + this.AddRequestHeaders(request); + using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); + + var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); + + var model = JsonSerializer.Deserialize(body); + + return + model ?? + throw new KernelException($"Unexpected response from {url}") + { + Data = { { "ResponseData", body } }, + }; + } + + private string PrepareUrl(string url) + { + if (string.IsNullOrWhiteSpace(this._version)) + { + return url; + } + + return $"{url}?api-version={this._version}"; + } + + private void AddRequestHeaders(HttpRequestMessage request) + { + request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); + request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent); + request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIFileService))); + + if (!string.IsNullOrWhiteSpace(this._version)) + { + // Azure OpenAI + request.Headers.Add(HeaderNameAzureApiKey, this._apiKey); + return; + } + + // OpenAI + request.Headers.Add(HeaderNameAuthorization, $"Bearer {this._apiKey}"); + + if (!string.IsNullOrEmpty(this._organization)) + { + this._httpClient.DefaultRequestHeaders.Add(OrganizationKey, this._organization); + } + } + + private OpenAIFileReference ConvertFileReference(FileInfo result) + { + return + new OpenAIFileReference + { + Id = result.Id, + FileName = result.FileName, + CreatedTimestamp = DateTimeOffset.FromUnixTimeSeconds(result.CreatedAt).UtcDateTime, + SizeInBytes = result.Bytes ?? 0, + Purpose = new(result.Purpose), + }; + } + + private sealed class FileInfoList + { + [JsonPropertyName("data")] + public FileInfo[] Data { get; set; } = []; + + [JsonPropertyName("object")] + public string Object { get; set; } = "list"; + } + + private sealed class FileInfo + { + [JsonPropertyName("id")] + public string Id { get; set; } = string.Empty; + + [JsonPropertyName("object")] + public string Object { get; set; } = "file"; + + [JsonPropertyName("bytes")] + public int? Bytes { get; set; } + + [JsonPropertyName("created_at")] + public long CreatedAt { get; set; } + + [JsonPropertyName("filename")] + public string FileName { get; set; } = string.Empty; + + [JsonPropertyName("purpose")] + public string Purpose { get; set; } = string.Empty; } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs index 3b49c1850df0..9412ea745fa3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs @@ -1,13 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Diagnostics.CodeAnalysis; namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// -/// Execution settings associated with Open AI file upload . +/// Execution serttings associated with Open AI file upload . /// [Experimental("SKEXP0010")] +[Obsolete("Use OpenAI SDK or AzureOpenAI SDK clients for file operations. This class is deprecated and will be removed in a future version.")] +[ExcludeFromCodeCoverage] public sealed class OpenAIFileUploadExecutionSettings { /// diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs new file mode 100644 index 000000000000..5e1f01055080 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using SemanticKernel.IntegrationTests.TestSettings; +using Xunit; + +namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; + +#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. + +[Obsolete("This class is deprecated and will be removed in a future version.")] +public sealed class OpenAIFileServiceTests +{ + private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() + .AddJsonFile(path: "testsettings.json", optional: true, reloadOnChange: true) + .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) + .AddEnvironmentVariables() + .AddUserSecrets() + .Build(); + + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData("test_image_001.jpg", "image/jpeg")] + [InlineData("test_content.txt", "text/plain")] + public async Task OpenAIFileServiceLifecycleAsync(string fileName, string mimeType) + { + // Arrange + OpenAIFileService fileService = this.CreateOpenAIFileService(); + + // Act & Assert + await this.VerifyFileServiceLifecycleAsync(fileService, fileName, mimeType); + } + + [Theory] + [InlineData("test_image_001.jpg", "image/jpeg")] + [InlineData("test_content.txt", "text/plain")] + public async Task AzureOpenAIFileServiceLifecycleAsync(string fileName, string mimeType) + { + // Arrange + OpenAIFileService fileService = this.CreateOpenAIFileService(); + + // Act & Assert + await this.VerifyFileServiceLifecycleAsync(fileService, fileName, mimeType); + } + + private async Task VerifyFileServiceLifecycleAsync(OpenAIFileService fileService, string fileName, string mimeType) + { + // Setup file content + await using FileStream fileStream = File.OpenRead($"./TestData/{fileName}"); + BinaryData sourceData = await BinaryData.FromStreamAsync(fileStream); + BinaryContent sourceContent = new(sourceData.ToArray(), mimeType); + + // Upload file with unsupported purpose (failure case) + await Assert.ThrowsAsync(() => fileService.UploadContentAsync(sourceContent, new(fileName, OpenAIFilePurpose.AssistantsOutput))); + + // Upload file with wacky purpose (failure case) + await Assert.ThrowsAsync(() => fileService.UploadContentAsync(sourceContent, new(fileName, new OpenAIFilePurpose("pretend")))); + + // Upload file + OpenAIFileReference fileReference = await fileService.UploadContentAsync(sourceContent, new(fileName, OpenAIFilePurpose.FineTune)); + try + { + AssertFileReferenceEquals(fileReference, fileName, sourceData.Length, OpenAIFilePurpose.FineTune); + + // Retrieve files by different purpose + Dictionary fileMap = await GetFilesAsync(fileService, OpenAIFilePurpose.Assistants); + Assert.DoesNotContain(fileReference.Id, fileMap.Keys); + + // Retrieve files by wacky purpose (failure case) + await Assert.ThrowsAsync(() => GetFilesAsync(fileService, new OpenAIFilePurpose("pretend"))); + + // Retrieve files by expected purpose + fileMap = await GetFilesAsync(fileService, OpenAIFilePurpose.FineTune); + Assert.Contains(fileReference.Id, fileMap.Keys); + AssertFileReferenceEquals(fileMap[fileReference.Id], fileName, sourceData.Length, OpenAIFilePurpose.FineTune); + + // Retrieve files by no specific purpose + fileMap = await GetFilesAsync(fileService); + Assert.Contains(fileReference.Id, fileMap.Keys); + AssertFileReferenceEquals(fileMap[fileReference.Id], fileName, sourceData.Length, OpenAIFilePurpose.FineTune); + + // Retrieve file by id + OpenAIFileReference file = await fileService.GetFileAsync(fileReference.Id); + AssertFileReferenceEquals(file, fileName, sourceData.Length, OpenAIFilePurpose.FineTune); + + // Retrieve file content + BinaryContent retrievedContent = await fileService.GetFileContentAsync(fileReference.Id); + Assert.NotNull(retrievedContent.Data); + Assert.NotNull(retrievedContent.Uri); + Assert.NotNull(retrievedContent.Metadata); + Assert.Equal(fileReference.Id, retrievedContent.Metadata["id"]); + Assert.Equal(sourceContent.Data!.Value.Length, retrievedContent.Data.Value.Length); + } + finally + { + // Delete file + await fileService.DeleteFileAsync(fileReference.Id); + } + } + + private static void AssertFileReferenceEquals(OpenAIFileReference fileReference, string expectedFileName, int expectedSize, OpenAIFilePurpose expectedPurpose) + { + Assert.Equal(expectedFileName, fileReference.FileName); + Assert.Equal(expectedPurpose, fileReference.Purpose); + Assert.Equal(expectedSize, fileReference.SizeInBytes); + } + + private static async Task> GetFilesAsync(OpenAIFileService fileService, OpenAIFilePurpose? purpose = null) + { + IEnumerable files = await fileService.GetFilesAsync(purpose); + Dictionary fileIds = files.DistinctBy(f => f.Id).ToDictionary(f => f.Id); + return fileIds; + } + + #region internals + + private OpenAIFileService CreateOpenAIFileService() + { + var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); + + Assert.NotNull(openAIConfiguration); + Assert.NotNull(openAIConfiguration.ApiKey); + Assert.NotNull(openAIConfiguration.ServiceId); + + return new(openAIConfiguration.ApiKey, openAIConfiguration.ServiceId); + } + + private OpenAIFileService CreateAzureOpenAIFileService() + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + + Assert.NotNull(azureOpenAIConfiguration); + Assert.NotNull(azureOpenAIConfiguration.Endpoint); + Assert.NotNull(azureOpenAIConfiguration.ApiKey); + Assert.NotNull(azureOpenAIConfiguration.ServiceId); + + return new(new Uri(azureOpenAIConfiguration.Endpoint), azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.ServiceId); + } + + #endregion +} diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj index 13bcc5ba0f44..3d564cd8aad2 100644 --- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj +++ b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj @@ -28,6 +28,7 @@ + diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_content.txt b/dotnet/src/IntegrationTestsV2/TestData/test_content.txt new file mode 100644 index 000000000000..447ce0649e56 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/TestData/test_content.txt @@ -0,0 +1,9 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Amet dictum sit amet justo donec enim diam vulputate ut. Nibh ipsum consequat nisl vel pretium lectus. Urna nec tincidunt praesent semper feugiat. Tristique nulla aliquet enim tortor. Ut morbi tincidunt augue interdum velit euismod in pellentesque massa. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra. Commodo ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis lectus nulla. Sem nulla pharetra diam sit amet nisl. Viverra aliquet eget sit amet tellus cras adipiscing enim eu. + +Morbi blandit cursus risus at ultrices mi tempus. Sagittis orci a scelerisque purus. Iaculis nunc sed augue lacus viverra. Accumsan sit amet nulla facilisi morbi tempus iaculis. Nisl rhoncus mattis rhoncus urna neque. Commodo odio aenean sed adipiscing diam donec adipiscing tristique. Tristique senectus et netus et malesuada fames. Nascetur ridiculus mus mauris vitae ultricies leo integer. Ut sem viverra aliquet eget. Sed egestas egestas fringilla phasellus faucibus scelerisque. + +In tellus integer feugiat scelerisque varius morbi. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Cum sociis natoque penatibus et magnis dis. Iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui. Praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus. Consectetur a erat nam at lectus urna. Hac habitasse platea dictumst vestibulum rhoncus est pellentesque elit. Aliquam vestibulum morbi blandit cursus risus at ultrices. Eu non diam phasellus vestibulum lorem sed. Risus pretium quam vulputate dignissim suspendisse in est. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. At varius vel pharetra vel turpis nunc eget. Aliquam malesuada bibendum arcu vitae. At consectetur lorem donec massa. Mi sit amet mauris commodo. Maecenas volutpat blandit aliquam etiam erat velit. Nullam ac tortor vitae purus faucibus ornare suspendisse. + +Facilisi nullam vehicula ipsum a arcu cursus vitae. Commodo sed egestas egestas fringilla phasellus. Lacus luctus accumsan tortor posuere ac ut consequat. Adipiscing commodo elit at imperdiet dui accumsan sit. Non tellus orci ac auctor augue. Viverra aliquet eget sit amet tellus. Luctus venenatis lectus magna fringilla urna porttitor rhoncus dolor. Mattis enim ut tellus elementum. Nunc sed id semper risus. At augue eget arcu dictum. + +Ullamcorper a lacus vestibulum sed arcu non. Vitae tortor condimentum lacinia quis vel. Dui faucibus in ornare quam viverra. Vel pharetra vel turpis nunc eget. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Lacus vestibulum sed arcu non odio euismod lacinia at quis. Augue mauris augue neque gravida in. Ornare quam viverra orci sagittis. Lacus suspendisse faucibus interdum posuere lorem ipsum. Arcu vitae elementum curabitur vitae nunc sed velit dignissim. Diam quam nulla porttitor massa id neque. Gravida dictum fusce ut placerat orci nulla pellentesque. Mus mauris vitae ultricies leo integer malesuada nunc vel risus. Donec pretium vulputate sapien nec sagittis aliquam. Velit egestas dui id ornare. Sed elementum tempus egestas sed sed risus pretium quam vulputate. \ No newline at end of file diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg b/dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a132825f9d641659e036ad6ebc9ac64abe7b192 GIT binary patch literal 61082 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 u=Z_`vzx?BYvS5Pa-52tZ+F+r7=vTm~>w#yfi Date: Thu, 11 Jul 2024 13:35:41 +0100 Subject: [PATCH 066/226] .Net: OpenAI V2 - Reverting all avoidable Breaking Changes - Phase 08 (#7203) ### Motivation and Context As a way to enforce the Non Breaking principle this PR remove all avoidable breaking changes from the V2 migration, including additions and small improvements identified. --- .../KernelBuilderExtensionsTests.cs | 32 +----- .../ServiceCollectionExtensionsTests.cs | 32 +----- .../Services/OpenAIAudioToTextServiceTests.cs | 7 +- .../Services/OpenAITextToAudioServiceTests.cs | 15 ++- .../Services/OpenAITextToImageServiceTests.cs | 27 ++--- .../OpenAIKernelBuilderExtensions.cs | 102 ++++-------------- .../OpenAIServiceCollectionExtensions.cs | 102 +++++------------- .../Services/OpenAIAudioToTextService.cs | 11 +- .../OpenAITextEmbbedingGenerationService.cs | 8 +- .../Services/OpenAITextToAudioService.cs | 21 +--- .../Services/OpenAITextToImageService.cs | 35 ++---- .../OpenAI/OpenAITextToImageTests.cs | 2 +- 12 files changed, 82 insertions(+), 312 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs index c57c7954f0b8..2c84068dc1b5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs @@ -55,22 +55,7 @@ public void ItCanAddTextToImageService() var sut = Kernel.CreateBuilder(); // Act - var service = sut.AddOpenAITextToImage("model", "key") - .Build() - .GetRequiredService(); - - // Assert - Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); - } - - [Fact] - public void ItCanAddTextToImageServiceWithOpenAIClient() - { - // Arrange - var sut = Kernel.CreateBuilder(); - - // Act - var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key")) + var service = sut.AddOpenAITextToImage("key", modelId: "model") .Build() .GetRequiredService(); @@ -93,21 +78,6 @@ public void ItCanAddTextToAudioService() Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } - [Fact] - public void ItCanAddTextToAudioServiceWithOpenAIClient() - { - // Arrange - var sut = Kernel.CreateBuilder(); - - // Act - var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key")) - .Build() - .GetRequiredService(); - - // Assert - Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); - } - [Fact] public void ItCanAddAudioToTextService() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs index 524d6c1ce8a4..f4b8ddf334e6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs @@ -88,22 +88,7 @@ public void ItCanAddImageToTextService() var sut = new ServiceCollection(); // Act - var service = sut.AddOpenAITextToImage("model", "key") - .BuildServiceProvider() - .GetRequiredService(); - - // Assert - Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); - } - - [Fact] - public void ItCanAddImageToTextServiceWithOpenAIClient() - { - // Arrange - var sut = new ServiceCollection(); - - // Act - var service = sut.AddOpenAITextToImage("model", new OpenAIClient("key")) + var service = sut.AddOpenAITextToImage("key", modelId: "model") .BuildServiceProvider() .GetRequiredService(); @@ -126,21 +111,6 @@ public void ItCanAddTextToAudioService() Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); } - [Fact] - public void ItCanAddTextToAudioServiceWithOpenAIClient() - { - // Arrange - var sut = new ServiceCollection(); - - // Act - var service = sut.AddOpenAITextToAudio("model", new OpenAIClient("key")) - .BuildServiceProvider() - .GetRequiredService(); - - // Assert - Assert.Equal("model", service.Attributes[AIServiceExtensions.ModelIdKey]); - } - [Fact] public void ItCanAddAudioToTextService() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs index 65be0cd4f384..660960697c21 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -43,7 +43,6 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) // Assert Assert.NotNull(service); Assert.Equal("model-id", service.Attributes["ModelId"]); - Assert.Equal("Organization", OpenAIAudioToTextService.OrganizationKey); } [Fact] @@ -115,7 +114,7 @@ public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] public async Task GetTextContentByDefaultWorksCorrectlyAsync() { // Arrange - var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", this._httpClient); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StringContent("Test audio-to-text response") @@ -133,7 +132,7 @@ public async Task GetTextContentByDefaultWorksCorrectlyAsync() public async Task GetTextContentThrowsIfAudioCantBeReadAsync() { // Arrange - var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", this._httpClient); // Act & Assert await Assert.ThrowsAsync(async () => { await service.GetTextContentsAsync(new AudioContent(new Uri("http://remote-audio")), new OpenAIAudioToTextExecutionSettings("file.mp3")); }); @@ -143,7 +142,7 @@ public async Task GetTextContentThrowsIfAudioCantBeReadAsync() public async Task GetTextContentThrowsIfFileNameIsInvalidAsync() { // Arrange - var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", this._httpClient); // Act & Assert await Assert.ThrowsAsync(async () => { await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("invalid")); }); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs index 0eca148eae8e..b77899b9a5b6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs @@ -51,11 +51,8 @@ public void ItThrowsIfModelIdIsNotProvided() { // Act & Assert Assert.Throws(() => new OpenAITextToAudioService(" ", "apikey")); - Assert.Throws(() => new OpenAITextToAudioService(" ", openAIClient: new("apikey"))); Assert.Throws(() => new OpenAITextToAudioService("", "apikey")); - Assert.Throws(() => new OpenAITextToAudioService("", openAIClient: new("apikey"))); Assert.Throws(() => new OpenAITextToAudioService(null!, "apikey")); - Assert.Throws(() => new OpenAITextToAudioService(null!, openAIClient: new("apikey"))); } [Theory] @@ -63,7 +60,7 @@ public void ItThrowsIfModelIdIsNotProvided() public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType) { // Arrange - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); using var stream = new MemoryStream([0x00, 0x00, 0xFF, 0x7F]); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) @@ -85,7 +82,7 @@ public async Task GetAudioContentByDefaultWorksCorrectlyAsync() // Arrange byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) @@ -113,7 +110,7 @@ public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string // Arrange byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) @@ -139,7 +136,7 @@ public async Task GetAudioContentThrowsWhenVoiceIsNotSupportedAsync() // Arrange byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); // Act & Assert await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice"))); @@ -151,7 +148,7 @@ public async Task GetAudioContentThrowsWhenFormatIsNotSupportedAsync() // Arrange byte[] expectedByteArray = [0x00, 0x00, 0xFF, 0x7F]; - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); // Act & Assert await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" })); @@ -170,7 +167,7 @@ public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAdd this._httpClient.BaseAddress = new Uri("http://local-endpoint"); } - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", null, this._httpClient); + var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); using var stream = new MemoryStream(expectedByteArray); this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs index c31c1f275dbc..08fb5c76c89f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -8,7 +8,6 @@ using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Services; using Moq; -using OpenAI; using Xunit; namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; @@ -39,7 +38,7 @@ public OpenAITextToImageServiceTests() public void ConstructorWorksCorrectly() { // Arrange & Act - var sut = new OpenAITextToImageService("model", "api-key", "organization"); + var sut = new OpenAITextToImageService("apikey", "organization", "model"); // Assert Assert.NotNull(sut); @@ -51,23 +50,9 @@ public void ConstructorWorksCorrectly() public void ItThrowsIfModelIdIsNotProvided() { // Act & Assert - Assert.Throws(() => new OpenAITextToImageService(" ", "apikey")); - Assert.Throws(() => new OpenAITextToImageService(" ", openAIClient: new("apikey"))); - Assert.Throws(() => new OpenAITextToImageService("", "apikey")); - Assert.Throws(() => new OpenAITextToImageService("", openAIClient: new("apikey"))); - Assert.Throws(() => new OpenAITextToImageService(null!, "apikey")); - Assert.Throws(() => new OpenAITextToImageService(null!, openAIClient: new("apikey"))); - } - - [Fact] - public void OpenAIClientConstructorWorksCorrectly() - { - // Arrange - var sut = new OpenAITextToImageService("model", new OpenAIClient("apikey")); - - // Assert - Assert.NotNull(sut); - Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); + Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: " ")); + Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: string.Empty)); + Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: null!)); } [Theory] @@ -82,7 +67,7 @@ public void OpenAIClientConstructorWorksCorrectly() public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) { // Arrange - var sut = new OpenAITextToImageService(modelId, "api-key", httpClient: this._httpClient); + var sut = new OpenAITextToImageService("api-key", modelId: modelId, httpClient: this._httpClient); Assert.Equal(modelId, sut.Attributes["ModelId"]); // Act @@ -103,7 +88,7 @@ public async Task GenerateImageDoesLogActionAsync() this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); // Arrange - var sut = new OpenAITextToImageService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); + var sut = new OpenAITextToImageService("apiKey", modelId: modelId, httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); // Act await sut.GenerateImageAsync("description", 256, 256); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs index 309bebfb9cc5..c713f3076ac3 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs @@ -19,6 +19,8 @@ using Microsoft.SemanticKernel.TextToImage; using OpenAI; +#pragma warning disable IDE0039 // Use local function + namespace Microsoft.SemanticKernel; /// @@ -35,7 +37,6 @@ public static class OpenAIKernelBuilderExtensions /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The HttpClient to use with this service. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. /// The same instance as . @@ -46,18 +47,18 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( string apiKey, string? orgId = null, string? serviceId = null, - Uri? endpoint = null, HttpClient? httpClient = null, int? dimensions = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextEmbeddingGenerationService( modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(httpClient, serviceProvider), serviceProvider.GetService(), dimensions)); @@ -83,6 +84,7 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( int? dimensions = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextEmbeddingGenerationService( @@ -97,60 +99,32 @@ public static IKernelBuilder AddOpenAITextEmbeddingGeneration( #region Text to Image /// - /// Adds the to the . - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAITextToImage( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(builder); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToImageService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds the to the . + /// Add the OpenAI Dall-E text to image service to the list /// /// The instance to augment. - /// The model to use for image generation. /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// The model to use for image generation. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The HttpClient to use with this service. /// The same instance as . [Experimental("SKEXP0010")] public static IKernelBuilder AddOpenAITextToImage( this IKernelBuilder builder, - string modelId, string apiKey, string? orgId = null, + string? modelId = null, string? serviceId = null, - Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(apiKey); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextToImageService( - modelId, apiKey, orgId, - endpoint, + modelId, HttpClientProvider.GetHttpClient(httpClient, serviceProvider), serviceProvider.GetService())); @@ -161,14 +135,13 @@ public static IKernelBuilder AddOpenAITextToImage( #region Text to Audio /// - /// Adds the to the . + /// Adds the OpenAI text-to-audio service to the list. /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The HttpClient to use with this service. /// The same instance as . [Experimental("SKEXP0010")] @@ -178,62 +151,34 @@ public static IKernelBuilder AddOpenAITextToAudio( string apiKey, string? orgId = null, string? serviceId = null, - Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextToAudioService( modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(httpClient, serviceProvider), serviceProvider.GetService())); return builder; } - - /// - /// Adds the to the . - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAITextToAudio( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(builder); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToAudioService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService())); - - return builder; - } - #endregion #region Audio-to-Text /// - /// Adds the to the . + /// Adds the OpenAI audio-to-text service to the list. /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The HttpClient to use with this service. /// The same instance as . [Experimental("SKEXP0010")] @@ -243,26 +188,26 @@ public static IKernelBuilder AddOpenAIAudioToText( string apiKey, string? orgId = null, string? serviceId = null, - Uri? endpoint = null, HttpClient? httpClient = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); - OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + Func factory = (serviceProvider, _) => new(modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(httpClient, serviceProvider), serviceProvider.GetService()); - builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + builder.Services.AddKeyedSingleton(serviceId, factory); return builder; } /// - /// Adds the to the . + /// Adds the OpenAI audio-to-text service to the list. /// /// The instance to augment. /// OpenAI model id @@ -277,13 +222,12 @@ public static IKernelBuilder AddOpenAIAudioToText( string? serviceId = null) { Verify.NotNull(builder); + Verify.NotNullOrWhiteSpace(modelId); - OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => - new(modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService()); + Func factory = (serviceProvider, _) => + new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - builder.Services.AddKeyedSingleton(serviceId, (Func)Factory); + builder.Services.AddKeyedSingleton(serviceId, factory); return builder; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs index d06dd65bba8d..02662815e1d8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs @@ -16,6 +16,8 @@ namespace Microsoft.SemanticKernel; +#pragma warning disable IDE0039 // Use local function + /* Phase 02 - Add endpoint parameter for both Embedding and TextToImage services extensions. - Removed unnecessary Validation checks (that are already happening in the service/client constructors) @@ -39,7 +41,6 @@ public static class OpenAIServiceCollectionExtensions /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// Non-default endpoint for the OpenAI API. /// The same instance as . [Experimental("SKEXP0010")] public static IServiceCollection AddOpenAITextEmbeddingGeneration( @@ -48,17 +49,17 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration( string apiKey, string? orgId = null, string? serviceId = null, - int? dimensions = null, - Uri? endpoint = null) + int? dimensions = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextEmbeddingGenerationService( modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(serviceProvider), serviceProvider.GetService(), dimensions)); @@ -81,6 +82,7 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceC int? dimensions = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextEmbeddingGenerationService( @@ -93,57 +95,30 @@ public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceC #region Text to Image /// - /// Adds the to the . + /// Add the OpenAI Dall-E text to image service to the list /// /// The instance to augment. - /// The model to use for image generation. /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// The model to use for image generation. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The same instance as . [Experimental("SKEXP0010")] public static IServiceCollection AddOpenAITextToImage(this IServiceCollection services, - string modelId, string apiKey, string? orgId = null, - string? serviceId = null, - Uri? endpoint = null) + string? modelId = null, + string? serviceId = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(apiKey); return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextToImageService( - modelId, apiKey, orgId, - endpoint, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - /// - /// Adds the to the . - /// - /// The instance to augment. - /// The OpenAI model id. - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAITextToImage(this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null, - int? dimensions = null) - { - Verify.NotNull(services); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToImageService( modelId, - openAIClient ?? serviceProvider.GetRequiredService(), + HttpClientProvider.GetHttpClient(serviceProvider), serviceProvider.GetService())); } #endregion @@ -151,14 +126,13 @@ public static IServiceCollection AddOpenAITextToImage(this IServiceCollection se #region Text to Audio /// - /// Adds the to the . + /// Adds the OpenAI text-to-audio service to the list. /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The same instance as . [Experimental("SKEXP0010")] public static IServiceCollection AddOpenAITextToAudio( @@ -166,58 +140,33 @@ public static IServiceCollection AddOpenAITextToAudio( string modelId, string apiKey, string? orgId = null, - string? serviceId = null, - Uri? endpoint = null) + string? serviceId = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => new OpenAITextToAudioService( modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(serviceProvider), serviceProvider.GetService())); } - /// - /// Adds the to the . - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAITextToAudio( - this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(services); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToAudioService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService())); - } - #endregion #region Audio-to-Text /// - /// Adds the to the . + /// Adds the OpenAI audio-to-text service to the list. /// /// The instance to augment. /// OpenAI model name, see https://platform.openai.com/docs/models /// OpenAI API key, see https://platform.openai.com/account/api-keys /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. /// A local identifier for the given AI service - /// Non-default endpoint for the OpenAI API. /// The same instance as . [Experimental("SKEXP0010")] public static IServiceCollection AddOpenAIAudioToText( @@ -225,26 +174,26 @@ public static IServiceCollection AddOpenAIAudioToText( string modelId, string apiKey, string? orgId = null, - string? serviceId = null, - Uri? endpoint = null) + string? serviceId = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); + Verify.NotNullOrWhiteSpace(apiKey); - OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + Func factory = (serviceProvider, _) => new(modelId, apiKey, orgId, - endpoint, HttpClientProvider.GetHttpClient(serviceProvider), serviceProvider.GetService()); - services.AddKeyedSingleton(serviceId, (Func)Factory); + services.AddKeyedSingleton(serviceId, factory); return services; } /// - /// Adds the to the . + /// Adds the OpenAI audio-to-text service to the list. /// /// The instance to augment. /// OpenAI model id @@ -259,11 +208,12 @@ public static IServiceCollection AddOpenAIAudioToText( string? serviceId = null) { Verify.NotNull(services); + Verify.NotNullOrWhiteSpace(modelId); - OpenAIAudioToTextService Factory(IServiceProvider serviceProvider, object? _) => + Func factory = (serviceProvider, _) => new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - services.AddKeyedSingleton(serviceId, (Func)Factory); + services.AddKeyedSingleton(serviceId, factory); return services; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index eb409cb24851..1ba3a0c23204 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Http; @@ -8,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.Services; using OpenAI; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -24,11 +22,6 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService /// private readonly ClientCore _client; - /// - /// Gets the attribute name used to store the organization in the dictionary. - /// - public static string OrganizationKey => "Organization"; - /// public IReadOnlyDictionary Attributes => this._client.Attributes; @@ -38,19 +31,17 @@ public sealed class OpenAIAudioToTextService : IAudioToTextService /// Model name /// OpenAI API Key /// OpenAI Organization Id (usually optional) - /// Non-default endpoint for the OpenAI API. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public OpenAIAudioToTextService( string modelId, string apiKey, string? organization = null, - Uri? endpoint = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); - this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); + this._client = new(modelId, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); } /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index dbb5ec08f135..fbe17e21f398 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -31,7 +31,6 @@ public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerat /// Model name /// OpenAI API Key /// OpenAI Organization Id (usually optional) - /// Non-default endpoint for the OpenAI API /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. @@ -39,16 +38,15 @@ public OpenAITextEmbeddingGenerationService( string modelId, string apiKey, string? organization = null, - Uri? endpoint = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null, int? dimensions = null) { - Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); + Verify.NotNullOrWhiteSpace(modelId); this._client = new( modelId: modelId, apiKey: apiKey, - endpoint: endpoint, + endpoint: null, organizationId: organization, httpClient: httpClient, logger: loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); @@ -69,7 +67,7 @@ public OpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); + Verify.NotNullOrWhiteSpace(modelId); this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); this._dimensions = dimensions; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs index 49ca77d74c6d..e064d640d55c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Http; @@ -9,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Services; using Microsoft.SemanticKernel.TextToAudio; -using OpenAI; namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -38,34 +36,17 @@ public sealed class OpenAITextToAudioService : ITextToAudioService /// Model name /// OpenAI API Key /// OpenAI Organization Id (usually optional) - /// Non-default endpoint for the OpenAI API. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public OpenAITextToAudioService( string modelId, string apiKey, string? organization = null, - Uri? endpoint = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); - this._client = new(modelId, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); - } - - /// - /// Initializes a new instance of the class. - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextToAudioService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); - this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); + this._client = new(modelId, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); } /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index e152c608922f..5f5631b3a642 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Net.Http; @@ -8,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.TextToImage; -using OpenAI; /* Phase 02 - Breaking the current constructor parameter order to follow the same order as the other services. @@ -18,6 +16,10 @@ - "modelId" parameter is now required in the constructor. - Added OpenAIClient breaking glass constructor. + +Phase 08 +- Removed OpenAIClient breaking glass constructor +- Reverted the order and parameter names. */ namespace Microsoft.SemanticKernel.Connectors.OpenAI; @@ -36,37 +38,20 @@ public class OpenAITextToImageService : ITextToImageService /// /// Initializes a new instance of the class. /// - /// The model to use for image generation. /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// Non-default endpoint for the OpenAI API. + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// The model to use for image generation. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. public OpenAITextToImageService( - string modelId, - string? apiKey = null, - string? organizationId = null, - Uri? endpoint = null, + string apiKey, + string? organization = null, + string? modelId = null, HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); - this._client = new(modelId, apiKey, organizationId, endpoint, httpClient, loggerFactory?.CreateLogger(this.GetType())); - } - - /// - /// Initializes a new instance of the class. - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextToImageService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); - this._client = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextToImageService))); + this._client = new(modelId, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(this.GetType())); } /// diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs index 812d41677b28..b2addba05188 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs @@ -27,7 +27,7 @@ public async Task OpenAITextToImageByModelTestAsync(string modelId, int width, i Assert.NotNull(openAIConfiguration); var kernel = Kernel.CreateBuilder() - .AddOpenAITextToImage(modelId, apiKey: openAIConfiguration.ApiKey) + .AddOpenAITextToImage(apiKey: openAIConfiguration.ApiKey, modelId: modelId) .Build(); var service = kernel.GetRequiredService(); From bd4dde0d73900bf446dfbb4182dc536a579ac1d5 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 11 Jul 2024 13:35:52 +0100 Subject: [PATCH 067/226] .Net: Remove AzureOpenAIFileService (#7195) ### Motivation and Context It was decided not to migrate existing `OpenAIFileService` to the Azure.AI.OpenAI SDK v2 and eventually deprecate it. ### Description This PR removes files related to the `OpenAIFileService` added by a previous PR. --- .../Connectors.AzureOpenAI.csproj | 16 --- .../Core/ClientCore.File.cs | 124 ----------------- .../Model/AzureOpenAIFilePurpose.cs | 22 --- .../Model/AzureOpenAIFileReference.cs | 38 ------ .../Services/AzureOpenAIFileService.cs | 128 ------------------ .../AzureOpenAIFileUploadExecutionSettings.cs | 35 ----- 6 files changed, 363 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 65a954656bf9..35c31788610d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -21,26 +21,10 @@ Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs deleted file mode 100644 index 32a97ed1e803..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.File.cs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -/* -Phase 05 -- Ignoring the specific Purposes not implemented by current FileService. -*/ - -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; -using OpenAI.Files; - -using OAIFilePurpose = OpenAI.Files.OpenAIFilePurpose; -using SKFilePurpose = Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Uploads a file to OpenAI. - /// - /// File name - /// File content - /// Purpose of the file - /// Cancellation token - /// Uploaded file information - internal async Task UploadFileAsync( - string fileName, - Stream fileContent, - SKFilePurpose purpose, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().UploadFileAsync(fileContent, fileName, ConvertToOpenAIFilePurpose(purpose), cancellationToken)).ConfigureAwait(false); - return ConvertToFileReference(response.Value); - } - - /// - /// Delete a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - internal async Task DeleteFileAsync( - string fileId, - CancellationToken cancellationToken) - { - await RunRequestAsync(() => this.Client.GetFileClient().DeleteFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - } - - /// - /// Retrieve metadata for a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The metadata associated with the specified file identifier. - internal async Task GetFileAsync( - string fileId, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - return ConvertToFileReference(response.Value); - } - - /// - /// Retrieve metadata for all previously uploaded files. - /// - /// The to monitor for cancellation requests. The default is . - /// The metadata of all uploaded files. - internal async Task> GetFilesAsync(CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().GetFilesAsync(cancellationToken: cancellationToken)).ConfigureAwait(false); - return response.Value.Select(ConvertToFileReference); - } - - /// - /// Retrieve the file content from a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The file content as - /// - /// Files uploaded with do not support content retrieval. - /// - internal async Task GetFileContentAsync( - string fileId, - CancellationToken cancellationToken) - { - ClientResult response = await RunRequestAsync(() => this.Client.GetFileClient().DownloadFileAsync(fileId, cancellationToken)).ConfigureAwait(false); - return response.Value.ToArray(); - } - - private static OpenAIFileReference ConvertToFileReference(OpenAIFileInfo fileInfo) - => new() - { - Id = fileInfo.Id, - CreatedTimestamp = fileInfo.CreatedAt.DateTime, - FileName = fileInfo.Filename, - SizeInBytes = (int)(fileInfo.SizeInBytes ?? 0), - Purpose = ConvertToFilePurpose(fileInfo.Purpose), - }; - - private static FileUploadPurpose ConvertToOpenAIFilePurpose(SKFilePurpose purpose) - { - if (purpose == SKFilePurpose.Assistants) { return FileUploadPurpose.Assistants; } - if (purpose == SKFilePurpose.FineTune) { return FileUploadPurpose.FineTune; } - - throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); - } - - private static SKFilePurpose ConvertToFilePurpose(OAIFilePurpose purpose) - { - if (purpose == OAIFilePurpose.Assistants) { return SKFilePurpose.Assistants; } - if (purpose == OAIFilePurpose.FineTune) { return SKFilePurpose.FineTune; } - - throw new KernelException($"Unknown {nameof(OpenAIFilePurpose)}: {purpose}."); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs deleted file mode 100644 index 0e7e7f46f233..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFilePurpose.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Defines the purpose associated with the uploaded file. -/// -[Experimental("SKEXP0010")] -public enum AzureOpenAIFilePurpose -{ - /// - /// File to be used by assistants for model processing. - /// - Assistants, - - /// - /// File to be used by fine-tuning jobs. - /// - FineTune, -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs deleted file mode 100644 index 80166c30e77b..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Model/AzureOpenAIFileReference.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// References an uploaded file by id. -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAIFileReference -{ - /// - /// The file identifier. - /// - public string Id { get; set; } = string.Empty; - - /// - /// The timestamp the file was uploaded.s - /// - public DateTime CreatedTimestamp { get; set; } - - /// - /// The name of the file.s - /// - public string FileName { get; set; } = string.Empty; - - /// - /// Describes the associated purpose of the file. - /// - public OpenAIFilePurpose Purpose { get; set; } - - /// - /// The file size, in bytes. - /// - public int SizeInBytes { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs deleted file mode 100644 index 6a2f3d01014a..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIFileService.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// File service access for Azure OpenAI. -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAIFileService -{ - /// - /// OpenAI client for HTTP operations. - /// - private readonly ClientCore _client; - - /// - /// Initializes a new instance of the class. - /// - /// Non-default endpoint for the OpenAI API. - /// API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIFileService( - Uri endpoint, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNull(apiKey, nameof(apiKey)); - - this._client = new(null, apiKey, organization, endpoint, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); - } - - /// - /// Initializes a new instance of the class. - /// - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIFileService( - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNull(apiKey, nameof(apiKey)); - - this._client = new(null, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(typeof(OpenAIFileService))); - } - - /// - /// Remove a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - public Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - - return this._client.DeleteFileAsync(id, cancellationToken); - } - - /// - /// Retrieve the file content from a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The file content as - /// - /// Files uploaded with do not support content retrieval. - /// - public async Task GetFileContentAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - var bytes = await this._client.GetFileContentAsync(id, cancellationToken).ConfigureAwait(false); - - // The mime type of the downloaded file is not provided by the OpenAI API. - return new(bytes, null); - } - - /// - /// Retrieve metadata for a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The metadata associated with the specified file identifier. - public Task GetFileAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - return this._client.GetFileAsync(id, cancellationToken); - } - - /// - /// Retrieve metadata for all previously uploaded files. - /// - /// The to monitor for cancellation requests. The default is . - /// The metadata of all uploaded files. - public async Task> GetFilesAsync(CancellationToken cancellationToken = default) - => await this._client.GetFilesAsync(cancellationToken).ConfigureAwait(false); - - /// - /// Upload a file. - /// - /// The file content as - /// The upload settings - /// The to monitor for cancellation requests. The default is . - /// The file metadata. - public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default) - { - Verify.NotNull(settings, nameof(settings)); - Verify.NotNull(fileContent.Data, nameof(fileContent.Data)); - - using var memoryStream = new MemoryStream(fileContent.Data.Value.ToArray()); - return await this._client.UploadFileAsync(settings.FileName, memoryStream, settings.Purpose, cancellationToken).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs deleted file mode 100644 index c7676c86076b..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIFileUploadExecutionSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Execution settings associated with Azure Open AI file upload . -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAIFileUploadExecutionSettings -{ - /// - /// Initializes a new instance of the class. - /// - /// The file name - /// The file purpose - public AzureOpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose) - { - Verify.NotNull(fileName, nameof(fileName)); - - this.FileName = fileName; - this.Purpose = purpose; - } - - /// - /// The file name. - /// - public string FileName { get; } - - /// - /// The file purpose. - /// - public OpenAIFilePurpose Purpose { get; } -} From 64120d3ad6ebbf67b03e6169448164ac1896947b Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 11 Jul 2024 16:17:09 +0100 Subject: [PATCH 068/226] .Net: OpenAI V2 Removing LogActivity Extra Implementation (#7205) ### Motivation and Context Removing non existing V1 logging. --- .../Services/OpenAIAudioToTextServiceTests.cs | 20 --- .../OpenAIChatCompletionServiceTests.cs | 121 ------------------ .../Services/OpenAITextToAudioServiceTests.cs | 20 --- .../Services/OpenAITextToImageServiceTests.cs | 20 --- .../Services/OpenAIAudioToTextService.cs | 5 +- .../Services/OpenAIChatCompletionService.cs | 44 ++++--- .../Services/OpenAITextToAudioService.cs | 5 +- .../Services/OpenAITextToImageService.cs | 5 +- 8 files changed, 27 insertions(+), 213 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs index 660960697c21..2ad74a6db04b 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -148,26 +148,6 @@ public async Task GetTextContentThrowsIfFileNameIsInvalidAsync() await Assert.ThrowsAsync(async () => { await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("invalid")); }); } - [Fact] - public async Task GetTextContentsDoesLogActionAsync() - { - // Assert - var modelId = "whisper-1"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - // Arrange - var sut = new OpenAIAudioToTextService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetTextContentsAsync(new(new byte[] { 0x01, 0x02 }, "text/plain")); - - // Assert - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIAudioToTextService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index e10bbd941b38..1a0145d137f2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -811,31 +811,6 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); } - [Fact] - public async Task GetAllContentsDoesLogActionAsync() - { - // Assert - var modelId = "gpt-4o"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - // Arrange - var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act & Assert - await Assert.ThrowsAnyAsync(async () => { await sut.GetChatMessageContentsAsync(this._chatHistoryForTest); }); - await Assert.ThrowsAnyAsync(async () => { await sut.GetStreamingChatMessageContentsAsync(this._chatHistoryForTest).GetAsyncEnumerator().MoveNextAsync(); }); - await Assert.ThrowsAnyAsync(async () => { await sut.GetTextContentsAsync("test"); }); - await Assert.ThrowsAnyAsync(async () => { await sut.GetStreamingTextContentsAsync("test").GetAsyncEnumerator().MoveNextAsync(); }); - - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - [Theory] [InlineData("string", "json_object")] [InlineData("string", "text")] @@ -878,102 +853,6 @@ public async Task GetChatMessageInResponseFormatsAsync(string formatType, string Assert.NotNull(result); } - [Fact] - public async Task GetChatMessageContentsLogsAsExpected() - { - // Assert - var modelId = "gpt-4o"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) - }; - - var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetChatMessageContentsAsync(this._chatHistoryForTest); - - // Arrange - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - - [Fact] - public async Task GetStreamingChatMessageContentsLogsAsExpected() - { - // Assert - var modelId = "gpt-4o"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) - }; - - var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetStreamingChatMessageContentsAsync(this._chatHistoryForTest).GetAsyncEnumerator().MoveNextAsync(); - - // Arrange - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingChatMessageContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - - [Fact] - public async Task GetTextContentsLogsAsExpected() - { - // Assert - var modelId = "gpt-4o"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(File.ReadAllText("TestData/chat_completion_test_response.json")) - }; - - var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetTextContentAsync("test"); - - // Arrange - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - - [Fact] - public async Task GetStreamingTextContentsLogsAsExpected() - { - // Assert - var modelId = "gpt-4o"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) - }; - - var sut = new OpenAIChatCompletionService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetStreamingTextContentsAsync("test").GetAsyncEnumerator().MoveNextAsync(); - - // Arrange - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAIChatCompletionService.GetStreamingTextContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - [Fact(Skip = "Not working running in the console")] public async Task GetInvalidResponseThrowsExceptionAndIsCapturedByDiagnosticsAsync() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs index b77899b9a5b6..e20d28385293 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs @@ -182,26 +182,6 @@ public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAdd Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); } - [Fact] - public async Task GetAudioContentDoesLogActionAsync() - { - // Assert - var modelId = "whisper-1"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - // Arrange - var sut = new OpenAITextToAudioService(modelId, "apiKey", httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GetAudioContentsAsync("description"); - - // Assert - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextToAudioService.GetAudioContentsAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs index 08fb5c76c89f..f59fea554eda 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -77,26 +77,6 @@ public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string Assert.Equal("https://image-url/", result); } - [Fact] - public async Task GenerateImageDoesLogActionAsync() - { - // Assert - var modelId = "dall-e-2"; - var logger = new Mock>(); - logger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(logger.Object); - - // Arrange - var sut = new OpenAITextToImageService("apiKey", modelId: modelId, httpClient: this._httpClient, loggerFactory: this._mockLoggerFactory.Object); - - // Act - await sut.GenerateImageAsync("description", 256, 256); - - // Assert - logger.VerifyLog(LogLevel.Information, $"Action: {nameof(OpenAITextToImageService.GenerateImageAsync)}. OpenAI Model ID: {modelId}.", Times.Once()); - } - public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index 1ba3a0c23204..585488d24f7f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -65,8 +65,5 @@ public Task> GetTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); - } + => this._client.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs index 4d87999cdf12..f3a5dd7fd790 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs @@ -123,30 +123,34 @@ public OpenAIChatCompletionService( public IReadOnlyDictionary Attributes => this._client.Attributes; /// - public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - } + public Task> GetChatMessageContentsAsync( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._client.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); /// - public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - } + public IAsyncEnumerable GetStreamingChatMessageContentsAsync( + ChatHistory chatHistory, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._client.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); /// - public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); - } + public Task> GetTextContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._client.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); /// - public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); - } + public IAsyncEnumerable GetStreamingTextContentsAsync( + string prompt, + PromptExecutionSettings? executionSettings = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this._client.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs index e064d640d55c..5c5aba683e6e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -55,8 +55,5 @@ public Task> GetAudioContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); - } + => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 5f5631b3a642..cca9073bfe9c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -56,8 +56,5 @@ public OpenAITextToImageService( /// public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - this._client.LogActionDetails(); - return this._client.GenerateImageAsync(description, width, height, cancellationToken); - } + => this._client.GenerateImageAsync(description, width, height, cancellationToken); } From a10e9f278d474b514be0936d44d0708255d17b68 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 11 Jul 2024 20:05:47 +0100 Subject: [PATCH 069/226] .Net: Align metadata names with underlying library ones (#7207) ### Motivation and Context This PR aligns metadata names with those provided by the underlying Azure.AI.OpenAI library. Additionally, it adds a few unit tests to increase code coverage. --- ...enAITextEmbeddingGenerationServiceTests.cs | 21 ++++-- .../AzureOpenAITextToImageServiceTests.cs | 67 +++++++++++++++++-- .../Core/ClientCore.ChatCompletion.cs | 16 ++--- .../Core/ClientCore.ChatCompletion.cs | 8 +-- 4 files changed, 92 insertions(+), 20 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs index 738364429cff..4e8a12b9b69b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextEmbeddingGenerationServiceTests.cs @@ -8,9 +8,11 @@ using System.Threading; using System.Threading.Tasks; using Azure.AI.OpenAI; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Services; +using Moq; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; @@ -19,12 +21,23 @@ namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; /// public class AzureOpenAITextEmbeddingGenerationServiceTests { - [Fact] - public void ItCanBeInstantiatedAndPropertiesSetAsExpected() + private readonly Mock _mockLoggerFactory; + + public AzureOpenAITextEmbeddingGenerationServiceTests() + { + this._mockLoggerFactory = new Mock(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ItCanBeInstantiatedAndPropertiesSetAsExpected(bool includeLoggerFactory) { // Arrange - var sut = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", modelId: "model", dimensions: 2); - var sutWithAzureOpenAIClient = new AzureOpenAITextEmbeddingGenerationService("deployment-name", new AzureOpenAIClient(new Uri("https://endpoint"), new ApiKeyCredential("apiKey")), modelId: "model", dimensions: 2); + var sut = includeLoggerFactory ? + new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", modelId: "model", dimensions: 2, loggerFactory: this._mockLoggerFactory.Object) : + new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", modelId: "model", dimensions: 2); + var sutWithAzureOpenAIClient = new AzureOpenAITextEmbeddingGenerationService("deployment-name", new AzureOpenAIClient(new Uri("https://endpoint"), new ApiKeyCredential("apiKey")), modelId: "model", dimensions: 2, loggerFactory: this._mockLoggerFactory.Object); // Assert Assert.NotNull(sut); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs index d384df3d627c..89b25e9b2ec0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs @@ -41,17 +41,17 @@ public AzureOpenAITextToImageServiceTests() public void ConstructorsAddRequiredMetadata() { // Case #1 - var sut = new AzureOpenAITextToImageService("deployment", "https://api-host/", "api-key", "model"); + var sut = new AzureOpenAITextToImageService("deployment", "https://api-host/", "api-key", "model", loggerFactory: this._mockLoggerFactory.Object); Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); // Case #2 - sut = new AzureOpenAITextToImageService("deployment", "https://api-hostapi/", new Mock().Object, "model"); + sut = new AzureOpenAITextToImageService("deployment", "https://api-hostapi/", new Mock().Object, "model", loggerFactory: this._mockLoggerFactory.Object); Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); // Case #3 - sut = new AzureOpenAITextToImageService("deployment", new AzureOpenAIClient(new Uri("https://api-host/"), "api-key"), "model"); + sut = new AzureOpenAITextToImageService("deployment", new AzureOpenAIClient(new Uri("https://api-host/"), "api-key"), "model", loggerFactory: this._mockLoggerFactory.Object); Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); } @@ -68,7 +68,7 @@ public void ConstructorsAddRequiredMetadata() public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string modelId) { // Arrange - var sut = new AzureOpenAITextToImageService("deployment", "https://api-host", "api-key", modelId, this._httpClient); + var sut = new AzureOpenAITextToImageService("deployment", "https://api-host", "api-key", modelId, this._httpClient, loggerFactory: this._mockLoggerFactory.Object); // Act var result = await sut.GenerateImageAsync("description", width, height); @@ -84,6 +84,65 @@ public async Task GenerateImageWorksCorrectlyAsync(int width, int height, string Assert.Equal($"{width}x{height}", request["size"]?.ToString()); } + [Theory] + [InlineData(true)] + [InlineData(false)] + public async Task ItShouldUseProvidedEndpoint(bool useTokeCredential) + { + // Arrange + var sut = useTokeCredential ? + new AzureOpenAITextToImageService("deployment", endpoint: "https://api-host", new Mock().Object, "dall-e-3", this._httpClient) : + new AzureOpenAITextToImageService("deployment", endpoint: "https://api-host", "api-key", "dall-e-3", this._httpClient); + + // Act + var result = await sut.GenerateImageAsync("description", 1024, 1024); + + // Assert + Assert.StartsWith("https://api-host", this._messageHandlerStub.RequestUri?.AbsoluteUri); + } + + [Theory] + [InlineData(true, "")] + [InlineData(true, null)] + [InlineData(false, "")] + [InlineData(false, null)] + public async Task ItShouldUseHttpClientUriIfNoEndpointProvided(bool useTokeCredential, string? endpoint) + { + // Arrange + this._httpClient.BaseAddress = new Uri("https://api-host"); + + var sut = useTokeCredential ? + new AzureOpenAITextToImageService("deployment", endpoint: endpoint!, new Mock().Object, "dall-e-3", this._httpClient) : + new AzureOpenAITextToImageService("deployment", endpoint: endpoint!, "api-key", "dall-e-3", this._httpClient); + + // Act + var result = await sut.GenerateImageAsync("description", 1024, 1024); + + // Assert + Assert.StartsWith("https://api-host", this._messageHandlerStub.RequestUri?.AbsoluteUri); + } + + [Theory] + [InlineData(true, "")] + [InlineData(true, null)] + [InlineData(false, "")] + [InlineData(false, null)] + public void ItShouldThrowExceptionIfNoEndpointProvided(bool useTokeCredential, string? endpoint) + { + // Arrange + this._httpClient.BaseAddress = null; + + // Act & Assert + if (useTokeCredential) + { + Assert.Throws(() => new AzureOpenAITextToImageService("deployment", endpoint: endpoint!, new Mock().Object, "dall-e-3", this._httpClient)); + } + else + { + Assert.Throws(() => new AzureOpenAITextToImageService("deployment", endpoint: endpoint!, "api-key", "dall-e-3", this._httpClient)); + } + } + public void Dispose() { this._httpClient.Dispose(); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index c9a6f26f94ef..0efe630c6006 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -27,9 +27,9 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// internal partial class ClientCore { - private const string PromptFilterResultsMetadataKey = "PromptFilterResults"; - private const string ContentFilterResultsMetadataKey = "ContentFilterResults"; - private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; + private const string ContentFilterResultForPromptKey = "ContentFilterResultForPrompt"; + private const string ContentFilterResultForResponseKey = "ContentFilterResultForResponse"; + private const string ContentTokenLogProbabilitiesKey = "ContentTokenLogProbabilities"; private const string ModelProvider = "openai"; private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); @@ -92,25 +92,25 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) { #pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - return new Dictionary(8) + return new Dictionary { { nameof(completions.Id), completions.Id }, { nameof(completions.CreatedAt), completions.CreatedAt }, - { PromptFilterResultsMetadataKey, completions.GetContentFilterResultForPrompt() }, + { ContentFilterResultForPromptKey, completions.GetContentFilterResultForPrompt() }, { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, { nameof(completions.Usage), completions.Usage }, - { ContentFilterResultsMetadataKey, completions.GetContentFilterResultForResponse() }, + { ContentFilterResultForResponseKey, completions.GetContentFilterResultForResponse() }, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, + { ContentTokenLogProbabilitiesKey, completions.ContentTokenLogProbabilities }, }; #pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) { - return new Dictionary(4) + return new Dictionary { { nameof(completionUpdate.Id), completionUpdate.Id }, { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs index effff740d2ed..9f162a041d76 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -26,7 +26,7 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// internal partial class ClientCore { - private const string LogProbabilityInfoMetadataKey = "LogProbabilityInfo"; + private const string ContentTokenLogProbabilitiesKey = "ContentTokenLogProbabilities"; private const string ModelProvider = "openai"; private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); @@ -88,7 +88,7 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) { - return new Dictionary(8) + return new Dictionary { { nameof(completions.Id), completions.Id }, { nameof(completions.CreatedAt), completions.CreatedAt }, @@ -97,13 +97,13 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { LogProbabilityInfoMetadataKey, completions.ContentTokenLogProbabilities }, + { ContentTokenLogProbabilitiesKey, completions.ContentTokenLogProbabilities }, }; } private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) { - return new Dictionary(4) + return new Dictionary { { nameof(completionUpdate.Id), completionUpdate.Id }, { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, From f0b2757df0fe9ab6d95ebf6c1e91c6e9409f01a3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 12 Jul 2024 09:56:14 +0100 Subject: [PATCH 070/226] .Net: Remove time stamp granularities (#7214) ### Motivation, Context and Description The new Azure.AI.OpenAI V2 introduced timestamp granularities for the audio-to-text API. This PR removes the first attempt to expose the settings in execution settings for the service. The setting will be introduced later after the migration is over in scope in this task: https://github.com/microsoft/semantic-kernel/issues/7213. --- .../AzureOpenAIAudioToTextServiceTests.cs | 38 ------------------ .../Core/ClientCore.AudioToText.cs | 24 +----------- ...AzureOpenAIAudioToTextExecutionSettings.cs | 27 ------------- .../Services/OpenAIAudioToTextServiceTests.cs | 39 ------------------- .../Core/ClientCore.AudioToText.cs | 24 +----------- .../OpenAIAudioToTextExecutionSettings.cs | 27 ------------- 6 files changed, 2 insertions(+), 177 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs index a0964dafedc0..f9b23f50c020 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs @@ -11,7 +11,6 @@ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Services; using Moq; -using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureOpenAIAudioToTextExecutionSettings; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; @@ -109,43 +108,6 @@ public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(AzureOpe Assert.IsType(expectedExceptionType, exception); } - [Theory] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default }, "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word }, "word")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment }, "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Word }, "word", "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Segment }, "word", "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Word }, "word", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Default }, "word", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Segment }, "segment", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Default }, "segment", "0")] - public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] granularities, params string[] expectedGranularities) - { - // Arrange - var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("Test audio-to-text response") - }; - - // Act - var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { Granularities = granularities }; - var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); - - // Assert - Assert.NotNull(this._messageHandlerStub.RequestContent); - Assert.NotNull(result); - - var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); - - foreach (var granularity in expectedGranularities) - { - var expectedMultipart = $"{granularity}\r\n{multiPartBreak}"; - Assert.Contains(expectedMultipart, multiPartData); - } - } - [Theory] [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, "verbose_json")] [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, "json")] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index b3910feaf1cb..67071a635738 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -51,35 +51,13 @@ internal async Task> GetTextFromAudioContentsAsync( private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(AzureOpenAIAudioToTextExecutionSettings executionSettings) => new() { - Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), + Granularities = AudioTimestampGranularities.Default, Language = executionSettings.Language, Prompt = executionSettings.Prompt, Temperature = executionSettings.Temperature, ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; - private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) - { - AudioTimestampGranularities result = AudioTimestampGranularities.Default; - - if (granularities is not null) - { - foreach (var granularity in granularities) - { - var openAIGranularity = granularity switch - { - AzureOpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, - AzureOpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, - _ => AudioTimestampGranularities.Default - }; - - result |= openAIGranularity; - } - } - - return result; - } - private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) => new(3) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs index 0f8115c70910..549fe69f5586 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs @@ -95,12 +95,6 @@ public float Temperature } } - /// - /// The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. - /// - [JsonPropertyName("granularities")] - public IReadOnlyList? Granularities { get; set; } - /// /// Creates an instance of class with default filename - "file.mp3". /// @@ -161,27 +155,6 @@ public static AzureOpenAIAudioToTextExecutionSettings FromExecutionSettings(Prom throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIAudioToTextExecutionSettings)}", nameof(executionSettings)); } - /// - /// The timestamp granularities available to populate transcriptions. - /// - public enum TimeStampGranularities - { - /// - /// Not specified. - /// - Default = 0, - - /// - /// The transcription is segmented by word. - /// - Word = 1, - - /// - /// The timestamp of transcription is by segment. - /// - Segment = 2, - } - /// /// Specifies the format of the audio transcription. /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs index 2ad74a6db04b..3ab5c0b7f960 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs @@ -2,7 +2,6 @@ using System; using System.Net.Http; -using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; @@ -10,7 +9,6 @@ using Moq; using OpenAI; using Xunit; -using static Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings; namespace SemanticKernel.Connectors.OpenAI.UnitTests.Services; @@ -73,43 +71,6 @@ public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) Assert.Equal("model-id", service.Attributes["ModelId"]); } - [Theory] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default }, "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word }, "word")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment }, "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Word }, "word", "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Segment }, "word", "segment")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Word }, "word", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Word, TimeStampGranularities.Default }, "word", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Default, TimeStampGranularities.Segment }, "segment", "0")] - [InlineData(new TimeStampGranularities[] { TimeStampGranularities.Segment, TimeStampGranularities.Default }, "segment", "0")] - public async Task GetTextContentGranularitiesWorksAsync(TimeStampGranularities[] granularities, params string[] expectedGranularities) - { - // Arrange - var service = new OpenAIAudioToTextService("model-id", "api-key", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("Test audio-to-text response") - }; - - // Act - var settings = new OpenAIAudioToTextExecutionSettings("file.mp3") { Granularities = granularities }; - var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); - - // Assert - Assert.NotNull(this._messageHandlerStub.RequestContent); - Assert.NotNull(result); - - var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); - - foreach (var granularity in expectedGranularities) - { - var expectedMultipart = $"{granularity}\r\n{multiPartBreak}"; - Assert.Contains(expectedMultipart, multiPartData); - } - } - [Fact] public async Task GetTextContentByDefaultWorksCorrectlyAsync() { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs index e8e974655175..1de1af26e41a 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -51,35 +51,13 @@ internal async Task> GetTextFromAudioContentsAsync( private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) => new() { - Granularities = ConvertToAudioTimestampGranularities(executionSettings!.Granularities), + Granularities = AudioTimestampGranularities.Default, Language = executionSettings.Language, Prompt = executionSettings.Prompt, Temperature = executionSettings.Temperature, ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; - private static AudioTimestampGranularities ConvertToAudioTimestampGranularities(IEnumerable? granularities) - { - AudioTimestampGranularities result = AudioTimestampGranularities.Default; - - if (granularities is not null) - { - foreach (var granularity in granularities) - { - var openAIGranularity = granularity switch - { - OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Word => AudioTimestampGranularities.Word, - OpenAIAudioToTextExecutionSettings.TimeStampGranularities.Segment => AudioTimestampGranularities.Segment, - _ => AudioTimestampGranularities.Default - }; - - result |= openAIGranularity; - } - } - - return result; - } - private static AudioTranscriptionFormat? ConvertResponseFormat(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat? responseFormat) { if (responseFormat is null) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs index 845c0220ef89..b8651a31bd50 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -94,12 +94,6 @@ public float Temperature } } - /// - /// The timestamp granularities to populate for this transcription. response_format must be set verbose_json to use timestamp granularities. Either or both of these options are supported: word, or segment. - /// - [JsonPropertyName("granularities")] - public IReadOnlyList? Granularities { get; set; } - /// /// Creates an instance of class with default filename - "file.mp3". /// @@ -155,27 +149,6 @@ public override PromptExecutionSettings Clone() return openAIExecutionSettings!; } - /// - /// The timestamp granularities available to populate transcriptions. - /// - public enum TimeStampGranularities - { - /// - /// Not specified. - /// - Default = 0, - - /// - /// The transcription is segmented by word. - /// - Word = 1, - - /// - /// The timestamp of transcription is by segment. - /// - Segment = 2, - } - /// /// Specifies the format of the audio transcription. /// From 49ff10f3b66eeab958f441e521f906238f8b539f Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:45:48 +0100 Subject: [PATCH 071/226] .Net: Rollback unnecessary breaking change (#7222) ### Motivation, Context and Description The unnecessary breaking change that was introduced earlier, which changed the type of the `OpenAIAudioToTextExecutionSettings.ResponseFormat` and `AzureOpenAIAudioToTextExecutionSettings.ResponseFormat` properties, has been rolled back. --- .../AzureOpenAIAudioToTextServiceTests.cs | 14 ++++---- ...OpenAIAudioToTextExecutionSettingsTests.cs | 14 ++++---- .../Core/ClientCore.AudioToText.cs | 15 +++------ .../Core/ClientCore.ChatCompletion.cs | 3 +- ...AzureOpenAIAudioToTextExecutionSettings.cs | 33 ++----------------- ...OpenAIAudioToTextExecutionSettingsTests.cs | 14 ++++---- .../Core/ClientCore.AudioToText.cs | 15 +++------ .../Core/ClientCore.ChatCompletion.cs | 3 +- .../OpenAIAudioToTextExecutionSettings.cs | 7 ++-- .../AzureOpenAIChatCompletionTests.cs | 5 ++- ...eOpenAIChatCompletion_NonStreamingTests.cs | 12 +++---- .../OpenAI/OpenAIChatCompletionTests.cs | 2 +- .../OpenAIChatCompletion_NonStreamingTests.cs | 4 +-- 13 files changed, 50 insertions(+), 91 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs index f9b23f50c020..46439311ccdc 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs @@ -109,11 +109,11 @@ public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(AzureOpe } [Theory] - [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, "verbose_json")] - [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, "json")] - [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt, "vtt")] - [InlineData(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt, "srt")] - public async Task ItRespectResultFormatExecutionSettingAsync(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat responseFormat, string expectedFormat) + [InlineData("verbose_json")] + [InlineData("json")] + [InlineData("vtt")] + [InlineData("srt")] + public async Task ItRespectResultFormatExecutionSettingAsync(string format) { // Arrange var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", httpClient: this._httpClient); @@ -123,7 +123,7 @@ public async Task ItRespectResultFormatExecutionSettingAsync(AzureOpenAIAudioToT }; // Act - var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { ResponseFormat = responseFormat }; + var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { ResponseFormat = format }; var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); // Assert @@ -133,7 +133,7 @@ public async Task ItRespectResultFormatExecutionSettingAsync(AzureOpenAIAudioToT var multiPartData = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); var multiPartBreak = multiPartData.Substring(0, multiPartData.IndexOf("\r\n", StringComparison.OrdinalIgnoreCase)); - Assert.Contains($"{expectedFormat}\r\n{multiPartBreak}", multiPartData); + Assert.Contains($"{format}\r\n{multiPartBreak}", multiPartData); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs index 4582a79282a4..5f7f89be988f 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs @@ -27,7 +27,7 @@ public void ItReturnsValidOpenAIAudioToTextExecutionSettings() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "json", Temperature = 0.2f }; @@ -48,7 +48,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() "language": "en", "filename": "file.mp3", "prompt": "prompt", - "response_format": "verbose", + "response_format": "verbose_json", "temperature": 0.2 } """; @@ -64,7 +64,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() Assert.Equal("en", settings.Language); Assert.Equal("file.mp3", settings.Filename); Assert.Equal("prompt", settings.Prompt); - Assert.Equal(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, settings.ResponseFormat); + Assert.Equal("verbose_json", settings.ResponseFormat); Assert.Equal(0.2f, settings.Temperature); } @@ -76,7 +76,7 @@ public void ItClonesAllProperties() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "vtt", Temperature = 0.2f, Filename = "something.mp3", }; @@ -87,7 +87,7 @@ public void ItClonesAllProperties() Assert.Equal("model_id", clone.ModelId); Assert.Equal("en", clone.Language); Assert.Equal("prompt", clone.Prompt); - Assert.Equal(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, clone.ResponseFormat); + Assert.Equal("vtt", clone.ResponseFormat); Assert.Equal(0.2f, clone.Temperature); Assert.Equal("something.mp3", clone.Filename); } @@ -100,7 +100,7 @@ public void ItFreezesAndPreventsMutation() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "srt", Temperature = 0.2f, Filename = "something.mp3", }; @@ -111,7 +111,7 @@ public void ItFreezesAndPreventsMutation() Assert.Throws(() => settings.ModelId = "new_model"); Assert.Throws(() => settings.Language = "some_format"); Assert.Throws(() => settings.Prompt = "prompt"); - Assert.Throws(() => settings.ResponseFormat = AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple); + Assert.Throws(() => settings.ResponseFormat = "srt"); Assert.Throws(() => settings.Temperature = 0.2f); Assert.Throws(() => settings.Filename = "something"); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index 67071a635738..5e3aa0565b93 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -66,19 +66,14 @@ private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(Azure [nameof(audioTranscription.Segments)] = audioTranscription.Segments }; - private static AudioTranscriptionFormat? ConvertResponseFormat(AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat? responseFormat) + private static AudioTranscriptionFormat ConvertResponseFormat(string responseFormat) { - if (responseFormat is null) - { - return null; - } - return responseFormat switch { - AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple => AudioTranscriptionFormat.Simple, - AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose => AudioTranscriptionFormat.Verbose, - AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt => AudioTranscriptionFormat.Vtt, - AzureOpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt => AudioTranscriptionFormat.Srt, + "json" => AudioTranscriptionFormat.Simple, + "verbose_json" => AudioTranscriptionFormat.Verbose, + "vtt" => AudioTranscriptionFormat.Vtt, + "srt" => AudioTranscriptionFormat.Srt, _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), }; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index 0efe630c6006..99587b8e5a00 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -29,7 +29,6 @@ internal partial class ClientCore { private const string ContentFilterResultForPromptKey = "ContentFilterResultForPrompt"; private const string ContentFilterResultForResponseKey = "ContentFilterResultForResponse"; - private const string ContentTokenLogProbabilitiesKey = "ContentTokenLogProbabilities"; private const string ModelProvider = "openai"; private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); @@ -103,7 +102,7 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { ContentTokenLogProbabilitiesKey, completions.ContentTokenLogProbabilities }, + { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, }; #pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs index 549fe69f5586..f09c4bb8072a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs @@ -62,11 +62,10 @@ public string? Prompt } /// - /// The format of the transcript output, in one of these options: Text, Simple, Verbose, Sttor vtt. Default is 'json'. + /// The format of the transcript output, in one of these options: json, srt, verbose_json, or vtt. Default is 'json'. /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public AudioTranscriptionFormat? ResponseFormat + public string ResponseFormat { get => this._responseFormat; @@ -155,38 +154,12 @@ public static AzureOpenAIAudioToTextExecutionSettings FromExecutionSettings(Prom throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIAudioToTextExecutionSettings)}", nameof(executionSettings)); } - /// - /// Specifies the format of the audio transcription. - /// - public enum AudioTranscriptionFormat - { - /// - /// Response body that is a JSON object containing a single 'text' field for the transcription. - /// - Simple, - - /// - /// Use a response body that is a JSON object containing transcription text along with timing, segments, and other metadata. - /// - Verbose, - - /// - /// Response body that is plain text in SubRip (SRT) format that also includes timing information. - /// - Srt, - - /// - /// Response body that is plain text in Web Video Text Tracks (VTT) format that also includes timing information. - /// - Vtt, - } - #region private ================================================================================ private const string DefaultFilename = "file.mp3"; private float _temperature = 0; - private AudioTranscriptionFormat? _responseFormat; + private string _responseFormat = "json"; private string _filename; private string? _language; private string? _prompt; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs index 4f443fdcc02a..66390ddfd94d 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs @@ -28,7 +28,7 @@ public void ItReturnsValidOpenAIAudioToTextExecutionSettings() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "srt", Temperature = 0.2f }; @@ -49,7 +49,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() "language": "en", "filename": "file.mp3", "prompt": "prompt", - "response_format": "verbose", + "response_format": "verbose_json", "temperature": 0.2 } """; @@ -65,7 +65,7 @@ public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() Assert.Equal("en", settings.Language); Assert.Equal("file.mp3", settings.Filename); Assert.Equal("prompt", settings.Prompt); - Assert.Equal(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose, settings.ResponseFormat); + Assert.Equal("verbose_json", settings.ResponseFormat); Assert.Equal(0.2f, settings.Temperature); } @@ -77,7 +77,7 @@ public void ItClonesAllProperties() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "json", Temperature = 0.2f, Filename = "something.mp3", }; @@ -88,7 +88,7 @@ public void ItClonesAllProperties() Assert.Equal("model_id", clone.ModelId); Assert.Equal("en", clone.Language); Assert.Equal("prompt", clone.Prompt); - Assert.Equal(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, clone.ResponseFormat); + Assert.Equal("json", clone.ResponseFormat); Assert.Equal(0.2f, clone.Temperature); Assert.Equal("something.mp3", clone.Filename); } @@ -101,7 +101,7 @@ public void ItFreezesAndPreventsMutation() ModelId = "model_id", Language = "en", Prompt = "prompt", - ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple, + ResponseFormat = "vtt", Temperature = 0.2f, Filename = "something.mp3", }; @@ -112,7 +112,7 @@ public void ItFreezesAndPreventsMutation() Assert.Throws(() => settings.ModelId = "new_model"); Assert.Throws(() => settings.Language = "some_format"); Assert.Throws(() => settings.Prompt = "prompt"); - Assert.Throws(() => settings.ResponseFormat = OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple); + Assert.Throws(() => settings.ResponseFormat = "vtt"); Assert.Throws(() => settings.Temperature = 0.2f); Assert.Throws(() => settings.Filename = "something"); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs index 1de1af26e41a..8a652abae397 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -58,19 +58,14 @@ internal async Task> GetTextFromAudioContentsAsync( ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; - private static AudioTranscriptionFormat? ConvertResponseFormat(OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat? responseFormat) + private static AudioTranscriptionFormat ConvertResponseFormat(string responseFormat) { - if (responseFormat is null) - { - return null; - } - return responseFormat switch { - OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Simple => AudioTranscriptionFormat.Simple, - OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Verbose => AudioTranscriptionFormat.Verbose, - OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Vtt => AudioTranscriptionFormat.Vtt, - OpenAIAudioToTextExecutionSettings.AudioTranscriptionFormat.Srt => AudioTranscriptionFormat.Srt, + "json" => AudioTranscriptionFormat.Simple, + "verbose_json" => AudioTranscriptionFormat.Verbose, + "vtt" => AudioTranscriptionFormat.Vtt, + "srt" => AudioTranscriptionFormat.Srt, _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), }; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs index 9f162a041d76..94ec624e05b8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -26,7 +26,6 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// internal partial class ClientCore { - private const string ContentTokenLogProbabilitiesKey = "ContentTokenLogProbabilities"; private const string ModelProvider = "openai"; private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); @@ -97,7 +96,7 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { ContentTokenLogProbabilitiesKey, completions.ContentTokenLogProbabilities }, + { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, }; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs index b8651a31bd50..ce3366059763 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -61,11 +61,10 @@ public string? Prompt } /// - /// The format of the transcript output, in one of these options: Text, Simple, Verbose, Sttor vtt. Default is 'json'. + /// The format of the transcript output, in one of these options: json, srt, verbose_json, or vtt. Default is 'json'. /// [JsonPropertyName("response_format")] - [JsonConverter(typeof(JsonStringEnumConverter))] - public AudioTranscriptionFormat? ResponseFormat + public string ResponseFormat { get => this._responseFormat; @@ -180,7 +179,7 @@ public enum AudioTranscriptionFormat private const string DefaultFilename = "file.mp3"; private float _temperature = 0; - private AudioTranscriptionFormat? _responseFormat; + private string _responseFormat = "json"; private string _filename; private string? _language; private string? _prompt; diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs index 69509508af98..4dcb9d12ebe4 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -137,8 +137,7 @@ public async Task AzureOpenAIShouldReturnMetadataAsync() Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - // ContentFilterResults - Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); } [Theory(Skip = "This test is for manual verification.")] @@ -212,7 +211,7 @@ public async Task LogProbsDataIsReturnedWhenRequestedAsync(bool? logprobs, int? // Act var result = await kernel.InvokePromptAsync("Hi, can you help me today?", new(settings)); - var logProbabilityInfo = result.Metadata?["LogProbabilityInfo"] as IReadOnlyList; + var logProbabilityInfo = result.Metadata?["ContentTokenLogProbabilities"] as IReadOnlyList; // Assert Assert.NotNull(logProbabilityInfo); diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs index 5847ad29a6d1..84b1fe1d7ad2 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -60,7 +60,7 @@ public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); Assert.NotNull(createdAt); - Assert.True(result.Metadata.ContainsKey("PromptFilterResults")); + Assert.True(result.Metadata.ContainsKey("ContentFilterResultForPrompt")); Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); @@ -76,12 +76,12 @@ public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); - Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.True(result.Metadata.TryGetValue("ContentTokenLogProbabilities", out object? logProbabilityInfo)); Assert.Empty((logProbabilityInfo as IReadOnlyList)!); } @@ -123,7 +123,7 @@ public async Task TextGenerationShouldReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("CreatedAt", out object? createdAt)); Assert.NotNull(createdAt); - Assert.True(result.Metadata.ContainsKey("PromptFilterResults")); + Assert.True(result.Metadata.ContainsKey("ContentFilterResultForPrompt")); Assert.True(result.Metadata.ContainsKey("SystemFingerprint")); @@ -139,12 +139,12 @@ public async Task TextGenerationShouldReturnMetadataAsync() Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); Assert.NotEqual(0, completionTokens); - Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); + Assert.True(result.Metadata.ContainsKey("ContentFilterResultForResponse")); Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); - Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.True(result.Metadata.TryGetValue("ContentTokenLogProbabilities", out object? logProbabilityInfo)); Assert.Empty((logProbabilityInfo as IReadOnlyList)!); } diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs index e475682b8c13..cb4fce766456 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs @@ -209,7 +209,7 @@ public async Task LogProbsDataIsReturnedWhenRequestedAsync(bool? logprobs, int? // Act var result = await kernel.InvokePromptAsync("Hi, can you help me today?", new(settings)); - var logProbabilityInfo = result.Metadata?["LogProbabilityInfo"] as IReadOnlyList; + var logProbabilityInfo = result.Metadata?["ContentTokenLogProbabilities"] as IReadOnlyList; // Assert Assert.NotNull(logProbabilityInfo); diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs index 3314ee944bbd..54be93609b8d 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs @@ -77,7 +77,7 @@ public async Task ChatCompletionShouldUseChatHistoryAndReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); - Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.True(result.Metadata.TryGetValue("ContentTokenLogProbabilities", out object? logProbabilityInfo)); Assert.Empty((logProbabilityInfo as IReadOnlyList)!); } @@ -136,7 +136,7 @@ public async Task TextGenerationShouldReturnMetadataAsync() Assert.True(result.Metadata.TryGetValue("FinishReason", out object? finishReason)); Assert.Equal("Stop", finishReason); - Assert.True(result.Metadata.TryGetValue("LogProbabilityInfo", out object? logProbabilityInfo)); + Assert.True(result.Metadata.TryGetValue("ContentTokenLogProbabilities", out object? logProbabilityInfo)); Assert.Empty((logProbabilityInfo as IReadOnlyList)!); } From a3145a2bdead709280e8a2c6b78c28a968147d77 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 12 Jul 2024 10:54:38 +0100 Subject: [PATCH 072/226] .Net: Preparing grounds for Concepts OpenAI V2 migration (#7229) ### Motivation and Context - Removing reference to OpenAI V1 - Adding reference to OpenAI V2 - Adding reference to new AzureOpenAI (Needed for BaseTest utility) - Making all files not compilable (improve visibility on the changes for migration) - Removed ConceptsV2 Project - Removed references to projects there are dependent on OpenAI V1 and Clash with V2. - `Experimental\Agents\Experimental.Agents` - `Planners\Planners.OpenAI\Planners.OpenAI.csproj` - `Agents\OpenAI\Agents.OpenAI.csproj` --- dotnet/SK-dotnet.sln | 9 - dotnet/samples/Concepts/Concepts.csproj | 231 +++++++++++++++++++- dotnet/samples/ConceptsV2/ConceptsV2.csproj | 72 ------ 3 files changed, 227 insertions(+), 85 deletions(-) delete mode 100644 dotnet/samples/ConceptsV2/ConceptsV2.csproj diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 861cb4f49a96..22880718593d 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -319,8 +319,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2.UnitTests", "src\Connectors\Connectors.OpenAIV2.UnitTests\Connectors.OpenAIV2.UnitTests.csproj", "{A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConceptsV2", "samples\ConceptsV2\ConceptsV2.csproj", "{932B6B93-C297-47BE-A061-081ACC6105FB}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsV2", "src\IntegrationTestsV2\IntegrationTestsV2.csproj", "{FDEB4884-89B9-4656-80A0-57C7464490F7}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}" @@ -815,12 +813,6 @@ Global {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Publish|Any CPU.Build.0 = Debug|Any CPU {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.Build.0 = Release|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Debug|Any CPU.Build.0 = Debug|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Publish|Any CPU.Build.0 = Debug|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Release|Any CPU.ActiveCfg = Release|Any CPU - {932B6B93-C297-47BE-A061-081ACC6105FB}.Release|Any CPU.Build.0 = Release|Any CPU {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.Build.0 = Debug|Any CPU {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.ActiveCfg = Debug|Any CPU @@ -959,7 +951,6 @@ Global {B0B3901E-AF56-432B-8FAA-858468E5D0DF} = {24503383-A8C4-4255-9998-28D70FE8E99A} {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} - {932B6B93-C297-47BE-A061-081ACC6105FB} = {FA3720F1-C99A-49B2-9577-A940257098BF} {FDEB4884-89B9-4656-80A0-57C7464490F7} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {6744272E-8326-48CE-9A3F-6BE227A5E777} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {DB219924-208B-4CDD-8796-EE424689901E} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 5f81653e6dff..63dabdd45eb6 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -44,10 +44,11 @@ + - + @@ -62,8 +63,8 @@ - - + + @@ -72,7 +73,7 @@ - + @@ -103,4 +104,226 @@ Always + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/dotnet/samples/ConceptsV2/ConceptsV2.csproj b/dotnet/samples/ConceptsV2/ConceptsV2.csproj deleted file mode 100644 index a9fe41232166..000000000000 --- a/dotnet/samples/ConceptsV2/ConceptsV2.csproj +++ /dev/null @@ -1,72 +0,0 @@ - - - - Concepts - - net8.0 - enable - false - true - - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 - Library - 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - Always - - - From 5b30e3348252e4a0edad94b9a92cd780e9148967 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Fri, 12 Jul 2024 16:29:55 +0100 Subject: [PATCH 073/226] .Net: Remove unnecessary breaking changes (#7235) ### Motivation, Context, and Description This PR rolls back unnecessary breaking changes that are no longer relevant and can be easily reverted. --- .../Core/OpenAIChatMessageContentTests.cs | 4 ++-- .../Extensions/OpenAIPluginCollectionExtensionsTests.cs | 6 +++--- .../Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs | 4 ++-- .../Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs | 2 +- .../Extensions/OpenAIPluginCollectionExtensions.cs | 6 +++--- .../OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs index 7860c375e9cb..e638dc803be0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs @@ -41,8 +41,8 @@ public void GetOpenAIFunctionToolCallsReturnsCorrectList() var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); // Act - var actualToolCalls1 = content1.GetFunctionToolCalls(); - var actualToolCalls2 = content2.GetFunctionToolCalls(); + var actualToolCalls1 = content1.GetOpenAIFunctionToolCalls(); + var actualToolCalls2 = content2.GetOpenAIFunctionToolCalls(); // Assert Assert.Equal(2, actualToolCalls1.Count); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs index d46884600d8e..a1381fd231f4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs @@ -22,7 +22,7 @@ public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); // Act - var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); // Assert Assert.False(result); @@ -41,7 +41,7 @@ public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); // Act - var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); // Assert Assert.True(result); @@ -60,7 +60,7 @@ public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); // Act - var result = plugins.TryGetOpenAIFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); + var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); // Assert Assert.True(result); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs index 94ec624e05b8..97258077c589 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -248,7 +248,7 @@ internal async Task> GetChatMessageContentsAsy } // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetOpenAIFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; @@ -515,7 +515,7 @@ internal async IAsyncEnumerable GetStreamingC } // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetOpenAIFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs index 0bc00fdf81b2..3015fa09604f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs @@ -75,7 +75,7 @@ private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList /// Retrieve the resulting function from the chat result. /// /// The , or null if no function was returned by the model. - public IReadOnlyList GetFunctionToolCalls() + public IReadOnlyList GetOpenAIFunctionToolCalls() { List? functionToolCallList = null; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs index 2451cab7d399..91da7138f9e4 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs @@ -18,12 +18,12 @@ public static class OpenAIPluginCollectionExtensions /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, /// When this method returns, the arguments for the function; otherwise, /// if the function was found; otherwise, . - public static bool TryGetOpenAIFunctionAndArguments( + public static bool TryGetFunctionAndArguments( this IReadOnlyKernelPluginCollection plugins, ChatToolCall functionToolCall, [NotNullWhen(true)] out KernelFunction? function, out KernelArguments? arguments) => - plugins.TryGetOpenAIFunctionAndArguments(new OpenAIFunctionToolCall(functionToolCall), out function, out arguments); + plugins.TryGetFunctionAndArguments(new OpenAIFunctionToolCall(functionToolCall), out function, out arguments); /// /// Given an object, tries to retrieve the corresponding and populate with its parameters. @@ -33,7 +33,7 @@ public static bool TryGetOpenAIFunctionAndArguments( /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, /// When this method returns, the arguments for the function; otherwise, /// if the function was found; otherwise, . - public static bool TryGetOpenAIFunctionAndArguments( + public static bool TryGetFunctionAndArguments( this IReadOnlyKernelPluginCollection plugins, OpenAIFunctionToolCall functionToolCall, [NotNullWhen(true)] out KernelFunction? function, diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs index 62267c6eb691..dc503960eaf4 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs @@ -211,7 +211,7 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu // Iterating over the requested function calls and invoking them foreach (var toolCall in toolCalls) { - string content = kernel.Plugins.TryGetOpenAIFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? + string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : "Unable to find function. Please try again!"; From 5eba985d208038baeabaadf3659f075e0ea8aba9 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 08:51:11 -0700 Subject: [PATCH 074/226] Resolve merge from parent --- .../OpenAI/Logging/AssistantThreadActionsLogMessages.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs index bc7c8d9919f0..288abadde2dd 100644 --- a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs +++ b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI.Assistants; using Microsoft.Extensions.Logging; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 3016324a1920..182acac7c9c2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -262,8 +262,8 @@ protected override async Task CreateChannelAsync(CancellationToken this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); - return - new OpenAIAssistantChannel(this._client, thread.Id) + OpenAIAssistantChannel channel = + new (this._client, thread.Id) { Logger = this.LoggerFactory.CreateLogger() }; From 696652d5376733cbe743a3f5be1dfdd1a4e77a58 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:06:53 -0700 Subject: [PATCH 075/226] Resolve breaking changes (first pass) --- .../Agents/ComplexChat_NestedShopper.cs | 3 +- .../Concepts/Agents/Legacy_AgentTools.cs | 9 ++--- .../Concepts/Agents/MixedChat_Agents.cs | 10 ++++-- .../Agents/OpenAIAssistant_ChartMaker.cs | 18 ++++++---- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 14 +++++--- .../OpenAIAssistant_FileManipulation.cs | 23 ++++++------ ...ieval.cs => OpenAIAssistant_FileSearch.cs} | 30 +++++++++++----- dotnet/samples/Concepts/Concepts.csproj | 36 ++----------------- .../Agents/Experimental.Agents.csproj | 2 +- .../Agents/Extensions/OpenAIRestExtensions.cs | 3 +- .../src/Experimental/Agents/Internal/Agent.cs | 2 +- 11 files changed, 75 insertions(+), 75 deletions(-) rename dotnet/samples/Concepts/Agents/{OpenAIAssistant_Retrieval.cs => OpenAIAssistant_FileSearch.cs} (66%) diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index aae984906ba3..d75e4fad7026 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -5,6 +5,7 @@ using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; using Resources; namespace Agents; @@ -98,7 +99,7 @@ public async Task NestedChatWithAggregatorAgentAsync() { Console.WriteLine($"! {Model}"); - OpenAIPromptExecutionSettings jsonSettings = new() { ResponseFormat = ChatCompletionsResponseFormat.JsonObject }; + OpenAIPromptExecutionSettings jsonSettings = new() { ResponseFormat = ChatResponseFormat.JsonObject }; OpenAIPromptExecutionSettings autoInvokeSettings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; ChatCompletionAgent internalLeaderAgent = CreateAgent(InternalLeaderName, InternalLeaderInstructions); diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs index 66d93ecc88d9..c285810fcb74 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs @@ -165,13 +165,8 @@ async Task InvokeAgentAsync(IAgent agent, string question) } } - private static Kernel CreateFileEnabledKernel() - { - return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() : - Kernel.CreateBuilder().AddAzureOpenAIFiles(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ApiKey).Build(); - } + private static Kernel CreateFileEnabledKernel() => + Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build(); private static AgentBuilder CreateAgentBuilder() { diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index d3a894dd6c8e..20769fa030b7 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -47,12 +47,12 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync() OpenAIAssistantAgent agentWriter = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: this.GetOpenAIConfiguration(), definition: new() { Instructions = CopyWriterInstructions, Name = CopyWriterName, - ModelId = this.Model, + ModelName = this.Model, }); // Create a chat for agent interaction. @@ -94,4 +94,10 @@ private sealed class ApprovalTerminationStrategy : TerminationStrategy protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) => Task.FromResult(history[history.Count - 1].Content?.Contains("approve", StringComparison.OrdinalIgnoreCase) ?? false); } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index ef5ba80154fa..63ed511742f8 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -27,17 +27,17 @@ public async Task GenerateChartWithOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: GetOpenAIConfiguration(), new() { Instructions = AgentInstructions, Name = AgentName, EnableCodeInterpreter = true, - ModelId = this.Model, + ModelName = this.Model, }); // Create a chat for agent interaction. - AgentGroupChat chat = new(); + var chat = new AgentGroupChat(); // Respond to user input try @@ -54,7 +54,7 @@ Others 23 373 156 552 Sum 426 1622 856 2904 """); - await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); + await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); // %%% WHY NOT ??? } finally { @@ -68,18 +68,24 @@ async Task InvokeAgentAsync(string input) Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (var message in chat.InvokeAsync(agent)) { if (!string.IsNullOrWhiteSpace(message.Content)) { Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); } - foreach (FileReferenceContent fileReference in message.Items.OfType()) + foreach (var fileReference in message.Items.OfType()) { Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); } } } } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs index 75b237489025..646c1f244967 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -11,7 +11,7 @@ namespace Agents; /// public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) { - protected override bool ForceOpenAI => true; + protected override bool ForceOpenAI => false; [Fact] public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() @@ -20,15 +20,15 @@ public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: GetOpenAIConfiguration(), new() { EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, + ModelName = this.Model, }); // Create a chat for agent interaction. - AgentGroupChat chat = new(); + var chat = new AgentGroupChat(); // Respond to user input try @@ -53,4 +53,10 @@ async Task InvokeAgentAsync(string input) } } } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 8e64006ee9d3..a0fa5a074694 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -23,24 +23,21 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = await fileService.UploadContentAsync( new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); - Console.WriteLine(this.ApiKey); - // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: GetOpenAIConfiguration(), new() { + CodeInterpterFileIds = [uploadFile.Id], EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file + ModelName = this.Model, }); // Create a chat for agent interaction. @@ -62,15 +59,15 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + chat.AddChatMessage(new(AuthorRole.User, input)); Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - foreach (AnnotationContent annotation in content.Items.OfType()) + foreach (AnnotationContent annotation in message.Items.OfType()) { Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); @@ -80,4 +77,10 @@ async Task InvokeAgentAsync(string input) } } } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs similarity index 66% rename from dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs rename to dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 6f30b6974ff7..9fef5a0c5830 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.VectorStores; using Resources; namespace Agents; @@ -11,7 +12,7 @@ namespace Agents; /// /// Demonstrate using retrieval on . /// -public class OpenAIAssistant_Retrieval(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(output) { /// /// Retrieval tool not supported on Azure OpenAI. @@ -22,25 +23,30 @@ public class OpenAIAssistant_Retrieval(ITestOutputHelper output) : BaseTest(outp public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); + VectorStore vectorStore = + await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) + .AddFile(uploadFile.Id) + .CreateAsync(); + + OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); + // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config: GetOpenAIConfiguration(), new() { - EnableRetrieval = true, // Enable retrieval - ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file + ModelName = this.Model, + VectorStoreId = vectorStore.Id, }); // Create a chat for agent interaction. - AgentGroupChat chat = new(); + var chat = new AgentGroupChat(); // Respond to user input try @@ -52,6 +58,8 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); + await openAIStore.DeleteAsync(); + await fileService.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -61,10 +69,16 @@ async Task InvokeAgentAsync(string input) Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (var content in chat.InvokeAsync(agent)) { Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); } } } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 1ef7ed607096..7233a8131715 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -47,8 +47,8 @@ - - + + @@ -64,7 +64,7 @@ - + @@ -105,21 +105,6 @@ - - - - - - - - - - - - - - - @@ -216,21 +201,6 @@ - - - - - - - - - - - - - - - diff --git a/dotnet/src/Experimental/Agents/Experimental.Agents.csproj b/dotnet/src/Experimental/Agents/Experimental.Agents.csproj index b5038dbabde9..648d6b7fd02f 100644 --- a/dotnet/src/Experimental/Agents/Experimental.Agents.csproj +++ b/dotnet/src/Experimental/Agents/Experimental.Agents.csproj @@ -20,7 +20,7 @@ - + diff --git a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs index aa4f324490d8..c099f7d609e4 100644 --- a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs +++ b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs @@ -4,7 +4,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents.Exceptions; using Microsoft.SemanticKernel.Experimental.Agents.Internal; using Microsoft.SemanticKernel.Http; @@ -92,7 +91,7 @@ private static void AddHeaders(this HttpRequestMessage request, OpenAIRestContex { request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIFileService))); + request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(IAgent))); if (context.HasVersion) { diff --git a/dotnet/src/Experimental/Agents/Internal/Agent.cs b/dotnet/src/Experimental/Agents/Internal/Agent.cs index ae64af04d39a..c078703cda02 100644 --- a/dotnet/src/Experimental/Agents/Internal/Agent.cs +++ b/dotnet/src/Experimental/Agents/Internal/Agent.cs @@ -121,7 +121,7 @@ internal Agent( this.Kernel = this._restContext.HasVersion ? builder.AddAzureOpenAIChatCompletion(this._model.Model, this.GetAzureRootEndpoint(), this._restContext.ApiKey).Build() : - builder.AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey).Build(); + new(); // %%% HACK builder.AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey).Build(); if (plugins is not null) { From bb98e448500abb495d3b2a21109fa315dd4a9446 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:15:07 -0700 Subject: [PATCH 076/226] Remove ConceptsV2 shim --- .../Agents/OpenAIAssistant_ChartMaker.cs | 91 --- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 62 -- .../OpenAIAssistant_FileManipulation.cs | 86 --- .../Agents/OpenAIAssistant_FileSearch.cs | 84 --- dotnet/samples/ConceptsV2/ConceptsV2.csproj | 77 -- dotnet/samples/ConceptsV2/Resources/sales.csv | 701 ------------------ .../ConceptsV2/Resources/travelinfo.txt | 217 ------ 7 files changed, 1318 deletions(-) delete mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs delete mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs delete mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs delete mode 100644 dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs delete mode 100644 dotnet/samples/ConceptsV2/ConceptsV2.csproj delete mode 100644 dotnet/samples/ConceptsV2/Resources/sales.csv delete mode 100644 dotnet/samples/ConceptsV2/Resources/travelinfo.txt diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs deleted file mode 100644 index 63ed511742f8..000000000000 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_ChartMaker.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Agents; - -/// -/// Demonstrate using code-interpreter with to -/// produce image content displays the requested charts. -/// -public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(output) -{ - /// - /// Target Open AI services. - /// - protected override bool ForceOpenAI => true; - - private const string AgentName = "ChartMaker"; - private const string AgentInstructions = "Create charts as requested without explanation."; - - [Fact] - public async Task GenerateChartWithOpenAIAssistantAgentAsync() - { - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: GetOpenAIConfiguration(), - new() - { - Instructions = AgentInstructions, - Name = AgentName, - EnableCodeInterpreter = true, - ModelName = this.Model, - }); - - // Create a chat for agent interaction. - var chat = new AgentGroupChat(); - - // Respond to user input - try - { - await InvokeAgentAsync( - """ - Display this data using a bar-chart: - - Banding Brown Pink Yellow Sum - X00000 339 433 126 898 - X00300 48 421 222 691 - X12345 16 395 352 763 - Others 23 373 156 552 - Sum 426 1622 856 2904 - """); - - await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); // %%% WHY NOT ??? - } - finally - { - await agent.DeleteAsync(); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var message in chat.InvokeAsync(agent)) - { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } - - foreach (var fileReference in message.Items.OfType()) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); - } - } - } - } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); -} diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs deleted file mode 100644 index 646c1f244967..000000000000 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_CodeInterpreter.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Agents; - -/// -/// Demonstrate using code-interpreter on . -/// -public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) -{ - protected override bool ForceOpenAI => false; - - [Fact] - public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() - { - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: GetOpenAIConfiguration(), - new() - { - EnableCodeInterpreter = true, // Enable code-interpreter - ModelName = this.Model, - }); - - // Create a chat for agent interaction. - var chat = new AgentGroupChat(); - - // Respond to user input - try - { - await InvokeAgentAsync("Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); - } - finally - { - await agent.DeleteAsync(); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var content in chat.InvokeAsync(agent)) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } - } - } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); -} diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs deleted file mode 100644 index a0fa5a074694..000000000000 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileManipulation.cs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Text; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Resources; - -namespace Agents; - -/// -/// Demonstrate using code-interpreter to manipulate and generate csv files with . -/// -public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTest(output) -{ - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - - [Fact] - public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() - { - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), - new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); - - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: GetOpenAIConfiguration(), - new() - { - CodeInterpterFileIds = [uploadFile.Id], - EnableCodeInterpreter = true, // Enable code-interpreter - ModelName = this.Model, - }); - - // Create a chat for agent interaction. - AgentGroupChat chat = new(); - - // Respond to user input - try - { - await InvokeAgentAsync("Which segment had the most sales?"); - await InvokeAgentAsync("List the top 5 countries that generated the most profit."); - await InvokeAgentAsync("Create a tab delimited file report of profit by each country per month."); - } - finally - { - await agent.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - - foreach (AnnotationContent annotation in message.Items.OfType()) - { - Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - Console.WriteLine(Encoding.Default.GetString(byteContent)); - } - } - } - } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); -} diff --git a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs deleted file mode 100644 index 9fef5a0c5830..000000000000 --- a/dotnet/samples/ConceptsV2/Agents/OpenAIAssistant_FileSearch.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using OpenAI.VectorStores; -using Resources; - -namespace Agents; - -/// -/// Demonstrate using retrieval on . -/// -public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(output) -{ - /// - /// Retrieval tool not supported on Azure OpenAI. - /// - protected override bool ForceOpenAI => true; - - [Fact] - public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() - { - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); - - VectorStore vectorStore = - await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) - .AddFile(uploadFile.Id) - .CreateAsync(); - - OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); - - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: GetOpenAIConfiguration(), - new() - { - ModelName = this.Model, - VectorStoreId = vectorStore.Id, - }); - - // Create a chat for agent interaction. - var chat = new AgentGroupChat(); - - // Respond to user input - try - { - await InvokeAgentAsync("Where did sam go?"); - await InvokeAgentAsync("When does the flight leave Seattle?"); - await InvokeAgentAsync("What is the hotel contact info at the destination?"); - } - finally - { - await agent.DeleteAsync(); - await openAIStore.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var content in chat.InvokeAsync(agent)) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } - } - } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); -} diff --git a/dotnet/samples/ConceptsV2/ConceptsV2.csproj b/dotnet/samples/ConceptsV2/ConceptsV2.csproj deleted file mode 100644 index 8eee3868170b..000000000000 --- a/dotnet/samples/ConceptsV2/ConceptsV2.csproj +++ /dev/null @@ -1,77 +0,0 @@ - - - - Concepts - - net8.0 - enable - false - true - - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 - Library - 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - PreserveNewest - - - - - Always - - - diff --git a/dotnet/samples/ConceptsV2/Resources/sales.csv b/dotnet/samples/ConceptsV2/Resources/sales.csv deleted file mode 100644 index 4a355d11bf83..000000000000 --- a/dotnet/samples/ConceptsV2/Resources/sales.csv +++ /dev/null @@ -1,701 +0,0 @@ -Segment,Country,Product,Units Sold,Sale Price,Gross Sales,Discounts,Sales,COGS,Profit,Date,Month Number,Month Name,Year -Government,Canada,Carretera,1618.5,20.00,32370.00,0.00,32370.00,16185.00,16185.00,1/1/2014,1,January,2014 -Government,Germany,Carretera,1321,20.00,26420.00,0.00,26420.00,13210.00,13210.00,1/1/2014,1,January,2014 -Midmarket,France,Carretera,2178,15.00,32670.00,0.00,32670.00,21780.00,10890.00,6/1/2014,6,June,2014 -Midmarket,Germany,Carretera,888,15.00,13320.00,0.00,13320.00,8880.00,4440.00,6/1/2014,6,June,2014 -Midmarket,Mexico,Carretera,2470,15.00,37050.00,0.00,37050.00,24700.00,12350.00,6/1/2014,6,June,2014 -Government,Germany,Carretera,1513,350.00,529550.00,0.00,529550.00,393380.00,136170.00,12/1/2014,12,December,2014 -Midmarket,Germany,Montana,921,15.00,13815.00,0.00,13815.00,9210.00,4605.00,3/1/2014,3,March,2014 -Channel Partners,Canada,Montana,2518,12.00,30216.00,0.00,30216.00,7554.00,22662.00,6/1/2014,6,June,2014 -Government,France,Montana,1899,20.00,37980.00,0.00,37980.00,18990.00,18990.00,6/1/2014,6,June,2014 -Channel Partners,Germany,Montana,1545,12.00,18540.00,0.00,18540.00,4635.00,13905.00,6/1/2014,6,June,2014 -Midmarket,Mexico,Montana,2470,15.00,37050.00,0.00,37050.00,24700.00,12350.00,6/1/2014,6,June,2014 -Enterprise,Canada,Montana,2665.5,125.00,333187.50,0.00,333187.50,319860.00,13327.50,7/1/2014,7,July,2014 -Small Business,Mexico,Montana,958,300.00,287400.00,0.00,287400.00,239500.00,47900.00,8/1/2014,8,August,2014 -Government,Germany,Montana,2146,7.00,15022.00,0.00,15022.00,10730.00,4292.00,9/1/2014,9,September,2014 -Enterprise,Canada,Montana,345,125.00,43125.00,0.00,43125.00,41400.00,1725.00,10/1/2013,10,October,2013 -Midmarket,United States of America,Montana,615,15.00,9225.00,0.00,9225.00,6150.00,3075.00,12/1/2014,12,December,2014 -Government,Canada,Paseo,292,20.00,5840.00,0.00,5840.00,2920.00,2920.00,2/1/2014,2,February,2014 -Midmarket,Mexico,Paseo,974,15.00,14610.00,0.00,14610.00,9740.00,4870.00,2/1/2014,2,February,2014 -Channel Partners,Canada,Paseo,2518,12.00,30216.00,0.00,30216.00,7554.00,22662.00,6/1/2014,6,June,2014 -Government,Germany,Paseo,1006,350.00,352100.00,0.00,352100.00,261560.00,90540.00,6/1/2014,6,June,2014 -Channel Partners,Germany,Paseo,367,12.00,4404.00,0.00,4404.00,1101.00,3303.00,7/1/2014,7,July,2014 -Government,Mexico,Paseo,883,7.00,6181.00,0.00,6181.00,4415.00,1766.00,8/1/2014,8,August,2014 -Midmarket,France,Paseo,549,15.00,8235.00,0.00,8235.00,5490.00,2745.00,9/1/2013,9,September,2013 -Small Business,Mexico,Paseo,788,300.00,236400.00,0.00,236400.00,197000.00,39400.00,9/1/2013,9,September,2013 -Midmarket,Mexico,Paseo,2472,15.00,37080.00,0.00,37080.00,24720.00,12360.00,9/1/2014,9,September,2014 -Government,United States of America,Paseo,1143,7.00,8001.00,0.00,8001.00,5715.00,2286.00,10/1/2014,10,October,2014 -Government,Canada,Paseo,1725,350.00,603750.00,0.00,603750.00,448500.00,155250.00,11/1/2013,11,November,2013 -Channel Partners,United States of America,Paseo,912,12.00,10944.00,0.00,10944.00,2736.00,8208.00,11/1/2013,11,November,2013 -Midmarket,Canada,Paseo,2152,15.00,32280.00,0.00,32280.00,21520.00,10760.00,12/1/2013,12,December,2013 -Government,Canada,Paseo,1817,20.00,36340.00,0.00,36340.00,18170.00,18170.00,12/1/2014,12,December,2014 -Government,Germany,Paseo,1513,350.00,529550.00,0.00,529550.00,393380.00,136170.00,12/1/2014,12,December,2014 -Government,Mexico,Velo,1493,7.00,10451.00,0.00,10451.00,7465.00,2986.00,1/1/2014,1,January,2014 -Enterprise,France,Velo,1804,125.00,225500.00,0.00,225500.00,216480.00,9020.00,2/1/2014,2,February,2014 -Channel Partners,Germany,Velo,2161,12.00,25932.00,0.00,25932.00,6483.00,19449.00,3/1/2014,3,March,2014 -Government,Germany,Velo,1006,350.00,352100.00,0.00,352100.00,261560.00,90540.00,6/1/2014,6,June,2014 -Channel Partners,Germany,Velo,1545,12.00,18540.00,0.00,18540.00,4635.00,13905.00,6/1/2014,6,June,2014 -Enterprise,United States of America,Velo,2821,125.00,352625.00,0.00,352625.00,338520.00,14105.00,8/1/2014,8,August,2014 -Enterprise,Canada,Velo,345,125.00,43125.00,0.00,43125.00,41400.00,1725.00,10/1/2013,10,October,2013 -Small Business,Canada,VTT,2001,300.00,600300.00,0.00,600300.00,500250.00,100050.00,2/1/2014,2,February,2014 -Channel Partners,Germany,VTT,2838,12.00,34056.00,0.00,34056.00,8514.00,25542.00,4/1/2014,4,April,2014 -Midmarket,France,VTT,2178,15.00,32670.00,0.00,32670.00,21780.00,10890.00,6/1/2014,6,June,2014 -Midmarket,Germany,VTT,888,15.00,13320.00,0.00,13320.00,8880.00,4440.00,6/1/2014,6,June,2014 -Government,France,VTT,1527,350.00,534450.00,0.00,534450.00,397020.00,137430.00,9/1/2013,9,September,2013 -Small Business,France,VTT,2151,300.00,645300.00,0.00,645300.00,537750.00,107550.00,9/1/2014,9,September,2014 -Government,Canada,VTT,1817,20.00,36340.00,0.00,36340.00,18170.00,18170.00,12/1/2014,12,December,2014 -Government,France,Amarilla,2750,350.00,962500.00,0.00,962500.00,715000.00,247500.00,2/1/2014,2,February,2014 -Channel Partners,United States of America,Amarilla,1953,12.00,23436.00,0.00,23436.00,5859.00,17577.00,4/1/2014,4,April,2014 -Enterprise,Germany,Amarilla,4219.5,125.00,527437.50,0.00,527437.50,506340.00,21097.50,4/1/2014,4,April,2014 -Government,France,Amarilla,1899,20.00,37980.00,0.00,37980.00,18990.00,18990.00,6/1/2014,6,June,2014 -Government,Germany,Amarilla,1686,7.00,11802.00,0.00,11802.00,8430.00,3372.00,7/1/2014,7,July,2014 -Channel Partners,United States of America,Amarilla,2141,12.00,25692.00,0.00,25692.00,6423.00,19269.00,8/1/2014,8,August,2014 -Government,United States of America,Amarilla,1143,7.00,8001.00,0.00,8001.00,5715.00,2286.00,10/1/2014,10,October,2014 -Midmarket,United States of America,Amarilla,615,15.00,9225.00,0.00,9225.00,6150.00,3075.00,12/1/2014,12,December,2014 -Government,France,Paseo,3945,7.00,27615.00,276.15,27338.85,19725.00,7613.85,1/1/2014,1,January,2014 -Midmarket,France,Paseo,2296,15.00,34440.00,344.40,34095.60,22960.00,11135.60,2/1/2014,2,February,2014 -Government,France,Paseo,1030,7.00,7210.00,72.10,7137.90,5150.00,1987.90,5/1/2014,5,May,2014 -Government,France,Velo,639,7.00,4473.00,44.73,4428.27,3195.00,1233.27,11/1/2014,11,November,2014 -Government,Canada,VTT,1326,7.00,9282.00,92.82,9189.18,6630.00,2559.18,3/1/2014,3,March,2014 -Channel Partners,United States of America,Carretera,1858,12.00,22296.00,222.96,22073.04,5574.00,16499.04,2/1/2014,2,February,2014 -Government,Mexico,Carretera,1210,350.00,423500.00,4235.00,419265.00,314600.00,104665.00,3/1/2014,3,March,2014 -Government,United States of America,Carretera,2529,7.00,17703.00,177.03,17525.97,12645.00,4880.97,7/1/2014,7,July,2014 -Channel Partners,Canada,Carretera,1445,12.00,17340.00,173.40,17166.60,4335.00,12831.60,9/1/2014,9,September,2014 -Enterprise,United States of America,Carretera,330,125.00,41250.00,412.50,40837.50,39600.00,1237.50,9/1/2013,9,September,2013 -Channel Partners,France,Carretera,2671,12.00,32052.00,320.52,31731.48,8013.00,23718.48,9/1/2014,9,September,2014 -Channel Partners,Germany,Carretera,766,12.00,9192.00,91.92,9100.08,2298.00,6802.08,10/1/2013,10,October,2013 -Small Business,Mexico,Carretera,494,300.00,148200.00,1482.00,146718.00,123500.00,23218.00,10/1/2013,10,October,2013 -Government,Mexico,Carretera,1397,350.00,488950.00,4889.50,484060.50,363220.00,120840.50,10/1/2014,10,October,2014 -Government,France,Carretera,2155,350.00,754250.00,7542.50,746707.50,560300.00,186407.50,12/1/2014,12,December,2014 -Midmarket,Mexico,Montana,2214,15.00,33210.00,332.10,32877.90,22140.00,10737.90,3/1/2014,3,March,2014 -Small Business,United States of America,Montana,2301,300.00,690300.00,6903.00,683397.00,575250.00,108147.00,4/1/2014,4,April,2014 -Government,France,Montana,1375.5,20.00,27510.00,275.10,27234.90,13755.00,13479.90,7/1/2014,7,July,2014 -Government,Canada,Montana,1830,7.00,12810.00,128.10,12681.90,9150.00,3531.90,8/1/2014,8,August,2014 -Small Business,United States of America,Montana,2498,300.00,749400.00,7494.00,741906.00,624500.00,117406.00,9/1/2013,9,September,2013 -Enterprise,United States of America,Montana,663,125.00,82875.00,828.75,82046.25,79560.00,2486.25,10/1/2013,10,October,2013 -Midmarket,United States of America,Paseo,1514,15.00,22710.00,227.10,22482.90,15140.00,7342.90,2/1/2014,2,February,2014 -Government,United States of America,Paseo,4492.5,7.00,31447.50,314.48,31133.03,22462.50,8670.53,4/1/2014,4,April,2014 -Enterprise,United States of America,Paseo,727,125.00,90875.00,908.75,89966.25,87240.00,2726.25,6/1/2014,6,June,2014 -Enterprise,France,Paseo,787,125.00,98375.00,983.75,97391.25,94440.00,2951.25,6/1/2014,6,June,2014 -Enterprise,Mexico,Paseo,1823,125.00,227875.00,2278.75,225596.25,218760.00,6836.25,7/1/2014,7,July,2014 -Midmarket,Germany,Paseo,747,15.00,11205.00,112.05,11092.95,7470.00,3622.95,9/1/2014,9,September,2014 -Channel Partners,Germany,Paseo,766,12.00,9192.00,91.92,9100.08,2298.00,6802.08,10/1/2013,10,October,2013 -Small Business,United States of America,Paseo,2905,300.00,871500.00,8715.00,862785.00,726250.00,136535.00,11/1/2014,11,November,2014 -Government,France,Paseo,2155,350.00,754250.00,7542.50,746707.50,560300.00,186407.50,12/1/2014,12,December,2014 -Government,France,Velo,3864,20.00,77280.00,772.80,76507.20,38640.00,37867.20,4/1/2014,4,April,2014 -Government,Mexico,Velo,362,7.00,2534.00,25.34,2508.66,1810.00,698.66,5/1/2014,5,May,2014 -Enterprise,Canada,Velo,923,125.00,115375.00,1153.75,114221.25,110760.00,3461.25,8/1/2014,8,August,2014 -Enterprise,United States of America,Velo,663,125.00,82875.00,828.75,82046.25,79560.00,2486.25,10/1/2013,10,October,2013 -Government,Canada,Velo,2092,7.00,14644.00,146.44,14497.56,10460.00,4037.56,11/1/2013,11,November,2013 -Government,Germany,VTT,263,7.00,1841.00,18.41,1822.59,1315.00,507.59,3/1/2014,3,March,2014 -Government,Canada,VTT,943.5,350.00,330225.00,3302.25,326922.75,245310.00,81612.75,4/1/2014,4,April,2014 -Enterprise,United States of America,VTT,727,125.00,90875.00,908.75,89966.25,87240.00,2726.25,6/1/2014,6,June,2014 -Enterprise,France,VTT,787,125.00,98375.00,983.75,97391.25,94440.00,2951.25,6/1/2014,6,June,2014 -Small Business,Germany,VTT,986,300.00,295800.00,2958.00,292842.00,246500.00,46342.00,9/1/2014,9,September,2014 -Small Business,Mexico,VTT,494,300.00,148200.00,1482.00,146718.00,123500.00,23218.00,10/1/2013,10,October,2013 -Government,Mexico,VTT,1397,350.00,488950.00,4889.50,484060.50,363220.00,120840.50,10/1/2014,10,October,2014 -Enterprise,France,VTT,1744,125.00,218000.00,2180.00,215820.00,209280.00,6540.00,11/1/2014,11,November,2014 -Channel Partners,United States of America,Amarilla,1989,12.00,23868.00,238.68,23629.32,5967.00,17662.32,9/1/2013,9,September,2013 -Midmarket,France,Amarilla,321,15.00,4815.00,48.15,4766.85,3210.00,1556.85,11/1/2013,11,November,2013 -Enterprise,Canada,Carretera,742.5,125.00,92812.50,1856.25,90956.25,89100.00,1856.25,4/1/2014,4,April,2014 -Channel Partners,Canada,Carretera,1295,12.00,15540.00,310.80,15229.20,3885.00,11344.20,10/1/2014,10,October,2014 -Small Business,Germany,Carretera,214,300.00,64200.00,1284.00,62916.00,53500.00,9416.00,10/1/2013,10,October,2013 -Government,France,Carretera,2145,7.00,15015.00,300.30,14714.70,10725.00,3989.70,11/1/2013,11,November,2013 -Government,Canada,Carretera,2852,350.00,998200.00,19964.00,978236.00,741520.00,236716.00,12/1/2014,12,December,2014 -Channel Partners,United States of America,Montana,1142,12.00,13704.00,274.08,13429.92,3426.00,10003.92,6/1/2014,6,June,2014 -Government,United States of America,Montana,1566,20.00,31320.00,626.40,30693.60,15660.00,15033.60,10/1/2014,10,October,2014 -Channel Partners,Mexico,Montana,690,12.00,8280.00,165.60,8114.40,2070.00,6044.40,11/1/2014,11,November,2014 -Enterprise,Mexico,Montana,1660,125.00,207500.00,4150.00,203350.00,199200.00,4150.00,11/1/2013,11,November,2013 -Midmarket,Canada,Paseo,2363,15.00,35445.00,708.90,34736.10,23630.00,11106.10,2/1/2014,2,February,2014 -Small Business,France,Paseo,918,300.00,275400.00,5508.00,269892.00,229500.00,40392.00,5/1/2014,5,May,2014 -Small Business,Germany,Paseo,1728,300.00,518400.00,10368.00,508032.00,432000.00,76032.00,5/1/2014,5,May,2014 -Channel Partners,United States of America,Paseo,1142,12.00,13704.00,274.08,13429.92,3426.00,10003.92,6/1/2014,6,June,2014 -Enterprise,Mexico,Paseo,662,125.00,82750.00,1655.00,81095.00,79440.00,1655.00,6/1/2014,6,June,2014 -Channel Partners,Canada,Paseo,1295,12.00,15540.00,310.80,15229.20,3885.00,11344.20,10/1/2014,10,October,2014 -Enterprise,Germany,Paseo,809,125.00,101125.00,2022.50,99102.50,97080.00,2022.50,10/1/2013,10,October,2013 -Enterprise,Mexico,Paseo,2145,125.00,268125.00,5362.50,262762.50,257400.00,5362.50,10/1/2013,10,October,2013 -Channel Partners,France,Paseo,1785,12.00,21420.00,428.40,20991.60,5355.00,15636.60,11/1/2013,11,November,2013 -Small Business,Canada,Paseo,1916,300.00,574800.00,11496.00,563304.00,479000.00,84304.00,12/1/2014,12,December,2014 -Government,Canada,Paseo,2852,350.00,998200.00,19964.00,978236.00,741520.00,236716.00,12/1/2014,12,December,2014 -Enterprise,Canada,Paseo,2729,125.00,341125.00,6822.50,334302.50,327480.00,6822.50,12/1/2014,12,December,2014 -Midmarket,United States of America,Paseo,1925,15.00,28875.00,577.50,28297.50,19250.00,9047.50,12/1/2013,12,December,2013 -Government,United States of America,Paseo,2013,7.00,14091.00,281.82,13809.18,10065.00,3744.18,12/1/2013,12,December,2013 -Channel Partners,France,Paseo,1055,12.00,12660.00,253.20,12406.80,3165.00,9241.80,12/1/2014,12,December,2014 -Channel Partners,Mexico,Paseo,1084,12.00,13008.00,260.16,12747.84,3252.00,9495.84,12/1/2014,12,December,2014 -Government,United States of America,Velo,1566,20.00,31320.00,626.40,30693.60,15660.00,15033.60,10/1/2014,10,October,2014 -Government,Germany,Velo,2966,350.00,1038100.00,20762.00,1017338.00,771160.00,246178.00,10/1/2013,10,October,2013 -Government,Germany,Velo,2877,350.00,1006950.00,20139.00,986811.00,748020.00,238791.00,10/1/2014,10,October,2014 -Enterprise,Germany,Velo,809,125.00,101125.00,2022.50,99102.50,97080.00,2022.50,10/1/2013,10,October,2013 -Enterprise,Mexico,Velo,2145,125.00,268125.00,5362.50,262762.50,257400.00,5362.50,10/1/2013,10,October,2013 -Channel Partners,France,Velo,1055,12.00,12660.00,253.20,12406.80,3165.00,9241.80,12/1/2014,12,December,2014 -Government,Mexico,Velo,544,20.00,10880.00,217.60,10662.40,5440.00,5222.40,12/1/2013,12,December,2013 -Channel Partners,Mexico,Velo,1084,12.00,13008.00,260.16,12747.84,3252.00,9495.84,12/1/2014,12,December,2014 -Enterprise,Mexico,VTT,662,125.00,82750.00,1655.00,81095.00,79440.00,1655.00,6/1/2014,6,June,2014 -Small Business,Germany,VTT,214,300.00,64200.00,1284.00,62916.00,53500.00,9416.00,10/1/2013,10,October,2013 -Government,Germany,VTT,2877,350.00,1006950.00,20139.00,986811.00,748020.00,238791.00,10/1/2014,10,October,2014 -Enterprise,Canada,VTT,2729,125.00,341125.00,6822.50,334302.50,327480.00,6822.50,12/1/2014,12,December,2014 -Government,United States of America,VTT,266,350.00,93100.00,1862.00,91238.00,69160.00,22078.00,12/1/2013,12,December,2013 -Government,Mexico,VTT,1940,350.00,679000.00,13580.00,665420.00,504400.00,161020.00,12/1/2013,12,December,2013 -Small Business,Germany,Amarilla,259,300.00,77700.00,1554.00,76146.00,64750.00,11396.00,3/1/2014,3,March,2014 -Small Business,Mexico,Amarilla,1101,300.00,330300.00,6606.00,323694.00,275250.00,48444.00,3/1/2014,3,March,2014 -Enterprise,Germany,Amarilla,2276,125.00,284500.00,5690.00,278810.00,273120.00,5690.00,5/1/2014,5,May,2014 -Government,Germany,Amarilla,2966,350.00,1038100.00,20762.00,1017338.00,771160.00,246178.00,10/1/2013,10,October,2013 -Government,United States of America,Amarilla,1236,20.00,24720.00,494.40,24225.60,12360.00,11865.60,11/1/2014,11,November,2014 -Government,France,Amarilla,941,20.00,18820.00,376.40,18443.60,9410.00,9033.60,11/1/2014,11,November,2014 -Small Business,Canada,Amarilla,1916,300.00,574800.00,11496.00,563304.00,479000.00,84304.00,12/1/2014,12,December,2014 -Enterprise,France,Carretera,4243.5,125.00,530437.50,15913.13,514524.38,509220.00,5304.38,4/1/2014,4,April,2014 -Government,Germany,Carretera,2580,20.00,51600.00,1548.00,50052.00,25800.00,24252.00,4/1/2014,4,April,2014 -Small Business,Germany,Carretera,689,300.00,206700.00,6201.00,200499.00,172250.00,28249.00,6/1/2014,6,June,2014 -Channel Partners,United States of America,Carretera,1947,12.00,23364.00,700.92,22663.08,5841.00,16822.08,9/1/2014,9,September,2014 -Channel Partners,Canada,Carretera,908,12.00,10896.00,326.88,10569.12,2724.00,7845.12,12/1/2013,12,December,2013 -Government,Germany,Montana,1958,7.00,13706.00,411.18,13294.82,9790.00,3504.82,2/1/2014,2,February,2014 -Channel Partners,France,Montana,1901,12.00,22812.00,684.36,22127.64,5703.00,16424.64,6/1/2014,6,June,2014 -Government,France,Montana,544,7.00,3808.00,114.24,3693.76,2720.00,973.76,9/1/2014,9,September,2014 -Government,Germany,Montana,1797,350.00,628950.00,18868.50,610081.50,467220.00,142861.50,9/1/2013,9,September,2013 -Enterprise,France,Montana,1287,125.00,160875.00,4826.25,156048.75,154440.00,1608.75,12/1/2014,12,December,2014 -Enterprise,Germany,Montana,1706,125.00,213250.00,6397.50,206852.50,204720.00,2132.50,12/1/2014,12,December,2014 -Small Business,France,Paseo,2434.5,300.00,730350.00,21910.50,708439.50,608625.00,99814.50,1/1/2014,1,January,2014 -Enterprise,Canada,Paseo,1774,125.00,221750.00,6652.50,215097.50,212880.00,2217.50,3/1/2014,3,March,2014 -Channel Partners,France,Paseo,1901,12.00,22812.00,684.36,22127.64,5703.00,16424.64,6/1/2014,6,June,2014 -Small Business,Germany,Paseo,689,300.00,206700.00,6201.00,200499.00,172250.00,28249.00,6/1/2014,6,June,2014 -Enterprise,Germany,Paseo,1570,125.00,196250.00,5887.50,190362.50,188400.00,1962.50,6/1/2014,6,June,2014 -Channel Partners,United States of America,Paseo,1369.5,12.00,16434.00,493.02,15940.98,4108.50,11832.48,7/1/2014,7,July,2014 -Enterprise,Canada,Paseo,2009,125.00,251125.00,7533.75,243591.25,241080.00,2511.25,10/1/2014,10,October,2014 -Midmarket,Germany,Paseo,1945,15.00,29175.00,875.25,28299.75,19450.00,8849.75,10/1/2013,10,October,2013 -Enterprise,France,Paseo,1287,125.00,160875.00,4826.25,156048.75,154440.00,1608.75,12/1/2014,12,December,2014 -Enterprise,Germany,Paseo,1706,125.00,213250.00,6397.50,206852.50,204720.00,2132.50,12/1/2014,12,December,2014 -Enterprise,Canada,Velo,2009,125.00,251125.00,7533.75,243591.25,241080.00,2511.25,10/1/2014,10,October,2014 -Small Business,United States of America,VTT,2844,300.00,853200.00,25596.00,827604.00,711000.00,116604.00,2/1/2014,2,February,2014 -Channel Partners,Mexico,VTT,1916,12.00,22992.00,689.76,22302.24,5748.00,16554.24,4/1/2014,4,April,2014 -Enterprise,Germany,VTT,1570,125.00,196250.00,5887.50,190362.50,188400.00,1962.50,6/1/2014,6,June,2014 -Small Business,Canada,VTT,1874,300.00,562200.00,16866.00,545334.00,468500.00,76834.00,8/1/2014,8,August,2014 -Government,Mexico,VTT,1642,350.00,574700.00,17241.00,557459.00,426920.00,130539.00,8/1/2014,8,August,2014 -Midmarket,Germany,VTT,1945,15.00,29175.00,875.25,28299.75,19450.00,8849.75,10/1/2013,10,October,2013 -Government,Canada,Carretera,831,20.00,16620.00,498.60,16121.40,8310.00,7811.40,5/1/2014,5,May,2014 -Government,Mexico,Paseo,1760,7.00,12320.00,369.60,11950.40,8800.00,3150.40,9/1/2013,9,September,2013 -Government,Canada,Velo,3850.5,20.00,77010.00,2310.30,74699.70,38505.00,36194.70,4/1/2014,4,April,2014 -Channel Partners,Germany,VTT,2479,12.00,29748.00,892.44,28855.56,7437.00,21418.56,1/1/2014,1,January,2014 -Midmarket,Mexico,Montana,2031,15.00,30465.00,1218.60,29246.40,20310.00,8936.40,10/1/2014,10,October,2014 -Midmarket,Mexico,Paseo,2031,15.00,30465.00,1218.60,29246.40,20310.00,8936.40,10/1/2014,10,October,2014 -Midmarket,France,Paseo,2261,15.00,33915.00,1356.60,32558.40,22610.00,9948.40,12/1/2013,12,December,2013 -Government,United States of America,Velo,736,20.00,14720.00,588.80,14131.20,7360.00,6771.20,9/1/2013,9,September,2013 -Government,Canada,Carretera,2851,7.00,19957.00,798.28,19158.72,14255.00,4903.72,10/1/2013,10,October,2013 -Small Business,Germany,Carretera,2021,300.00,606300.00,24252.00,582048.00,505250.00,76798.00,10/1/2014,10,October,2014 -Government,United States of America,Carretera,274,350.00,95900.00,3836.00,92064.00,71240.00,20824.00,12/1/2014,12,December,2014 -Midmarket,Canada,Montana,1967,15.00,29505.00,1180.20,28324.80,19670.00,8654.80,3/1/2014,3,March,2014 -Small Business,Germany,Montana,1859,300.00,557700.00,22308.00,535392.00,464750.00,70642.00,8/1/2014,8,August,2014 -Government,Canada,Montana,2851,7.00,19957.00,798.28,19158.72,14255.00,4903.72,10/1/2013,10,October,2013 -Small Business,Germany,Montana,2021,300.00,606300.00,24252.00,582048.00,505250.00,76798.00,10/1/2014,10,October,2014 -Enterprise,Mexico,Montana,1138,125.00,142250.00,5690.00,136560.00,136560.00,0.00,12/1/2014,12,December,2014 -Government,Canada,Paseo,4251,7.00,29757.00,1190.28,28566.72,21255.00,7311.72,1/1/2014,1,January,2014 -Enterprise,Germany,Paseo,795,125.00,99375.00,3975.00,95400.00,95400.00,0.00,3/1/2014,3,March,2014 -Small Business,Germany,Paseo,1414.5,300.00,424350.00,16974.00,407376.00,353625.00,53751.00,4/1/2014,4,April,2014 -Small Business,United States of America,Paseo,2918,300.00,875400.00,35016.00,840384.00,729500.00,110884.00,5/1/2014,5,May,2014 -Government,United States of America,Paseo,3450,350.00,1207500.00,48300.00,1159200.00,897000.00,262200.00,7/1/2014,7,July,2014 -Enterprise,France,Paseo,2988,125.00,373500.00,14940.00,358560.00,358560.00,0.00,7/1/2014,7,July,2014 -Midmarket,Canada,Paseo,218,15.00,3270.00,130.80,3139.20,2180.00,959.20,9/1/2014,9,September,2014 -Government,Canada,Paseo,2074,20.00,41480.00,1659.20,39820.80,20740.00,19080.80,9/1/2014,9,September,2014 -Government,United States of America,Paseo,1056,20.00,21120.00,844.80,20275.20,10560.00,9715.20,9/1/2014,9,September,2014 -Midmarket,United States of America,Paseo,671,15.00,10065.00,402.60,9662.40,6710.00,2952.40,10/1/2013,10,October,2013 -Midmarket,Mexico,Paseo,1514,15.00,22710.00,908.40,21801.60,15140.00,6661.60,10/1/2013,10,October,2013 -Government,United States of America,Paseo,274,350.00,95900.00,3836.00,92064.00,71240.00,20824.00,12/1/2014,12,December,2014 -Enterprise,Mexico,Paseo,1138,125.00,142250.00,5690.00,136560.00,136560.00,0.00,12/1/2014,12,December,2014 -Channel Partners,United States of America,Velo,1465,12.00,17580.00,703.20,16876.80,4395.00,12481.80,3/1/2014,3,March,2014 -Government,Canada,Velo,2646,20.00,52920.00,2116.80,50803.20,26460.00,24343.20,9/1/2013,9,September,2013 -Government,France,Velo,2177,350.00,761950.00,30478.00,731472.00,566020.00,165452.00,10/1/2014,10,October,2014 -Channel Partners,France,VTT,866,12.00,10392.00,415.68,9976.32,2598.00,7378.32,5/1/2014,5,May,2014 -Government,United States of America,VTT,349,350.00,122150.00,4886.00,117264.00,90740.00,26524.00,9/1/2013,9,September,2013 -Government,France,VTT,2177,350.00,761950.00,30478.00,731472.00,566020.00,165452.00,10/1/2014,10,October,2014 -Midmarket,Mexico,VTT,1514,15.00,22710.00,908.40,21801.60,15140.00,6661.60,10/1/2013,10,October,2013 -Government,Mexico,Amarilla,1865,350.00,652750.00,26110.00,626640.00,484900.00,141740.00,2/1/2014,2,February,2014 -Enterprise,Mexico,Amarilla,1074,125.00,134250.00,5370.00,128880.00,128880.00,0.00,4/1/2014,4,April,2014 -Government,Germany,Amarilla,1907,350.00,667450.00,26698.00,640752.00,495820.00,144932.00,9/1/2014,9,September,2014 -Midmarket,United States of America,Amarilla,671,15.00,10065.00,402.60,9662.40,6710.00,2952.40,10/1/2013,10,October,2013 -Government,Canada,Amarilla,1778,350.00,622300.00,24892.00,597408.00,462280.00,135128.00,12/1/2013,12,December,2013 -Government,Germany,Montana,1159,7.00,8113.00,405.65,7707.35,5795.00,1912.35,10/1/2013,10,October,2013 -Government,Germany,Paseo,1372,7.00,9604.00,480.20,9123.80,6860.00,2263.80,1/1/2014,1,January,2014 -Government,Canada,Paseo,2349,7.00,16443.00,822.15,15620.85,11745.00,3875.85,9/1/2013,9,September,2013 -Government,Mexico,Paseo,2689,7.00,18823.00,941.15,17881.85,13445.00,4436.85,10/1/2014,10,October,2014 -Channel Partners,Canada,Paseo,2431,12.00,29172.00,1458.60,27713.40,7293.00,20420.40,12/1/2014,12,December,2014 -Channel Partners,Canada,Velo,2431,12.00,29172.00,1458.60,27713.40,7293.00,20420.40,12/1/2014,12,December,2014 -Government,Mexico,VTT,2689,7.00,18823.00,941.15,17881.85,13445.00,4436.85,10/1/2014,10,October,2014 -Government,Mexico,Amarilla,1683,7.00,11781.00,589.05,11191.95,8415.00,2776.95,7/1/2014,7,July,2014 -Channel Partners,Mexico,Amarilla,1123,12.00,13476.00,673.80,12802.20,3369.00,9433.20,8/1/2014,8,August,2014 -Government,Germany,Amarilla,1159,7.00,8113.00,405.65,7707.35,5795.00,1912.35,10/1/2013,10,October,2013 -Channel Partners,France,Carretera,1865,12.00,22380.00,1119.00,21261.00,5595.00,15666.00,2/1/2014,2,February,2014 -Channel Partners,Germany,Carretera,1116,12.00,13392.00,669.60,12722.40,3348.00,9374.40,2/1/2014,2,February,2014 -Government,France,Carretera,1563,20.00,31260.00,1563.00,29697.00,15630.00,14067.00,5/1/2014,5,May,2014 -Small Business,United States of America,Carretera,991,300.00,297300.00,14865.00,282435.00,247750.00,34685.00,6/1/2014,6,June,2014 -Government,Germany,Carretera,1016,7.00,7112.00,355.60,6756.40,5080.00,1676.40,11/1/2013,11,November,2013 -Midmarket,Mexico,Carretera,2791,15.00,41865.00,2093.25,39771.75,27910.00,11861.75,11/1/2014,11,November,2014 -Government,United States of America,Carretera,570,7.00,3990.00,199.50,3790.50,2850.00,940.50,12/1/2014,12,December,2014 -Government,France,Carretera,2487,7.00,17409.00,870.45,16538.55,12435.00,4103.55,12/1/2014,12,December,2014 -Government,France,Montana,1384.5,350.00,484575.00,24228.75,460346.25,359970.00,100376.25,1/1/2014,1,January,2014 -Enterprise,United States of America,Montana,3627,125.00,453375.00,22668.75,430706.25,435240.00,-4533.75,7/1/2014,7,July,2014 -Government,Mexico,Montana,720,350.00,252000.00,12600.00,239400.00,187200.00,52200.00,9/1/2013,9,September,2013 -Channel Partners,Germany,Montana,2342,12.00,28104.00,1405.20,26698.80,7026.00,19672.80,11/1/2014,11,November,2014 -Small Business,Mexico,Montana,1100,300.00,330000.00,16500.00,313500.00,275000.00,38500.00,12/1/2013,12,December,2013 -Government,France,Paseo,1303,20.00,26060.00,1303.00,24757.00,13030.00,11727.00,2/1/2014,2,February,2014 -Enterprise,United States of America,Paseo,2992,125.00,374000.00,18700.00,355300.00,359040.00,-3740.00,3/1/2014,3,March,2014 -Enterprise,France,Paseo,2385,125.00,298125.00,14906.25,283218.75,286200.00,-2981.25,3/1/2014,3,March,2014 -Small Business,Mexico,Paseo,1607,300.00,482100.00,24105.00,457995.00,401750.00,56245.00,4/1/2014,4,April,2014 -Government,United States of America,Paseo,2327,7.00,16289.00,814.45,15474.55,11635.00,3839.55,5/1/2014,5,May,2014 -Small Business,United States of America,Paseo,991,300.00,297300.00,14865.00,282435.00,247750.00,34685.00,6/1/2014,6,June,2014 -Government,United States of America,Paseo,602,350.00,210700.00,10535.00,200165.00,156520.00,43645.00,6/1/2014,6,June,2014 -Midmarket,France,Paseo,2620,15.00,39300.00,1965.00,37335.00,26200.00,11135.00,9/1/2014,9,September,2014 -Government,Canada,Paseo,1228,350.00,429800.00,21490.00,408310.00,319280.00,89030.00,10/1/2013,10,October,2013 -Government,Canada,Paseo,1389,20.00,27780.00,1389.00,26391.00,13890.00,12501.00,10/1/2013,10,October,2013 -Enterprise,United States of America,Paseo,861,125.00,107625.00,5381.25,102243.75,103320.00,-1076.25,10/1/2014,10,October,2014 -Enterprise,France,Paseo,704,125.00,88000.00,4400.00,83600.00,84480.00,-880.00,10/1/2013,10,October,2013 -Government,Canada,Paseo,1802,20.00,36040.00,1802.00,34238.00,18020.00,16218.00,12/1/2013,12,December,2013 -Government,United States of America,Paseo,2663,20.00,53260.00,2663.00,50597.00,26630.00,23967.00,12/1/2014,12,December,2014 -Government,France,Paseo,2136,7.00,14952.00,747.60,14204.40,10680.00,3524.40,12/1/2013,12,December,2013 -Midmarket,Germany,Paseo,2116,15.00,31740.00,1587.00,30153.00,21160.00,8993.00,12/1/2013,12,December,2013 -Midmarket,United States of America,Velo,555,15.00,8325.00,416.25,7908.75,5550.00,2358.75,1/1/2014,1,January,2014 -Midmarket,Mexico,Velo,2861,15.00,42915.00,2145.75,40769.25,28610.00,12159.25,1/1/2014,1,January,2014 -Enterprise,Germany,Velo,807,125.00,100875.00,5043.75,95831.25,96840.00,-1008.75,2/1/2014,2,February,2014 -Government,United States of America,Velo,602,350.00,210700.00,10535.00,200165.00,156520.00,43645.00,6/1/2014,6,June,2014 -Government,United States of America,Velo,2832,20.00,56640.00,2832.00,53808.00,28320.00,25488.00,8/1/2014,8,August,2014 -Government,France,Velo,1579,20.00,31580.00,1579.00,30001.00,15790.00,14211.00,8/1/2014,8,August,2014 -Enterprise,United States of America,Velo,861,125.00,107625.00,5381.25,102243.75,103320.00,-1076.25,10/1/2014,10,October,2014 -Enterprise,France,Velo,704,125.00,88000.00,4400.00,83600.00,84480.00,-880.00,10/1/2013,10,October,2013 -Government,France,Velo,1033,20.00,20660.00,1033.00,19627.00,10330.00,9297.00,12/1/2013,12,December,2013 -Small Business,Germany,Velo,1250,300.00,375000.00,18750.00,356250.00,312500.00,43750.00,12/1/2014,12,December,2014 -Government,Canada,VTT,1389,20.00,27780.00,1389.00,26391.00,13890.00,12501.00,10/1/2013,10,October,2013 -Government,United States of America,VTT,1265,20.00,25300.00,1265.00,24035.00,12650.00,11385.00,11/1/2013,11,November,2013 -Government,Germany,VTT,2297,20.00,45940.00,2297.00,43643.00,22970.00,20673.00,11/1/2013,11,November,2013 -Government,United States of America,VTT,2663,20.00,53260.00,2663.00,50597.00,26630.00,23967.00,12/1/2014,12,December,2014 -Government,United States of America,VTT,570,7.00,3990.00,199.50,3790.50,2850.00,940.50,12/1/2014,12,December,2014 -Government,France,VTT,2487,7.00,17409.00,870.45,16538.55,12435.00,4103.55,12/1/2014,12,December,2014 -Government,Germany,Amarilla,1350,350.00,472500.00,23625.00,448875.00,351000.00,97875.00,2/1/2014,2,February,2014 -Government,Canada,Amarilla,552,350.00,193200.00,9660.00,183540.00,143520.00,40020.00,8/1/2014,8,August,2014 -Government,Canada,Amarilla,1228,350.00,429800.00,21490.00,408310.00,319280.00,89030.00,10/1/2013,10,October,2013 -Small Business,Germany,Amarilla,1250,300.00,375000.00,18750.00,356250.00,312500.00,43750.00,12/1/2014,12,December,2014 -Midmarket,France,Paseo,3801,15.00,57015.00,3420.90,53594.10,38010.00,15584.10,4/1/2014,4,April,2014 -Government,United States of America,Carretera,1117.5,20.00,22350.00,1341.00,21009.00,11175.00,9834.00,1/1/2014,1,January,2014 -Midmarket,Canada,Carretera,2844,15.00,42660.00,2559.60,40100.40,28440.00,11660.40,6/1/2014,6,June,2014 -Channel Partners,Mexico,Carretera,562,12.00,6744.00,404.64,6339.36,1686.00,4653.36,9/1/2014,9,September,2014 -Channel Partners,Canada,Carretera,2299,12.00,27588.00,1655.28,25932.72,6897.00,19035.72,10/1/2013,10,October,2013 -Midmarket,United States of America,Carretera,2030,15.00,30450.00,1827.00,28623.00,20300.00,8323.00,11/1/2014,11,November,2014 -Government,United States of America,Carretera,263,7.00,1841.00,110.46,1730.54,1315.00,415.54,11/1/2013,11,November,2013 -Enterprise,Germany,Carretera,887,125.00,110875.00,6652.50,104222.50,106440.00,-2217.50,12/1/2013,12,December,2013 -Government,Mexico,Montana,980,350.00,343000.00,20580.00,322420.00,254800.00,67620.00,4/1/2014,4,April,2014 -Government,Germany,Montana,1460,350.00,511000.00,30660.00,480340.00,379600.00,100740.00,5/1/2014,5,May,2014 -Government,France,Montana,1403,7.00,9821.00,589.26,9231.74,7015.00,2216.74,10/1/2013,10,October,2013 -Channel Partners,United States of America,Montana,2723,12.00,32676.00,1960.56,30715.44,8169.00,22546.44,11/1/2014,11,November,2014 -Government,France,Paseo,1496,350.00,523600.00,31416.00,492184.00,388960.00,103224.00,6/1/2014,6,June,2014 -Channel Partners,Canada,Paseo,2299,12.00,27588.00,1655.28,25932.72,6897.00,19035.72,10/1/2013,10,October,2013 -Government,United States of America,Paseo,727,350.00,254450.00,15267.00,239183.00,189020.00,50163.00,10/1/2013,10,October,2013 -Enterprise,Canada,Velo,952,125.00,119000.00,7140.00,111860.00,114240.00,-2380.00,2/1/2014,2,February,2014 -Enterprise,United States of America,Velo,2755,125.00,344375.00,20662.50,323712.50,330600.00,-6887.50,2/1/2014,2,February,2014 -Midmarket,Germany,Velo,1530,15.00,22950.00,1377.00,21573.00,15300.00,6273.00,5/1/2014,5,May,2014 -Government,France,Velo,1496,350.00,523600.00,31416.00,492184.00,388960.00,103224.00,6/1/2014,6,June,2014 -Government,Mexico,Velo,1498,7.00,10486.00,629.16,9856.84,7490.00,2366.84,6/1/2014,6,June,2014 -Small Business,France,Velo,1221,300.00,366300.00,21978.00,344322.00,305250.00,39072.00,10/1/2013,10,October,2013 -Government,France,Velo,2076,350.00,726600.00,43596.00,683004.00,539760.00,143244.00,10/1/2013,10,October,2013 -Midmarket,Canada,VTT,2844,15.00,42660.00,2559.60,40100.40,28440.00,11660.40,6/1/2014,6,June,2014 -Government,Mexico,VTT,1498,7.00,10486.00,629.16,9856.84,7490.00,2366.84,6/1/2014,6,June,2014 -Small Business,France,VTT,1221,300.00,366300.00,21978.00,344322.00,305250.00,39072.00,10/1/2013,10,October,2013 -Government,Mexico,VTT,1123,20.00,22460.00,1347.60,21112.40,11230.00,9882.40,11/1/2013,11,November,2013 -Small Business,Canada,VTT,2436,300.00,730800.00,43848.00,686952.00,609000.00,77952.00,12/1/2013,12,December,2013 -Enterprise,France,Amarilla,1987.5,125.00,248437.50,14906.25,233531.25,238500.00,-4968.75,1/1/2014,1,January,2014 -Government,Mexico,Amarilla,1679,350.00,587650.00,35259.00,552391.00,436540.00,115851.00,9/1/2014,9,September,2014 -Government,United States of America,Amarilla,727,350.00,254450.00,15267.00,239183.00,189020.00,50163.00,10/1/2013,10,October,2013 -Government,France,Amarilla,1403,7.00,9821.00,589.26,9231.74,7015.00,2216.74,10/1/2013,10,October,2013 -Government,France,Amarilla,2076,350.00,726600.00,43596.00,683004.00,539760.00,143244.00,10/1/2013,10,October,2013 -Government,France,Montana,1757,20.00,35140.00,2108.40,33031.60,17570.00,15461.60,10/1/2013,10,October,2013 -Midmarket,United States of America,Paseo,2198,15.00,32970.00,1978.20,30991.80,21980.00,9011.80,8/1/2014,8,August,2014 -Midmarket,Germany,Paseo,1743,15.00,26145.00,1568.70,24576.30,17430.00,7146.30,8/1/2014,8,August,2014 -Midmarket,United States of America,Paseo,1153,15.00,17295.00,1037.70,16257.30,11530.00,4727.30,10/1/2014,10,October,2014 -Government,France,Paseo,1757,20.00,35140.00,2108.40,33031.60,17570.00,15461.60,10/1/2013,10,October,2013 -Government,Germany,Velo,1001,20.00,20020.00,1201.20,18818.80,10010.00,8808.80,8/1/2014,8,August,2014 -Government,Mexico,Velo,1333,7.00,9331.00,559.86,8771.14,6665.00,2106.14,11/1/2014,11,November,2014 -Midmarket,United States of America,VTT,1153,15.00,17295.00,1037.70,16257.30,11530.00,4727.30,10/1/2014,10,October,2014 -Channel Partners,Mexico,Carretera,727,12.00,8724.00,610.68,8113.32,2181.00,5932.32,2/1/2014,2,February,2014 -Channel Partners,Canada,Carretera,1884,12.00,22608.00,1582.56,21025.44,5652.00,15373.44,8/1/2014,8,August,2014 -Government,Mexico,Carretera,1834,20.00,36680.00,2567.60,34112.40,18340.00,15772.40,9/1/2013,9,September,2013 -Channel Partners,Mexico,Montana,2340,12.00,28080.00,1965.60,26114.40,7020.00,19094.40,1/1/2014,1,January,2014 -Channel Partners,France,Montana,2342,12.00,28104.00,1967.28,26136.72,7026.00,19110.72,11/1/2014,11,November,2014 -Government,France,Paseo,1031,7.00,7217.00,505.19,6711.81,5155.00,1556.81,9/1/2013,9,September,2013 -Midmarket,Canada,Velo,1262,15.00,18930.00,1325.10,17604.90,12620.00,4984.90,5/1/2014,5,May,2014 -Government,Canada,Velo,1135,7.00,7945.00,556.15,7388.85,5675.00,1713.85,6/1/2014,6,June,2014 -Government,United States of America,Velo,547,7.00,3829.00,268.03,3560.97,2735.00,825.97,11/1/2014,11,November,2014 -Government,Canada,Velo,1582,7.00,11074.00,775.18,10298.82,7910.00,2388.82,12/1/2014,12,December,2014 -Channel Partners,France,VTT,1738.5,12.00,20862.00,1460.34,19401.66,5215.50,14186.16,4/1/2014,4,April,2014 -Channel Partners,Germany,VTT,2215,12.00,26580.00,1860.60,24719.40,6645.00,18074.40,9/1/2013,9,September,2013 -Government,Canada,VTT,1582,7.00,11074.00,775.18,10298.82,7910.00,2388.82,12/1/2014,12,December,2014 -Government,Canada,Amarilla,1135,7.00,7945.00,556.15,7388.85,5675.00,1713.85,6/1/2014,6,June,2014 -Government,United States of America,Carretera,1761,350.00,616350.00,43144.50,573205.50,457860.00,115345.50,3/1/2014,3,March,2014 -Small Business,France,Carretera,448,300.00,134400.00,9408.00,124992.00,112000.00,12992.00,6/1/2014,6,June,2014 -Small Business,France,Carretera,2181,300.00,654300.00,45801.00,608499.00,545250.00,63249.00,10/1/2014,10,October,2014 -Government,France,Montana,1976,20.00,39520.00,2766.40,36753.60,19760.00,16993.60,10/1/2014,10,October,2014 -Small Business,France,Montana,2181,300.00,654300.00,45801.00,608499.00,545250.00,63249.00,10/1/2014,10,October,2014 -Enterprise,Germany,Montana,2500,125.00,312500.00,21875.00,290625.00,300000.00,-9375.00,11/1/2013,11,November,2013 -Small Business,Canada,Paseo,1702,300.00,510600.00,35742.00,474858.00,425500.00,49358.00,5/1/2014,5,May,2014 -Small Business,France,Paseo,448,300.00,134400.00,9408.00,124992.00,112000.00,12992.00,6/1/2014,6,June,2014 -Enterprise,Germany,Paseo,3513,125.00,439125.00,30738.75,408386.25,421560.00,-13173.75,7/1/2014,7,July,2014 -Midmarket,France,Paseo,2101,15.00,31515.00,2206.05,29308.95,21010.00,8298.95,8/1/2014,8,August,2014 -Midmarket,United States of America,Paseo,2931,15.00,43965.00,3077.55,40887.45,29310.00,11577.45,9/1/2013,9,September,2013 -Government,France,Paseo,1535,20.00,30700.00,2149.00,28551.00,15350.00,13201.00,9/1/2014,9,September,2014 -Small Business,Germany,Paseo,1123,300.00,336900.00,23583.00,313317.00,280750.00,32567.00,9/1/2013,9,September,2013 -Small Business,Canada,Paseo,1404,300.00,421200.00,29484.00,391716.00,351000.00,40716.00,11/1/2013,11,November,2013 -Channel Partners,Mexico,Paseo,2763,12.00,33156.00,2320.92,30835.08,8289.00,22546.08,11/1/2013,11,November,2013 -Government,Germany,Paseo,2125,7.00,14875.00,1041.25,13833.75,10625.00,3208.75,12/1/2013,12,December,2013 -Small Business,France,Velo,1659,300.00,497700.00,34839.00,462861.00,414750.00,48111.00,7/1/2014,7,July,2014 -Government,Mexico,Velo,609,20.00,12180.00,852.60,11327.40,6090.00,5237.40,8/1/2014,8,August,2014 -Enterprise,Germany,Velo,2087,125.00,260875.00,18261.25,242613.75,250440.00,-7826.25,9/1/2014,9,September,2014 -Government,France,Velo,1976,20.00,39520.00,2766.40,36753.60,19760.00,16993.60,10/1/2014,10,October,2014 -Government,United States of America,Velo,1421,20.00,28420.00,1989.40,26430.60,14210.00,12220.60,12/1/2013,12,December,2013 -Small Business,United States of America,Velo,1372,300.00,411600.00,28812.00,382788.00,343000.00,39788.00,12/1/2014,12,December,2014 -Government,Germany,Velo,588,20.00,11760.00,823.20,10936.80,5880.00,5056.80,12/1/2013,12,December,2013 -Channel Partners,Canada,VTT,3244.5,12.00,38934.00,2725.38,36208.62,9733.50,26475.12,1/1/2014,1,January,2014 -Small Business,France,VTT,959,300.00,287700.00,20139.00,267561.00,239750.00,27811.00,2/1/2014,2,February,2014 -Small Business,Mexico,VTT,2747,300.00,824100.00,57687.00,766413.00,686750.00,79663.00,2/1/2014,2,February,2014 -Enterprise,Canada,Amarilla,1645,125.00,205625.00,14393.75,191231.25,197400.00,-6168.75,5/1/2014,5,May,2014 -Government,France,Amarilla,2876,350.00,1006600.00,70462.00,936138.00,747760.00,188378.00,9/1/2014,9,September,2014 -Enterprise,Germany,Amarilla,994,125.00,124250.00,8697.50,115552.50,119280.00,-3727.50,9/1/2013,9,September,2013 -Government,Canada,Amarilla,1118,20.00,22360.00,1565.20,20794.80,11180.00,9614.80,11/1/2014,11,November,2014 -Small Business,United States of America,Amarilla,1372,300.00,411600.00,28812.00,382788.00,343000.00,39788.00,12/1/2014,12,December,2014 -Government,Canada,Montana,488,7.00,3416.00,273.28,3142.72,2440.00,702.72,2/1/2014,2,February,2014 -Government,United States of America,Montana,1282,20.00,25640.00,2051.20,23588.80,12820.00,10768.80,6/1/2014,6,June,2014 -Government,Canada,Paseo,257,7.00,1799.00,143.92,1655.08,1285.00,370.08,5/1/2014,5,May,2014 -Government,United States of America,Amarilla,1282,20.00,25640.00,2051.20,23588.80,12820.00,10768.80,6/1/2014,6,June,2014 -Enterprise,Mexico,Carretera,1540,125.00,192500.00,15400.00,177100.00,184800.00,-7700.00,8/1/2014,8,August,2014 -Midmarket,France,Carretera,490,15.00,7350.00,588.00,6762.00,4900.00,1862.00,11/1/2014,11,November,2014 -Government,Mexico,Carretera,1362,350.00,476700.00,38136.00,438564.00,354120.00,84444.00,12/1/2014,12,December,2014 -Midmarket,France,Montana,2501,15.00,37515.00,3001.20,34513.80,25010.00,9503.80,3/1/2014,3,March,2014 -Government,Canada,Montana,708,20.00,14160.00,1132.80,13027.20,7080.00,5947.20,6/1/2014,6,June,2014 -Government,Germany,Montana,645,20.00,12900.00,1032.00,11868.00,6450.00,5418.00,7/1/2014,7,July,2014 -Small Business,France,Montana,1562,300.00,468600.00,37488.00,431112.00,390500.00,40612.00,8/1/2014,8,August,2014 -Small Business,Canada,Montana,1283,300.00,384900.00,30792.00,354108.00,320750.00,33358.00,9/1/2013,9,September,2013 -Midmarket,Germany,Montana,711,15.00,10665.00,853.20,9811.80,7110.00,2701.80,12/1/2014,12,December,2014 -Enterprise,Mexico,Paseo,1114,125.00,139250.00,11140.00,128110.00,133680.00,-5570.00,3/1/2014,3,March,2014 -Government,Germany,Paseo,1259,7.00,8813.00,705.04,8107.96,6295.00,1812.96,4/1/2014,4,April,2014 -Government,Germany,Paseo,1095,7.00,7665.00,613.20,7051.80,5475.00,1576.80,5/1/2014,5,May,2014 -Government,Germany,Paseo,1366,20.00,27320.00,2185.60,25134.40,13660.00,11474.40,6/1/2014,6,June,2014 -Small Business,Mexico,Paseo,2460,300.00,738000.00,59040.00,678960.00,615000.00,63960.00,6/1/2014,6,June,2014 -Government,United States of America,Paseo,678,7.00,4746.00,379.68,4366.32,3390.00,976.32,8/1/2014,8,August,2014 -Government,Germany,Paseo,1598,7.00,11186.00,894.88,10291.12,7990.00,2301.12,8/1/2014,8,August,2014 -Government,Germany,Paseo,2409,7.00,16863.00,1349.04,15513.96,12045.00,3468.96,9/1/2013,9,September,2013 -Government,Germany,Paseo,1934,20.00,38680.00,3094.40,35585.60,19340.00,16245.60,9/1/2014,9,September,2014 -Government,Mexico,Paseo,2993,20.00,59860.00,4788.80,55071.20,29930.00,25141.20,9/1/2014,9,September,2014 -Government,Germany,Paseo,2146,350.00,751100.00,60088.00,691012.00,557960.00,133052.00,11/1/2013,11,November,2013 -Government,Mexico,Paseo,1946,7.00,13622.00,1089.76,12532.24,9730.00,2802.24,12/1/2013,12,December,2013 -Government,Mexico,Paseo,1362,350.00,476700.00,38136.00,438564.00,354120.00,84444.00,12/1/2014,12,December,2014 -Channel Partners,Canada,Velo,598,12.00,7176.00,574.08,6601.92,1794.00,4807.92,3/1/2014,3,March,2014 -Government,United States of America,Velo,2907,7.00,20349.00,1627.92,18721.08,14535.00,4186.08,6/1/2014,6,June,2014 -Government,Germany,Velo,2338,7.00,16366.00,1309.28,15056.72,11690.00,3366.72,6/1/2014,6,June,2014 -Small Business,France,Velo,386,300.00,115800.00,9264.00,106536.00,96500.00,10036.00,11/1/2013,11,November,2013 -Small Business,Mexico,Velo,635,300.00,190500.00,15240.00,175260.00,158750.00,16510.00,12/1/2014,12,December,2014 -Government,France,VTT,574.5,350.00,201075.00,16086.00,184989.00,149370.00,35619.00,4/1/2014,4,April,2014 -Government,Germany,VTT,2338,7.00,16366.00,1309.28,15056.72,11690.00,3366.72,6/1/2014,6,June,2014 -Government,France,VTT,381,350.00,133350.00,10668.00,122682.00,99060.00,23622.00,8/1/2014,8,August,2014 -Government,Germany,VTT,422,350.00,147700.00,11816.00,135884.00,109720.00,26164.00,8/1/2014,8,August,2014 -Small Business,Canada,VTT,2134,300.00,640200.00,51216.00,588984.00,533500.00,55484.00,9/1/2014,9,September,2014 -Small Business,United States of America,VTT,808,300.00,242400.00,19392.00,223008.00,202000.00,21008.00,12/1/2013,12,December,2013 -Government,Canada,Amarilla,708,20.00,14160.00,1132.80,13027.20,7080.00,5947.20,6/1/2014,6,June,2014 -Government,United States of America,Amarilla,2907,7.00,20349.00,1627.92,18721.08,14535.00,4186.08,6/1/2014,6,June,2014 -Government,Germany,Amarilla,1366,20.00,27320.00,2185.60,25134.40,13660.00,11474.40,6/1/2014,6,June,2014 -Small Business,Mexico,Amarilla,2460,300.00,738000.00,59040.00,678960.00,615000.00,63960.00,6/1/2014,6,June,2014 -Government,Germany,Amarilla,1520,20.00,30400.00,2432.00,27968.00,15200.00,12768.00,11/1/2014,11,November,2014 -Midmarket,Germany,Amarilla,711,15.00,10665.00,853.20,9811.80,7110.00,2701.80,12/1/2014,12,December,2014 -Channel Partners,Mexico,Amarilla,1375,12.00,16500.00,1320.00,15180.00,4125.00,11055.00,12/1/2013,12,December,2013 -Small Business,Mexico,Amarilla,635,300.00,190500.00,15240.00,175260.00,158750.00,16510.00,12/1/2014,12,December,2014 -Government,United States of America,VTT,436.5,20.00,8730.00,698.40,8031.60,4365.00,3666.60,7/1/2014,7,July,2014 -Small Business,Canada,Carretera,1094,300.00,328200.00,29538.00,298662.00,273500.00,25162.00,6/1/2014,6,June,2014 -Channel Partners,Mexico,Carretera,367,12.00,4404.00,396.36,4007.64,1101.00,2906.64,10/1/2013,10,October,2013 -Small Business,Canada,Montana,3802.5,300.00,1140750.00,102667.50,1038082.50,950625.00,87457.50,4/1/2014,4,April,2014 -Government,France,Montana,1666,350.00,583100.00,52479.00,530621.00,433160.00,97461.00,5/1/2014,5,May,2014 -Small Business,France,Montana,322,300.00,96600.00,8694.00,87906.00,80500.00,7406.00,9/1/2013,9,September,2013 -Channel Partners,Canada,Montana,2321,12.00,27852.00,2506.68,25345.32,6963.00,18382.32,11/1/2014,11,November,2014 -Enterprise,France,Montana,1857,125.00,232125.00,20891.25,211233.75,222840.00,-11606.25,11/1/2013,11,November,2013 -Government,Canada,Montana,1611,7.00,11277.00,1014.93,10262.07,8055.00,2207.07,12/1/2013,12,December,2013 -Enterprise,United States of America,Montana,2797,125.00,349625.00,31466.25,318158.75,335640.00,-17481.25,12/1/2014,12,December,2014 -Small Business,Germany,Montana,334,300.00,100200.00,9018.00,91182.00,83500.00,7682.00,12/1/2013,12,December,2013 -Small Business,Mexico,Paseo,2565,300.00,769500.00,69255.00,700245.00,641250.00,58995.00,1/1/2014,1,January,2014 -Government,Mexico,Paseo,2417,350.00,845950.00,76135.50,769814.50,628420.00,141394.50,1/1/2014,1,January,2014 -Midmarket,United States of America,Paseo,3675,15.00,55125.00,4961.25,50163.75,36750.00,13413.75,4/1/2014,4,April,2014 -Small Business,Canada,Paseo,1094,300.00,328200.00,29538.00,298662.00,273500.00,25162.00,6/1/2014,6,June,2014 -Midmarket,France,Paseo,1227,15.00,18405.00,1656.45,16748.55,12270.00,4478.55,10/1/2014,10,October,2014 -Channel Partners,Mexico,Paseo,367,12.00,4404.00,396.36,4007.64,1101.00,2906.64,10/1/2013,10,October,2013 -Small Business,France,Paseo,1324,300.00,397200.00,35748.00,361452.00,331000.00,30452.00,11/1/2014,11,November,2014 -Channel Partners,Germany,Paseo,1775,12.00,21300.00,1917.00,19383.00,5325.00,14058.00,11/1/2013,11,November,2013 -Enterprise,United States of America,Paseo,2797,125.00,349625.00,31466.25,318158.75,335640.00,-17481.25,12/1/2014,12,December,2014 -Midmarket,Mexico,Velo,245,15.00,3675.00,330.75,3344.25,2450.00,894.25,5/1/2014,5,May,2014 -Small Business,Canada,Velo,3793.5,300.00,1138050.00,102424.50,1035625.50,948375.00,87250.50,7/1/2014,7,July,2014 -Government,Germany,Velo,1307,350.00,457450.00,41170.50,416279.50,339820.00,76459.50,7/1/2014,7,July,2014 -Enterprise,Canada,Velo,567,125.00,70875.00,6378.75,64496.25,68040.00,-3543.75,9/1/2014,9,September,2014 -Enterprise,Mexico,Velo,2110,125.00,263750.00,23737.50,240012.50,253200.00,-13187.50,9/1/2014,9,September,2014 -Government,Canada,Velo,1269,350.00,444150.00,39973.50,404176.50,329940.00,74236.50,10/1/2014,10,October,2014 -Channel Partners,United States of America,VTT,1956,12.00,23472.00,2112.48,21359.52,5868.00,15491.52,1/1/2014,1,January,2014 -Small Business,Germany,VTT,2659,300.00,797700.00,71793.00,725907.00,664750.00,61157.00,2/1/2014,2,February,2014 -Government,United States of America,VTT,1351.5,350.00,473025.00,42572.25,430452.75,351390.00,79062.75,4/1/2014,4,April,2014 -Channel Partners,Germany,VTT,880,12.00,10560.00,950.40,9609.60,2640.00,6969.60,5/1/2014,5,May,2014 -Small Business,United States of America,VTT,1867,300.00,560100.00,50409.00,509691.00,466750.00,42941.00,9/1/2014,9,September,2014 -Channel Partners,France,VTT,2234,12.00,26808.00,2412.72,24395.28,6702.00,17693.28,9/1/2013,9,September,2013 -Midmarket,France,VTT,1227,15.00,18405.00,1656.45,16748.55,12270.00,4478.55,10/1/2014,10,October,2014 -Enterprise,Mexico,VTT,877,125.00,109625.00,9866.25,99758.75,105240.00,-5481.25,11/1/2014,11,November,2014 -Government,United States of America,Amarilla,2071,350.00,724850.00,65236.50,659613.50,538460.00,121153.50,9/1/2014,9,September,2014 -Government,Canada,Amarilla,1269,350.00,444150.00,39973.50,404176.50,329940.00,74236.50,10/1/2014,10,October,2014 -Midmarket,Germany,Amarilla,970,15.00,14550.00,1309.50,13240.50,9700.00,3540.50,11/1/2013,11,November,2013 -Government,Mexico,Amarilla,1694,20.00,33880.00,3049.20,30830.80,16940.00,13890.80,11/1/2014,11,November,2014 -Government,Germany,Carretera,663,20.00,13260.00,1193.40,12066.60,6630.00,5436.60,5/1/2014,5,May,2014 -Government,Canada,Carretera,819,7.00,5733.00,515.97,5217.03,4095.00,1122.03,7/1/2014,7,July,2014 -Channel Partners,Germany,Carretera,1580,12.00,18960.00,1706.40,17253.60,4740.00,12513.60,9/1/2014,9,September,2014 -Government,Mexico,Carretera,521,7.00,3647.00,328.23,3318.77,2605.00,713.77,12/1/2014,12,December,2014 -Government,United States of America,Paseo,973,20.00,19460.00,1751.40,17708.60,9730.00,7978.60,3/1/2014,3,March,2014 -Government,Mexico,Paseo,1038,20.00,20760.00,1868.40,18891.60,10380.00,8511.60,6/1/2014,6,June,2014 -Government,Germany,Paseo,360,7.00,2520.00,226.80,2293.20,1800.00,493.20,10/1/2014,10,October,2014 -Channel Partners,France,Velo,1967,12.00,23604.00,2124.36,21479.64,5901.00,15578.64,3/1/2014,3,March,2014 -Midmarket,Mexico,Velo,2628,15.00,39420.00,3547.80,35872.20,26280.00,9592.20,4/1/2014,4,April,2014 -Government,Germany,VTT,360,7.00,2520.00,226.80,2293.20,1800.00,493.20,10/1/2014,10,October,2014 -Government,France,VTT,2682,20.00,53640.00,4827.60,48812.40,26820.00,21992.40,11/1/2013,11,November,2013 -Government,Mexico,VTT,521,7.00,3647.00,328.23,3318.77,2605.00,713.77,12/1/2014,12,December,2014 -Government,Mexico,Amarilla,1038,20.00,20760.00,1868.40,18891.60,10380.00,8511.60,6/1/2014,6,June,2014 -Midmarket,Canada,Amarilla,1630.5,15.00,24457.50,2201.18,22256.33,16305.00,5951.33,7/1/2014,7,July,2014 -Channel Partners,France,Amarilla,306,12.00,3672.00,330.48,3341.52,918.00,2423.52,12/1/2013,12,December,2013 -Channel Partners,United States of America,Carretera,386,12.00,4632.00,463.20,4168.80,1158.00,3010.80,10/1/2013,10,October,2013 -Government,United States of America,Montana,2328,7.00,16296.00,1629.60,14666.40,11640.00,3026.40,9/1/2014,9,September,2014 -Channel Partners,United States of America,Paseo,386,12.00,4632.00,463.20,4168.80,1158.00,3010.80,10/1/2013,10,October,2013 -Enterprise,United States of America,Carretera,3445.5,125.00,430687.50,43068.75,387618.75,413460.00,-25841.25,4/1/2014,4,April,2014 -Enterprise,France,Carretera,1482,125.00,185250.00,18525.00,166725.00,177840.00,-11115.00,12/1/2013,12,December,2013 -Government,United States of America,Montana,2313,350.00,809550.00,80955.00,728595.00,601380.00,127215.00,5/1/2014,5,May,2014 -Enterprise,United States of America,Montana,1804,125.00,225500.00,22550.00,202950.00,216480.00,-13530.00,11/1/2013,11,November,2013 -Midmarket,France,Montana,2072,15.00,31080.00,3108.00,27972.00,20720.00,7252.00,12/1/2014,12,December,2014 -Government,France,Paseo,1954,20.00,39080.00,3908.00,35172.00,19540.00,15632.00,3/1/2014,3,March,2014 -Small Business,Mexico,Paseo,591,300.00,177300.00,17730.00,159570.00,147750.00,11820.00,5/1/2014,5,May,2014 -Midmarket,France,Paseo,2167,15.00,32505.00,3250.50,29254.50,21670.00,7584.50,10/1/2013,10,October,2013 -Government,Germany,Paseo,241,20.00,4820.00,482.00,4338.00,2410.00,1928.00,10/1/2014,10,October,2014 -Midmarket,Germany,Velo,681,15.00,10215.00,1021.50,9193.50,6810.00,2383.50,1/1/2014,1,January,2014 -Midmarket,Germany,Velo,510,15.00,7650.00,765.00,6885.00,5100.00,1785.00,4/1/2014,4,April,2014 -Midmarket,United States of America,Velo,790,15.00,11850.00,1185.00,10665.00,7900.00,2765.00,5/1/2014,5,May,2014 -Government,France,Velo,639,350.00,223650.00,22365.00,201285.00,166140.00,35145.00,7/1/2014,7,July,2014 -Enterprise,United States of America,Velo,1596,125.00,199500.00,19950.00,179550.00,191520.00,-11970.00,9/1/2014,9,September,2014 -Small Business,United States of America,Velo,2294,300.00,688200.00,68820.00,619380.00,573500.00,45880.00,10/1/2013,10,October,2013 -Government,Germany,Velo,241,20.00,4820.00,482.00,4338.00,2410.00,1928.00,10/1/2014,10,October,2014 -Government,Germany,Velo,2665,7.00,18655.00,1865.50,16789.50,13325.00,3464.50,11/1/2014,11,November,2014 -Enterprise,Canada,Velo,1916,125.00,239500.00,23950.00,215550.00,229920.00,-14370.00,12/1/2013,12,December,2013 -Small Business,France,Velo,853,300.00,255900.00,25590.00,230310.00,213250.00,17060.00,12/1/2014,12,December,2014 -Enterprise,Mexico,VTT,341,125.00,42625.00,4262.50,38362.50,40920.00,-2557.50,5/1/2014,5,May,2014 -Midmarket,Mexico,VTT,641,15.00,9615.00,961.50,8653.50,6410.00,2243.50,7/1/2014,7,July,2014 -Government,United States of America,VTT,2807,350.00,982450.00,98245.00,884205.00,729820.00,154385.00,8/1/2014,8,August,2014 -Small Business,Mexico,VTT,432,300.00,129600.00,12960.00,116640.00,108000.00,8640.00,9/1/2014,9,September,2014 -Small Business,United States of America,VTT,2294,300.00,688200.00,68820.00,619380.00,573500.00,45880.00,10/1/2013,10,October,2013 -Midmarket,France,VTT,2167,15.00,32505.00,3250.50,29254.50,21670.00,7584.50,10/1/2013,10,October,2013 -Enterprise,Canada,VTT,2529,125.00,316125.00,31612.50,284512.50,303480.00,-18967.50,11/1/2014,11,November,2014 -Government,Germany,VTT,1870,350.00,654500.00,65450.00,589050.00,486200.00,102850.00,12/1/2013,12,December,2013 -Enterprise,United States of America,Amarilla,579,125.00,72375.00,7237.50,65137.50,69480.00,-4342.50,1/1/2014,1,January,2014 -Government,Canada,Amarilla,2240,350.00,784000.00,78400.00,705600.00,582400.00,123200.00,2/1/2014,2,February,2014 -Small Business,United States of America,Amarilla,2993,300.00,897900.00,89790.00,808110.00,748250.00,59860.00,3/1/2014,3,March,2014 -Channel Partners,Canada,Amarilla,3520.5,12.00,42246.00,4224.60,38021.40,10561.50,27459.90,4/1/2014,4,April,2014 -Government,Mexico,Amarilla,2039,20.00,40780.00,4078.00,36702.00,20390.00,16312.00,5/1/2014,5,May,2014 -Channel Partners,Germany,Amarilla,2574,12.00,30888.00,3088.80,27799.20,7722.00,20077.20,8/1/2014,8,August,2014 -Government,Canada,Amarilla,707,350.00,247450.00,24745.00,222705.00,183820.00,38885.00,9/1/2014,9,September,2014 -Midmarket,France,Amarilla,2072,15.00,31080.00,3108.00,27972.00,20720.00,7252.00,12/1/2014,12,December,2014 -Small Business,France,Amarilla,853,300.00,255900.00,25590.00,230310.00,213250.00,17060.00,12/1/2014,12,December,2014 -Channel Partners,France,Carretera,1198,12.00,14376.00,1581.36,12794.64,3594.00,9200.64,10/1/2013,10,October,2013 -Government,France,Paseo,2532,7.00,17724.00,1949.64,15774.36,12660.00,3114.36,4/1/2014,4,April,2014 -Channel Partners,France,Paseo,1198,12.00,14376.00,1581.36,12794.64,3594.00,9200.64,10/1/2013,10,October,2013 -Midmarket,Canada,Velo,384,15.00,5760.00,633.60,5126.40,3840.00,1286.40,1/1/2014,1,January,2014 -Channel Partners,Germany,Velo,472,12.00,5664.00,623.04,5040.96,1416.00,3624.96,10/1/2014,10,October,2014 -Government,United States of America,VTT,1579,7.00,11053.00,1215.83,9837.17,7895.00,1942.17,3/1/2014,3,March,2014 -Channel Partners,Mexico,VTT,1005,12.00,12060.00,1326.60,10733.40,3015.00,7718.40,9/1/2013,9,September,2013 -Midmarket,United States of America,Amarilla,3199.5,15.00,47992.50,5279.18,42713.33,31995.00,10718.33,7/1/2014,7,July,2014 -Channel Partners,Germany,Amarilla,472,12.00,5664.00,623.04,5040.96,1416.00,3624.96,10/1/2014,10,October,2014 -Channel Partners,Canada,Carretera,1937,12.00,23244.00,2556.84,20687.16,5811.00,14876.16,2/1/2014,2,February,2014 -Government,Germany,Carretera,792,350.00,277200.00,30492.00,246708.00,205920.00,40788.00,3/1/2014,3,March,2014 -Small Business,Germany,Carretera,2811,300.00,843300.00,92763.00,750537.00,702750.00,47787.00,7/1/2014,7,July,2014 -Enterprise,France,Carretera,2441,125.00,305125.00,33563.75,271561.25,292920.00,-21358.75,10/1/2014,10,October,2014 -Midmarket,Canada,Carretera,1560,15.00,23400.00,2574.00,20826.00,15600.00,5226.00,11/1/2013,11,November,2013 -Government,Mexico,Carretera,2706,7.00,18942.00,2083.62,16858.38,13530.00,3328.38,11/1/2013,11,November,2013 -Government,Germany,Montana,766,350.00,268100.00,29491.00,238609.00,199160.00,39449.00,1/1/2014,1,January,2014 -Government,Germany,Montana,2992,20.00,59840.00,6582.40,53257.60,29920.00,23337.60,10/1/2013,10,October,2013 -Midmarket,Mexico,Montana,2157,15.00,32355.00,3559.05,28795.95,21570.00,7225.95,12/1/2014,12,December,2014 -Small Business,Canada,Paseo,873,300.00,261900.00,28809.00,233091.00,218250.00,14841.00,1/1/2014,1,January,2014 -Government,Mexico,Paseo,1122,20.00,22440.00,2468.40,19971.60,11220.00,8751.60,3/1/2014,3,March,2014 -Government,Canada,Paseo,2104.5,350.00,736575.00,81023.25,655551.75,547170.00,108381.75,7/1/2014,7,July,2014 -Channel Partners,Canada,Paseo,4026,12.00,48312.00,5314.32,42997.68,12078.00,30919.68,7/1/2014,7,July,2014 -Channel Partners,France,Paseo,2425.5,12.00,29106.00,3201.66,25904.34,7276.50,18627.84,7/1/2014,7,July,2014 -Government,Canada,Paseo,2394,20.00,47880.00,5266.80,42613.20,23940.00,18673.20,8/1/2014,8,August,2014 -Midmarket,Mexico,Paseo,1984,15.00,29760.00,3273.60,26486.40,19840.00,6646.40,8/1/2014,8,August,2014 -Enterprise,France,Paseo,2441,125.00,305125.00,33563.75,271561.25,292920.00,-21358.75,10/1/2014,10,October,2014 -Government,Germany,Paseo,2992,20.00,59840.00,6582.40,53257.60,29920.00,23337.60,10/1/2013,10,October,2013 -Small Business,Canada,Paseo,1366,300.00,409800.00,45078.00,364722.00,341500.00,23222.00,11/1/2014,11,November,2014 -Government,France,Velo,2805,20.00,56100.00,6171.00,49929.00,28050.00,21879.00,9/1/2013,9,September,2013 -Midmarket,Mexico,Velo,655,15.00,9825.00,1080.75,8744.25,6550.00,2194.25,9/1/2013,9,September,2013 -Government,Mexico,Velo,344,350.00,120400.00,13244.00,107156.00,89440.00,17716.00,10/1/2013,10,October,2013 -Government,Canada,Velo,1808,7.00,12656.00,1392.16,11263.84,9040.00,2223.84,11/1/2014,11,November,2014 -Channel Partners,France,VTT,1734,12.00,20808.00,2288.88,18519.12,5202.00,13317.12,1/1/2014,1,January,2014 -Enterprise,Mexico,VTT,554,125.00,69250.00,7617.50,61632.50,66480.00,-4847.50,1/1/2014,1,January,2014 -Government,Canada,VTT,2935,20.00,58700.00,6457.00,52243.00,29350.00,22893.00,11/1/2013,11,November,2013 -Enterprise,Germany,Amarilla,3165,125.00,395625.00,43518.75,352106.25,379800.00,-27693.75,1/1/2014,1,January,2014 -Government,Mexico,Amarilla,2629,20.00,52580.00,5783.80,46796.20,26290.00,20506.20,1/1/2014,1,January,2014 -Enterprise,France,Amarilla,1433,125.00,179125.00,19703.75,159421.25,171960.00,-12538.75,5/1/2014,5,May,2014 -Enterprise,Mexico,Amarilla,947,125.00,118375.00,13021.25,105353.75,113640.00,-8286.25,9/1/2013,9,September,2013 -Government,Mexico,Amarilla,344,350.00,120400.00,13244.00,107156.00,89440.00,17716.00,10/1/2013,10,October,2013 -Midmarket,Mexico,Amarilla,2157,15.00,32355.00,3559.05,28795.95,21570.00,7225.95,12/1/2014,12,December,2014 -Government,United States of America,Paseo,380,7.00,2660.00,292.60,2367.40,1900.00,467.40,9/1/2013,9,September,2013 -Government,Mexico,Carretera,886,350.00,310100.00,37212.00,272888.00,230360.00,42528.00,6/1/2014,6,June,2014 -Enterprise,Canada,Carretera,2416,125.00,302000.00,36240.00,265760.00,289920.00,-24160.00,9/1/2013,9,September,2013 -Enterprise,Mexico,Carretera,2156,125.00,269500.00,32340.00,237160.00,258720.00,-21560.00,10/1/2014,10,October,2014 -Midmarket,Canada,Carretera,2689,15.00,40335.00,4840.20,35494.80,26890.00,8604.80,11/1/2014,11,November,2014 -Midmarket,United States of America,Montana,677,15.00,10155.00,1218.60,8936.40,6770.00,2166.40,3/1/2014,3,March,2014 -Small Business,France,Montana,1773,300.00,531900.00,63828.00,468072.00,443250.00,24822.00,4/1/2014,4,April,2014 -Government,Mexico,Montana,2420,7.00,16940.00,2032.80,14907.20,12100.00,2807.20,9/1/2014,9,September,2014 -Government,Canada,Montana,2734,7.00,19138.00,2296.56,16841.44,13670.00,3171.44,10/1/2014,10,October,2014 -Government,Mexico,Montana,1715,20.00,34300.00,4116.00,30184.00,17150.00,13034.00,10/1/2013,10,October,2013 -Small Business,France,Montana,1186,300.00,355800.00,42696.00,313104.00,296500.00,16604.00,12/1/2013,12,December,2013 -Small Business,United States of America,Paseo,3495,300.00,1048500.00,125820.00,922680.00,873750.00,48930.00,1/1/2014,1,January,2014 -Government,Mexico,Paseo,886,350.00,310100.00,37212.00,272888.00,230360.00,42528.00,6/1/2014,6,June,2014 -Enterprise,Mexico,Paseo,2156,125.00,269500.00,32340.00,237160.00,258720.00,-21560.00,10/1/2014,10,October,2014 -Government,Mexico,Paseo,905,20.00,18100.00,2172.00,15928.00,9050.00,6878.00,10/1/2014,10,October,2014 -Government,Mexico,Paseo,1715,20.00,34300.00,4116.00,30184.00,17150.00,13034.00,10/1/2013,10,October,2013 -Government,France,Paseo,1594,350.00,557900.00,66948.00,490952.00,414440.00,76512.00,11/1/2014,11,November,2014 -Small Business,Germany,Paseo,1359,300.00,407700.00,48924.00,358776.00,339750.00,19026.00,11/1/2014,11,November,2014 -Small Business,Mexico,Paseo,2150,300.00,645000.00,77400.00,567600.00,537500.00,30100.00,11/1/2014,11,November,2014 -Government,Mexico,Paseo,1197,350.00,418950.00,50274.00,368676.00,311220.00,57456.00,11/1/2014,11,November,2014 -Midmarket,Mexico,Paseo,380,15.00,5700.00,684.00,5016.00,3800.00,1216.00,12/1/2013,12,December,2013 -Government,Mexico,Paseo,1233,20.00,24660.00,2959.20,21700.80,12330.00,9370.80,12/1/2014,12,December,2014 -Government,Mexico,Velo,1395,350.00,488250.00,58590.00,429660.00,362700.00,66960.00,7/1/2014,7,July,2014 -Government,United States of America,Velo,986,350.00,345100.00,41412.00,303688.00,256360.00,47328.00,10/1/2014,10,October,2014 -Government,Mexico,Velo,905,20.00,18100.00,2172.00,15928.00,9050.00,6878.00,10/1/2014,10,October,2014 -Channel Partners,Canada,VTT,2109,12.00,25308.00,3036.96,22271.04,6327.00,15944.04,5/1/2014,5,May,2014 -Midmarket,France,VTT,3874.5,15.00,58117.50,6974.10,51143.40,38745.00,12398.40,7/1/2014,7,July,2014 -Government,Canada,VTT,623,350.00,218050.00,26166.00,191884.00,161980.00,29904.00,9/1/2013,9,September,2013 -Government,United States of America,VTT,986,350.00,345100.00,41412.00,303688.00,256360.00,47328.00,10/1/2014,10,October,2014 -Enterprise,United States of America,VTT,2387,125.00,298375.00,35805.00,262570.00,286440.00,-23870.00,11/1/2014,11,November,2014 -Government,Mexico,VTT,1233,20.00,24660.00,2959.20,21700.80,12330.00,9370.80,12/1/2014,12,December,2014 -Government,United States of America,Amarilla,270,350.00,94500.00,11340.00,83160.00,70200.00,12960.00,2/1/2014,2,February,2014 -Government,France,Amarilla,3421.5,7.00,23950.50,2874.06,21076.44,17107.50,3968.94,7/1/2014,7,July,2014 -Government,Canada,Amarilla,2734,7.00,19138.00,2296.56,16841.44,13670.00,3171.44,10/1/2014,10,October,2014 -Midmarket,United States of America,Amarilla,2548,15.00,38220.00,4586.40,33633.60,25480.00,8153.60,11/1/2013,11,November,2013 -Government,France,Carretera,2521.5,20.00,50430.00,6051.60,44378.40,25215.00,19163.40,1/1/2014,1,January,2014 -Channel Partners,Mexico,Montana,2661,12.00,31932.00,3831.84,28100.16,7983.00,20117.16,5/1/2014,5,May,2014 -Government,Germany,Paseo,1531,20.00,30620.00,3674.40,26945.60,15310.00,11635.60,12/1/2014,12,December,2014 -Government,France,VTT,1491,7.00,10437.00,1252.44,9184.56,7455.00,1729.56,3/1/2014,3,March,2014 -Government,Germany,VTT,1531,20.00,30620.00,3674.40,26945.60,15310.00,11635.60,12/1/2014,12,December,2014 -Channel Partners,Canada,Amarilla,2761,12.00,33132.00,3975.84,29156.16,8283.00,20873.16,9/1/2013,9,September,2013 -Midmarket,United States of America,Carretera,2567,15.00,38505.00,5005.65,33499.35,25670.00,7829.35,6/1/2014,6,June,2014 -Midmarket,United States of America,VTT,2567,15.00,38505.00,5005.65,33499.35,25670.00,7829.35,6/1/2014,6,June,2014 -Government,Canada,Carretera,923,350.00,323050.00,41996.50,281053.50,239980.00,41073.50,3/1/2014,3,March,2014 -Government,France,Carretera,1790,350.00,626500.00,81445.00,545055.00,465400.00,79655.00,3/1/2014,3,March,2014 -Government,Germany,Carretera,442,20.00,8840.00,1149.20,7690.80,4420.00,3270.80,9/1/2013,9,September,2013 -Government,United States of America,Montana,982.5,350.00,343875.00,44703.75,299171.25,255450.00,43721.25,1/1/2014,1,January,2014 -Government,United States of America,Montana,1298,7.00,9086.00,1181.18,7904.82,6490.00,1414.82,2/1/2014,2,February,2014 -Channel Partners,Mexico,Montana,604,12.00,7248.00,942.24,6305.76,1812.00,4493.76,6/1/2014,6,June,2014 -Government,Mexico,Montana,2255,20.00,45100.00,5863.00,39237.00,22550.00,16687.00,7/1/2014,7,July,2014 -Government,Canada,Montana,1249,20.00,24980.00,3247.40,21732.60,12490.00,9242.60,10/1/2014,10,October,2014 -Government,United States of America,Paseo,1438.5,7.00,10069.50,1309.04,8760.47,7192.50,1567.97,1/1/2014,1,January,2014 -Small Business,Germany,Paseo,807,300.00,242100.00,31473.00,210627.00,201750.00,8877.00,1/1/2014,1,January,2014 -Government,United States of America,Paseo,2641,20.00,52820.00,6866.60,45953.40,26410.00,19543.40,2/1/2014,2,February,2014 -Government,Germany,Paseo,2708,20.00,54160.00,7040.80,47119.20,27080.00,20039.20,2/1/2014,2,February,2014 -Government,Canada,Paseo,2632,350.00,921200.00,119756.00,801444.00,684320.00,117124.00,6/1/2014,6,June,2014 -Enterprise,Canada,Paseo,1583,125.00,197875.00,25723.75,172151.25,189960.00,-17808.75,6/1/2014,6,June,2014 -Channel Partners,Mexico,Paseo,571,12.00,6852.00,890.76,5961.24,1713.00,4248.24,7/1/2014,7,July,2014 -Government,France,Paseo,2696,7.00,18872.00,2453.36,16418.64,13480.00,2938.64,8/1/2014,8,August,2014 -Midmarket,Canada,Paseo,1565,15.00,23475.00,3051.75,20423.25,15650.00,4773.25,10/1/2014,10,October,2014 -Government,Canada,Paseo,1249,20.00,24980.00,3247.40,21732.60,12490.00,9242.60,10/1/2014,10,October,2014 -Government,Germany,Paseo,357,350.00,124950.00,16243.50,108706.50,92820.00,15886.50,11/1/2014,11,November,2014 -Channel Partners,Germany,Paseo,1013,12.00,12156.00,1580.28,10575.72,3039.00,7536.72,12/1/2014,12,December,2014 -Midmarket,France,Velo,3997.5,15.00,59962.50,7795.13,52167.38,39975.00,12192.38,1/1/2014,1,January,2014 -Government,Canada,Velo,2632,350.00,921200.00,119756.00,801444.00,684320.00,117124.00,6/1/2014,6,June,2014 -Government,France,Velo,1190,7.00,8330.00,1082.90,7247.10,5950.00,1297.10,6/1/2014,6,June,2014 -Channel Partners,Mexico,Velo,604,12.00,7248.00,942.24,6305.76,1812.00,4493.76,6/1/2014,6,June,2014 -Midmarket,Germany,Velo,660,15.00,9900.00,1287.00,8613.00,6600.00,2013.00,9/1/2013,9,September,2013 -Channel Partners,Mexico,Velo,410,12.00,4920.00,639.60,4280.40,1230.00,3050.40,10/1/2014,10,October,2014 -Small Business,Mexico,Velo,2605,300.00,781500.00,101595.00,679905.00,651250.00,28655.00,11/1/2013,11,November,2013 -Channel Partners,Germany,Velo,1013,12.00,12156.00,1580.28,10575.72,3039.00,7536.72,12/1/2014,12,December,2014 -Enterprise,Canada,VTT,1583,125.00,197875.00,25723.75,172151.25,189960.00,-17808.75,6/1/2014,6,June,2014 -Midmarket,Canada,VTT,1565,15.00,23475.00,3051.75,20423.25,15650.00,4773.25,10/1/2014,10,October,2014 -Enterprise,Canada,Amarilla,1659,125.00,207375.00,26958.75,180416.25,199080.00,-18663.75,1/1/2014,1,January,2014 -Government,France,Amarilla,1190,7.00,8330.00,1082.90,7247.10,5950.00,1297.10,6/1/2014,6,June,2014 -Channel Partners,Mexico,Amarilla,410,12.00,4920.00,639.60,4280.40,1230.00,3050.40,10/1/2014,10,October,2014 -Channel Partners,Germany,Amarilla,1770,12.00,21240.00,2761.20,18478.80,5310.00,13168.80,12/1/2013,12,December,2013 -Government,Mexico,Carretera,2579,20.00,51580.00,7221.20,44358.80,25790.00,18568.80,4/1/2014,4,April,2014 -Government,United States of America,Carretera,1743,20.00,34860.00,4880.40,29979.60,17430.00,12549.60,5/1/2014,5,May,2014 -Government,United States of America,Carretera,2996,7.00,20972.00,2936.08,18035.92,14980.00,3055.92,10/1/2013,10,October,2013 -Government,Germany,Carretera,280,7.00,1960.00,274.40,1685.60,1400.00,285.60,12/1/2014,12,December,2014 -Government,France,Montana,293,7.00,2051.00,287.14,1763.86,1465.00,298.86,2/1/2014,2,February,2014 -Government,United States of America,Montana,2996,7.00,20972.00,2936.08,18035.92,14980.00,3055.92,10/1/2013,10,October,2013 -Midmarket,Germany,Paseo,278,15.00,4170.00,583.80,3586.20,2780.00,806.20,2/1/2014,2,February,2014 -Government,Canada,Paseo,2428,20.00,48560.00,6798.40,41761.60,24280.00,17481.60,3/1/2014,3,March,2014 -Midmarket,United States of America,Paseo,1767,15.00,26505.00,3710.70,22794.30,17670.00,5124.30,9/1/2014,9,September,2014 -Channel Partners,France,Paseo,1393,12.00,16716.00,2340.24,14375.76,4179.00,10196.76,10/1/2014,10,October,2014 -Government,Germany,VTT,280,7.00,1960.00,274.40,1685.60,1400.00,285.60,12/1/2014,12,December,2014 -Channel Partners,France,Amarilla,1393,12.00,16716.00,2340.24,14375.76,4179.00,10196.76,10/1/2014,10,October,2014 -Channel Partners,United States of America,Amarilla,2015,12.00,24180.00,3385.20,20794.80,6045.00,14749.80,12/1/2013,12,December,2013 -Small Business,Mexico,Carretera,801,300.00,240300.00,33642.00,206658.00,200250.00,6408.00,7/1/2014,7,July,2014 -Enterprise,France,Carretera,1023,125.00,127875.00,17902.50,109972.50,122760.00,-12787.50,9/1/2013,9,September,2013 -Small Business,Canada,Carretera,1496,300.00,448800.00,62832.00,385968.00,374000.00,11968.00,10/1/2014,10,October,2014 -Small Business,United States of America,Carretera,1010,300.00,303000.00,42420.00,260580.00,252500.00,8080.00,10/1/2014,10,October,2014 -Midmarket,Germany,Carretera,1513,15.00,22695.00,3177.30,19517.70,15130.00,4387.70,11/1/2014,11,November,2014 -Midmarket,Canada,Carretera,2300,15.00,34500.00,4830.00,29670.00,23000.00,6670.00,12/1/2014,12,December,2014 -Enterprise,Mexico,Carretera,2821,125.00,352625.00,49367.50,303257.50,338520.00,-35262.50,12/1/2013,12,December,2013 -Government,Canada,Montana,2227.5,350.00,779625.00,109147.50,670477.50,579150.00,91327.50,1/1/2014,1,January,2014 -Government,Germany,Montana,1199,350.00,419650.00,58751.00,360899.00,311740.00,49159.00,4/1/2014,4,April,2014 -Government,Canada,Montana,200,350.00,70000.00,9800.00,60200.00,52000.00,8200.00,5/1/2014,5,May,2014 -Government,Canada,Montana,388,7.00,2716.00,380.24,2335.76,1940.00,395.76,9/1/2014,9,September,2014 -Government,Mexico,Montana,1727,7.00,12089.00,1692.46,10396.54,8635.00,1761.54,10/1/2013,10,October,2013 -Midmarket,Canada,Montana,2300,15.00,34500.00,4830.00,29670.00,23000.00,6670.00,12/1/2014,12,December,2014 -Government,Mexico,Paseo,260,20.00,5200.00,728.00,4472.00,2600.00,1872.00,2/1/2014,2,February,2014 -Midmarket,Canada,Paseo,2470,15.00,37050.00,5187.00,31863.00,24700.00,7163.00,9/1/2013,9,September,2013 -Midmarket,Canada,Paseo,1743,15.00,26145.00,3660.30,22484.70,17430.00,5054.70,10/1/2013,10,October,2013 -Channel Partners,United States of America,Paseo,2914,12.00,34968.00,4895.52,30072.48,8742.00,21330.48,10/1/2014,10,October,2014 -Government,France,Paseo,1731,7.00,12117.00,1696.38,10420.62,8655.00,1765.62,10/1/2014,10,October,2014 -Government,Canada,Paseo,700,350.00,245000.00,34300.00,210700.00,182000.00,28700.00,11/1/2014,11,November,2014 -Channel Partners,Canada,Paseo,2222,12.00,26664.00,3732.96,22931.04,6666.00,16265.04,11/1/2013,11,November,2013 -Government,United States of America,Paseo,1177,350.00,411950.00,57673.00,354277.00,306020.00,48257.00,11/1/2014,11,November,2014 -Government,France,Paseo,1922,350.00,672700.00,94178.00,578522.00,499720.00,78802.00,11/1/2013,11,November,2013 -Enterprise,Mexico,Velo,1575,125.00,196875.00,27562.50,169312.50,189000.00,-19687.50,2/1/2014,2,February,2014 -Government,United States of America,Velo,606,20.00,12120.00,1696.80,10423.20,6060.00,4363.20,4/1/2014,4,April,2014 -Small Business,United States of America,Velo,2460,300.00,738000.00,103320.00,634680.00,615000.00,19680.00,7/1/2014,7,July,2014 -Small Business,Canada,Velo,269,300.00,80700.00,11298.00,69402.00,67250.00,2152.00,10/1/2013,10,October,2013 -Small Business,Germany,Velo,2536,300.00,760800.00,106512.00,654288.00,634000.00,20288.00,11/1/2013,11,November,2013 -Government,Mexico,VTT,2903,7.00,20321.00,2844.94,17476.06,14515.00,2961.06,3/1/2014,3,March,2014 -Small Business,United States of America,VTT,2541,300.00,762300.00,106722.00,655578.00,635250.00,20328.00,8/1/2014,8,August,2014 -Small Business,Canada,VTT,269,300.00,80700.00,11298.00,69402.00,67250.00,2152.00,10/1/2013,10,October,2013 -Small Business,Canada,VTT,1496,300.00,448800.00,62832.00,385968.00,374000.00,11968.00,10/1/2014,10,October,2014 -Small Business,United States of America,VTT,1010,300.00,303000.00,42420.00,260580.00,252500.00,8080.00,10/1/2014,10,October,2014 -Government,France,VTT,1281,350.00,448350.00,62769.00,385581.00,333060.00,52521.00,12/1/2013,12,December,2013 -Small Business,Canada,Amarilla,888,300.00,266400.00,37296.00,229104.00,222000.00,7104.00,3/1/2014,3,March,2014 -Enterprise,United States of America,Amarilla,2844,125.00,355500.00,49770.00,305730.00,341280.00,-35550.00,5/1/2014,5,May,2014 -Channel Partners,France,Amarilla,2475,12.00,29700.00,4158.00,25542.00,7425.00,18117.00,8/1/2014,8,August,2014 -Midmarket,Canada,Amarilla,1743,15.00,26145.00,3660.30,22484.70,17430.00,5054.70,10/1/2013,10,October,2013 -Channel Partners,United States of America,Amarilla,2914,12.00,34968.00,4895.52,30072.48,8742.00,21330.48,10/1/2014,10,October,2014 -Government,France,Amarilla,1731,7.00,12117.00,1696.38,10420.62,8655.00,1765.62,10/1/2014,10,October,2014 -Government,Mexico,Amarilla,1727,7.00,12089.00,1692.46,10396.54,8635.00,1761.54,10/1/2013,10,October,2013 -Midmarket,Mexico,Amarilla,1870,15.00,28050.00,3927.00,24123.00,18700.00,5423.00,11/1/2013,11,November,2013 -Enterprise,France,Carretera,1174,125.00,146750.00,22012.50,124737.50,140880.00,-16142.50,8/1/2014,8,August,2014 -Enterprise,Germany,Carretera,2767,125.00,345875.00,51881.25,293993.75,332040.00,-38046.25,8/1/2014,8,August,2014 -Enterprise,Germany,Carretera,1085,125.00,135625.00,20343.75,115281.25,130200.00,-14918.75,10/1/2014,10,October,2014 -Small Business,Mexico,Montana,546,300.00,163800.00,24570.00,139230.00,136500.00,2730.00,10/1/2014,10,October,2014 -Government,Germany,Paseo,1158,20.00,23160.00,3474.00,19686.00,11580.00,8106.00,3/1/2014,3,March,2014 -Midmarket,Canada,Paseo,1614,15.00,24210.00,3631.50,20578.50,16140.00,4438.50,4/1/2014,4,April,2014 -Government,Mexico,Paseo,2535,7.00,17745.00,2661.75,15083.25,12675.00,2408.25,4/1/2014,4,April,2014 -Government,Mexico,Paseo,2851,350.00,997850.00,149677.50,848172.50,741260.00,106912.50,5/1/2014,5,May,2014 -Midmarket,Canada,Paseo,2559,15.00,38385.00,5757.75,32627.25,25590.00,7037.25,8/1/2014,8,August,2014 -Government,United States of America,Paseo,267,20.00,5340.00,801.00,4539.00,2670.00,1869.00,10/1/2013,10,October,2013 -Enterprise,Germany,Paseo,1085,125.00,135625.00,20343.75,115281.25,130200.00,-14918.75,10/1/2014,10,October,2014 -Midmarket,Germany,Paseo,1175,15.00,17625.00,2643.75,14981.25,11750.00,3231.25,10/1/2014,10,October,2014 -Government,United States of America,Paseo,2007,350.00,702450.00,105367.50,597082.50,521820.00,75262.50,11/1/2013,11,November,2013 -Government,Mexico,Paseo,2151,350.00,752850.00,112927.50,639922.50,559260.00,80662.50,11/1/2013,11,November,2013 -Channel Partners,United States of America,Paseo,914,12.00,10968.00,1645.20,9322.80,2742.00,6580.80,12/1/2014,12,December,2014 -Government,France,Paseo,293,20.00,5860.00,879.00,4981.00,2930.00,2051.00,12/1/2014,12,December,2014 -Channel Partners,Mexico,Velo,500,12.00,6000.00,900.00,5100.00,1500.00,3600.00,3/1/2014,3,March,2014 -Midmarket,France,Velo,2826,15.00,42390.00,6358.50,36031.50,28260.00,7771.50,5/1/2014,5,May,2014 -Enterprise,France,Velo,663,125.00,82875.00,12431.25,70443.75,79560.00,-9116.25,9/1/2014,9,September,2014 -Small Business,United States of America,Velo,2574,300.00,772200.00,115830.00,656370.00,643500.00,12870.00,11/1/2013,11,November,2013 -Enterprise,United States of America,Velo,2438,125.00,304750.00,45712.50,259037.50,292560.00,-33522.50,12/1/2013,12,December,2013 -Channel Partners,United States of America,Velo,914,12.00,10968.00,1645.20,9322.80,2742.00,6580.80,12/1/2014,12,December,2014 -Government,Canada,VTT,865.5,20.00,17310.00,2596.50,14713.50,8655.00,6058.50,7/1/2014,7,July,2014 -Midmarket,Germany,VTT,492,15.00,7380.00,1107.00,6273.00,4920.00,1353.00,7/1/2014,7,July,2014 -Government,United States of America,VTT,267,20.00,5340.00,801.00,4539.00,2670.00,1869.00,10/1/2013,10,October,2013 -Midmarket,Germany,VTT,1175,15.00,17625.00,2643.75,14981.25,11750.00,3231.25,10/1/2014,10,October,2014 -Enterprise,Canada,VTT,2954,125.00,369250.00,55387.50,313862.50,354480.00,-40617.50,11/1/2013,11,November,2013 -Enterprise,Germany,VTT,552,125.00,69000.00,10350.00,58650.00,66240.00,-7590.00,11/1/2014,11,November,2014 -Government,France,VTT,293,20.00,5860.00,879.00,4981.00,2930.00,2051.00,12/1/2014,12,December,2014 -Small Business,France,Amarilla,2475,300.00,742500.00,111375.00,631125.00,618750.00,12375.00,3/1/2014,3,March,2014 -Small Business,Mexico,Amarilla,546,300.00,163800.00,24570.00,139230.00,136500.00,2730.00,10/1/2014,10,October,2014 -Government,Mexico,Montana,1368,7.00,9576.00,1436.40,8139.60,6840.00,1299.60,2/1/2014,2,February,2014 -Government,Canada,Paseo,723,7.00,5061.00,759.15,4301.85,3615.00,686.85,4/1/2014,4,April,2014 -Channel Partners,United States of America,VTT,1806,12.00,21672.00,3250.80,18421.20,5418.00,13003.20,5/1/2014,5,May,2014 diff --git a/dotnet/samples/ConceptsV2/Resources/travelinfo.txt b/dotnet/samples/ConceptsV2/Resources/travelinfo.txt deleted file mode 100644 index 21665c82198e..000000000000 --- a/dotnet/samples/ConceptsV2/Resources/travelinfo.txt +++ /dev/null @@ -1,217 +0,0 @@ -Invoice Booking Reference LMNOPQ Trip ID - 11110011111 -Passenger Name(s) -MARKS/SAM ALBERT Agent W2 - - -MICROSOFT CORPORATION 14820 NE 36TH STREET REDMOND WA US 98052 - -American Express Global Business Travel Microsoft Travel -14711 NE 29th Place, Suite 215 -Bellevue, WA 98007 -Phone: +1 (669) 210-8041 - - - - -BILLING CODE : 1010-10010110 -Invoice Information - - - - - - -Invoice Details -Ticket Number - - - - - - - -0277993883295 - - - - - - -Charges -Ticket Base Fare - - - - - - - -306.29 - -Airline Name - -ALASKA AIRLINES - -Ticket Tax Fare 62.01 - -Passenger Name Flight Details - -MARKS/SAM ALBERT -11 Sep 2023 ALASKA AIRLINES -0572 H Class -SEATTLE-TACOMA,WA/RALEIGH DURHAM,NC -13 Sep 2023 ALASKA AIRLINES -0491 M Class -RALEIGH DURHAM,NC/SEATTLE- TACOMA,WA - -Total (USD) Ticket Amount - -368.30 - -Credit Card Information -Charged to Card - - - -AX XXXXXXXXXXX4321 - - - -368.30 - - - - -Payment Details - - - -Charged by Airline -Total Invoice Charge - - - -USD - - - -368.30 -368.30 - -Monday 11 September 2023 - -10:05 AM - -Seattle (SEA) to Durham (RDU) -Airline Booking Ref: ABCXYZ - -Carrier: ALASKA AIRLINES - -Flight: AS 572 - -Status: Confirmed - -Operated By: ALASKA AIRLINES -Origin: Seattle, WA, Seattle-Tacoma International Apt (SEA) - -Departing: Monday 11 September 2023 at 10:05 AM Destination: Durham, Raleigh, Raleigh (RDU) Arriving: Monday 11 September 2023 at 06:15 PM -Additional Information - -Departure Terminal: Not Applicable - -Arrival Terminal: TERMINAL 2 - - -Class: ECONOMY -Aircraft Type: Boeing 737-900 -Meal Service: Not Applicable -Frequent Flyer Number: Not Applicable -Number of Stops: 0 -Greenhouse Gas Emissions: 560 kg CO2e / person - - -Distance: 2354 Miles Estimated Time: 05 hours 10 minutes -Seat: 24A - - -THE WESTIN RALEIGH DURHAM AP -Address: 3931 Macaw Street, Raleigh, NC, 27617, US -Phone: (1) 919-224-1400 Fax: (1) 919-224-1401 -Check In Date: Monday 11 September 2023 Check Out Date: Wednesday 13 September 2023 Number Of Nights: 2 -Rate: USD 280.00 per night may be subject to local taxes and service charges -Guaranteed to: AX XXXXXXXXXXX4321 - -Reference Number: 987654 -Additional Information -Membership ID: 123456789 -CANCEL PERMITTED UP TO 1 DAYS BEFORE CHECKIN - -Status: Confirmed - - -Corporate Id: Not Applicable - -Number Of Rooms: 1 - -Wednesday 13 September 2023 - -07:15 PM - -Durham (RDU) to Seattle (SEA) -Airline Booking Ref: ABCXYZ - -Carrier: ALASKA AIRLINES - -Flight: AS 491 - -Status: Confirmed - -Operated By: ALASKA AIRLINES -Origin: Durham, Raleigh, Raleigh (RDU) -Departing: Wednesday 13 September 2023 at 07:15 PM - - - -Departure Terminal: TERMINAL 2 - -Destination: Seattle, WA, Seattle-Tacoma International Apt (SEA) -Arriving: Wednesday 13 September 2023 at 09:59 PM Arrival Terminal: Not Applicable -Additional Information - - -Class: ECONOMY -Aircraft Type: Boeing 737-900 -Meal Service: Not Applicable -Frequent Flyer Number: Not Applicable -Number of Stops: 0 -Greenhouse Gas Emissions: 560 kg CO2e / person - - -Distance: 2354 Miles Estimated Time: 05 hours 44 minutes -Seat: 16A - - - -Greenhouse Gas Emissions -Total Greenhouse Gas Emissions for this trip is: 1120 kg CO2e / person -Air Fare Information - -Routing : ONLINE RESERVATION -Total Fare : USD 368.30 -Additional Messages -FOR 24X7 Travel Reservations Please Call 1-669-210-8041 Unable To Use Requested As Frequent Flyer Program Invalid Use Of Frequent Flyer Number 0123XYZ Please Contact Corresponding Frequent Travel Program Support Desk For Assistance -Trip Name-Trip From Seattle To Raleigh/Durham -This Ticket Is Nonrefundable. Changes Or Cancellations Must Be Made Prior To Scheduled Flight Departure -All Changes Must Be Made On Same Carrier And Will Be Subject To Service Fee And Difference In Airfare -******************************************************* -Please Be Advised That Certain Mandatory Hotel-Imposed Charges Including But Not Limited To Daily Resort Or Facility Fees May Be Applicable To Your Stay And Payable To The Hotel Operator At Check-Out From The Property. You May Wish To Inquire With The Hotel Before Your Trip Regarding The Existence And Amount Of Such Charges. -******************************************************* -Hotel Cancel Policies Vary Depending On The Property And Date. If You Have Questions Regarding Cancellation Fees Please Call The Travel Office. -Important Information -COVID-19 Updates: Click here to access Travel Vitals https://travelvitals.amexgbt.com for the latest information and advisories compiled by American Express Global Business Travel. - -Carbon Emissions: The total emissions value for this itinerary includes air travel only. Emissions for each individual flight are displayed in the flight details section. For more information on carbon emissions please refer to https://www.amexglobalbusinesstravel.com/sustainable-products-and-platforms. - -For important information regarding your booking in relation to the conditions applying to your booking, managing your booking and travel advisory, please refer to www.amexglobalbusinesstravel.com/booking-info. - -GBT Travel Services UK Limited (GBT UK) and its authorized sublicensees (including Ovation Travel Group and Egencia) use certain trademarks and service marks of American Express Company or its subsidiaries (American Express) in the American Express Global Business Travel and American Express Meetings & Events brands and in connection with its business for permitted uses only under a limited license from American Express (Licensed Marks). The Licensed Marks are trademarks or service marks of, and the property of, American Express. GBT UK is a subsidiary of Global Business Travel Group, Inc. (NYSE: GBTG). American Express holds a minority interest in GBTG, which operates as a separate company from American Express. From 4992cd3ec12afac1ff454fcf2a43044c626ae08e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:18:51 -0700 Subject: [PATCH 077/226] Namespace --- dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index d75e4fad7026..81b2914ade3b 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; From daed2f840cbf6edd9147385bc08e3e53765f425d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 09:23:22 -0700 Subject: [PATCH 078/226] Whitespace --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 182acac7c9c2..c301237b917b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -263,7 +263,7 @@ protected override async Task CreateChannelAsync(CancellationToken this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); OpenAIAssistantChannel channel = - new (this._client, thread.Id) + new(this._client, thread.Id) { Logger = this.LoggerFactory.CreateLogger() }; From 6bd518c5cb54f39385035be87f6f59cbcf4e5728 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 10:49:41 -0700 Subject: [PATCH 079/226] Resolve OpenAIV2 dependency --- .../GettingStartedWithAgents.csproj | 1 + .../InternalUtilities/samples/InternalUtilities/BaseTest.cs | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index b95bbd546d34..99d086787951 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -39,6 +39,7 @@ + diff --git a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs index 4e41ebf6bf24..d71d3c1f0032 100644 --- a/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs +++ b/dotnet/src/InternalUtilities/samples/InternalUtilities/BaseTest.cs @@ -43,9 +43,9 @@ protected Kernel CreateKernelWithChatCompletion() if (this.UseOpenAIConfig) { - //builder.AddOpenAIChatCompletion( // %%% CONNECTOR - // TestConfiguration.OpenAI.ChatModelId, - // TestConfiguration.OpenAI.ApiKey); + builder.AddOpenAIChatCompletion( + TestConfiguration.OpenAI.ChatModelId, + TestConfiguration.OpenAI.ApiKey); } else { From 51f80c5964fc872340e946b0151adeb4289afc90 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 12:59:27 -0700 Subject: [PATCH 080/226] Remove file-service usage --- .../Concepts/Agents/Legacy_AgentCharts.cs | 36 +++++++-------- .../Concepts/Agents/Legacy_AgentTools.cs | 45 +++++++++---------- .../OpenAIAssistant_FileManipulation.cs | 33 +++++++++----- .../Agents/OpenAIAssistant_FileSearch.cs | 27 ++++++++--- dotnet/samples/Concepts/Concepts.csproj | 1 + 5 files changed, 84 insertions(+), 58 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index 877ba0971710..957b06e51f96 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents; +using OpenAI; +using OpenAI.Files; namespace Agents; @@ -18,13 +20,6 @@ public sealed class Legacy_AgentCharts(ITestOutputHelper output) : BaseTest(outp /// private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - private new const bool ForceOpenAI = false; - /// /// Create a chart and retrieve by file_id. /// @@ -33,7 +28,7 @@ public async Task CreateChartAsync() { Console.WriteLine("======== Using CodeInterpreter tool ========"); - var fileService = CreateFileService(); + FileClient fileClient = CreateFileClient(); var agent = await CreateAgentBuilder().WithCodeInterpreter().BuildAsync(); @@ -69,11 +64,12 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi { var filename = $"{imageName}.jpg"; var path = Path.Combine(Environment.CurrentDirectory, filename); - Console.WriteLine($"# {message.Role}: {message.Content}"); + var fileId = message.Content; + Console.WriteLine($"# {message.Role}: {fileId}"); Console.WriteLine($"# {message.Role}: {path}"); - var content = await fileService.GetFileContentAsync(message.Content); + BinaryData content = await fileClient.DownloadFileAsync(fileId); await using var outputStream = File.OpenWrite(filename); - await outputStream.WriteAsync(content.Data!.Value); + await outputStream.WriteAsync(content.ToArray()); Process.Start( new ProcessStartInfo { @@ -91,18 +87,20 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi } } - private static OpenAIFileService CreateFileService() + private FileClient CreateFileClient() { - return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new OpenAIFileService(TestConfiguration.OpenAI.ApiKey) : - new OpenAIFileService(new Uri(TestConfiguration.AzureOpenAI.Endpoint), apiKey: TestConfiguration.AzureOpenAI.ApiKey); + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs index c285810fcb74..baf5b249dd33 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents; +using OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -19,16 +20,6 @@ public sealed class Legacy_AgentTools(ITestOutputHelper output) : BaseTest(outpu /// private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - /// - /// NOTE: Retrieval tools is not currently available on Azure. - /// - private new const bool ForceOpenAI = true; - // Track agents for clean-up private readonly List _agents = []; @@ -79,12 +70,13 @@ public async Task RunRetrievalToolAsync() return; } - Kernel kernel = CreateFileEnabledKernel(); - var fileService = kernel.GetRequiredService(); - var result = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); + FileClient fileClient = CreateFileClient(); + + OpenAIFileInfo result = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!), + "travelinfo.txt", + FileUploadPurpose.Assistants); var fileId = result.Id; Console.WriteLine($"! {fileId}"); @@ -110,7 +102,7 @@ await ChatAsync( } finally { - await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId))); + await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileClient.DeleteFileAsync(fileId))); } } @@ -165,13 +157,20 @@ async Task InvokeAgentAsync(IAgent agent, string question) } } - private static Kernel CreateFileEnabledKernel() => - Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build(); + private FileClient CreateFileClient() + { + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); + } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index a0fa5a074694..0272ed1eb8de 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -1,10 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -22,11 +24,13 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), - new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); + FileClient fileClient = CreateFileClient(); + + OpenAIFileInfo uploadFile = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("sales.csv")!), + "sales.csv", + FileUploadPurpose.Assistants); // Define the agent OpenAIAssistantAgent agent = @@ -53,7 +57,7 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); + await fileClient.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -70,9 +74,8 @@ async Task InvokeAgentAsync(string input) foreach (AnnotationContent annotation in message.Items.OfType()) { Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - Console.WriteLine(Encoding.Default.GetString(byteContent)); + BinaryData content = await fileClient.DownloadFileAsync(annotation.FileId!); + Console.WriteLine(Encoding.Default.GetString(content.ToArray())); } } } @@ -83,4 +86,14 @@ private OpenAIServiceConfiguration GetOpenAIConfiguration() this.UseOpenAIConfig ? OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + + private FileClient CreateFileClient() + { + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); + } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 9fef5a0c5830..157fb671ac13 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -1,9 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI; +using OpenAI.Files; using OpenAI.VectorStores; using Resources; @@ -22,10 +24,13 @@ public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(out [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); + FileClient fileClient = CreateFileClient(); + + OpenAIFileInfo uploadFile = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!), + "travelinfo.txt", + FileUploadPurpose.Assistants); VectorStore vectorStore = await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) @@ -59,7 +64,7 @@ await OpenAIAssistantAgent.CreateAsync( { await agent.DeleteAsync(); await openAIStore.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); + await fileClient.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -81,4 +86,14 @@ private OpenAIServiceConfiguration GetOpenAIConfiguration() this.UseOpenAIConfig ? OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + + private FileClient CreateFileClient() + { + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); + } } diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 7233a8131715..edeb850eafd8 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -105,6 +105,7 @@ + From 9e7e2dc289deaab7aed8d29f154511adeaf80edd Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 13:07:28 -0700 Subject: [PATCH 081/226] Fix concepts resources --- dotnet/samples/Concepts/Concepts.csproj | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index edeb850eafd8..87b9ccfec4fc 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -103,6 +103,9 @@ Always + + Always + From 37363fd041534ff5320f12802cd13ef284a75ea4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 13:18:51 -0700 Subject: [PATCH 082/226] Fix sample migration --- .../Concepts/Agents/Legacy_AgentAuthoring.cs | 16 ++++------ .../Concepts/Agents/Legacy_AgentCharts.cs | 10 ++----- .../Agents/Legacy_AgentCollaboration.cs | 29 +++++-------------- .../Concepts/Agents/Legacy_AgentDelegation.cs | 14 ++------- .../Concepts/Agents/Legacy_AgentTools.cs | 9 ++---- .../samples/Concepts/Agents/Legacy_Agents.cs | 21 +++----------- 6 files changed, 24 insertions(+), 75 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs index 062262fe8a8c..32f8c83d0e6e 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs @@ -9,16 +9,10 @@ namespace Agents; /// public class Legacy_AgentAuthoring(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - // Track agents for clean-up private static readonly List s_agents = []; - [Fact(Skip = "This test take more than 2 minutes to execute")] + [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] public async Task RunAgentAsync() { Console.WriteLine($"======== {nameof(Legacy_AgentAuthoring)} ========"); @@ -40,7 +34,7 @@ public async Task RunAgentAsync() } } - [Fact(Skip = "This test take more than 2 minutes to execute")] + [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] public async Task RunAsPluginAsync() { Console.WriteLine($"======== {nameof(Legacy_AgentAuthoring)} ========"); @@ -72,7 +66,7 @@ private static async Task CreateArticleGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("You write concise opinionated articles that are published online. Use an outline to generate an article with one section of prose for each top-level outline element. Each section is based on research with a maximum of 120 words.") .WithName("Article Author") .WithDescription("Author an article on a given topic.") @@ -87,7 +81,7 @@ private static async Task CreateOutlineGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("Produce an single-level outline (no child elements) based on the given topic with at most 3 sections.") .WithName("Outline Generator") .WithDescription("Generate an outline.") @@ -100,7 +94,7 @@ private static async Task CreateResearchGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("Provide insightful research that supports the given topic based on your knowledge of the outline topic.") .WithName("Researcher") .WithDescription("Author research summary.") diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index 957b06e51f96..0f93c701683f 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -14,16 +14,10 @@ namespace Agents; /// public sealed class Legacy_AgentCharts(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - /// /// Create a chart and retrieve by file_id. /// - [Fact(Skip = "Launches external processes")] + [Fact/*(Skip = "Launches external processes")*/] public async Task CreateChartAsync() { Console.WriteLine("======== Using CodeInterpreter tool ========"); @@ -101,7 +95,7 @@ private AgentBuilder CreateAgentBuilder() { return this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } } diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs index 53ae0c07662a..22ccc749f3eb 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs @@ -9,28 +9,15 @@ namespace Agents; /// public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-turbo-preview"; - - /// - /// Set this to 'true' to target OpenAI instead of Azure OpenAI. - /// - private const bool UseOpenAI = false; - // Track agents for clean-up private static readonly List s_agents = []; /// /// Show how two agents are able to collaborate as agents on a single thread. /// - [Fact(Skip = "This test take more than 5 minutes to execute")] + [Fact/*(Skip = "This test take more than 5 minutes to execute")*/] public async Task RunCollaborationAsync() { - Console.WriteLine($"======== Example72:Collaboration:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========"); - IAgentThread? thread = null; try { @@ -79,11 +66,9 @@ public async Task RunCollaborationAsync() /// While this may achieve an equivalent result to , /// it is not using shared thread state for agent interaction. /// - [Fact(Skip = "This test take more than 2 minutes to execute")] + [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] public async Task RunAsPluginsAsync() { - Console.WriteLine($"======== Example72:AsPlugins:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========"); - try { // Create copy-writer agent to generate ideas @@ -113,7 +98,7 @@ await CreateAgentBuilder() } } - private static async Task CreateCopyWriterAsync(IAgent? agent = null) + private async Task CreateCopyWriterAsync(IAgent? agent = null) { return Track( @@ -125,7 +110,7 @@ await CreateAgentBuilder() .BuildAsync()); } - private static async Task CreateArtDirectorAsync() + private async Task CreateArtDirectorAsync() { return Track( @@ -136,13 +121,13 @@ await CreateAgentBuilder() .BuildAsync()); } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { var builder = new AgentBuilder(); return - UseOpenAI ? - builder.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + builder.WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : builder.WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs index 86dacb9c256d..08f7e99096f0 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs @@ -12,12 +12,6 @@ namespace Agents; /// public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106"; - // Track agents for clean-up private static readonly List s_agents = []; @@ -27,8 +21,6 @@ public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task RunAsync() { - Console.WriteLine("======== Example71_AgentDelegation ========"); - if (TestConfiguration.OpenAI.ApiKey is null) { Console.WriteLine("OpenAI apiKey not found. Skipping example."); @@ -43,7 +35,7 @@ public async Task RunAsync() var menuAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml")) .WithDescription("Answer questions about how the menu uses the tool.") .WithPlugin(plugin) @@ -52,14 +44,14 @@ public async Task RunAsync() var parrotAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml")) .BuildAsync()); var toolAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml")) .WithPlugin(parrotAgent.AsPlugin()) .WithPlugin(menuAgent.AsPlugin()) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs index baf5b249dd33..00af8faab617 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs @@ -14,11 +14,8 @@ namespace Agents; /// public sealed class Legacy_AgentTools(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; + /// + protected override bool ForceOpenAI => true; // Track agents for clean-up private readonly List _agents = []; @@ -171,7 +168,7 @@ private AgentBuilder CreateAgentBuilder() { return this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs index 5af10987bb3a..0a03d4c10809 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs @@ -13,19 +13,6 @@ namespace Agents; /// public class Legacy_Agents(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106"; - - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - private new const bool ForceOpenAI = false; - /// /// Chat using the "Parrot" agent. /// Tools/functions: None @@ -114,7 +101,7 @@ public async Task RunAsFunctionAsync() // Create parrot agent, same as the other cases. var agent = await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml")) .BuildAsync(); @@ -187,11 +174,11 @@ await Task.WhenAll( } } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } } From 1c986cf5f3ebe142efd0f4d77281f4c7131919c4 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 13:24:16 -0700 Subject: [PATCH 083/226] Restore skipped samples --- dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs | 4 ++-- dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs | 2 +- dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs index 32f8c83d0e6e..53276c75a24d 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs @@ -12,7 +12,7 @@ public class Legacy_AgentAuthoring(ITestOutputHelper output) : BaseTest(output) // Track agents for clean-up private static readonly List s_agents = []; - [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] + [Fact(Skip = "This test take more than 2 minutes to execute")] public async Task RunAgentAsync() { Console.WriteLine($"======== {nameof(Legacy_AgentAuthoring)} ========"); @@ -34,7 +34,7 @@ public async Task RunAgentAsync() } } - [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] + [Fact(Skip = "This test take more than 2 minutes to execute")] public async Task RunAsPluginAsync() { Console.WriteLine($"======== {nameof(Legacy_AgentAuthoring)} ========"); diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index 0f93c701683f..0f1485e9c9be 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -17,7 +17,7 @@ public sealed class Legacy_AgentCharts(ITestOutputHelper output) : BaseTest(outp /// /// Create a chart and retrieve by file_id. /// - [Fact/*(Skip = "Launches external processes")*/] + [Fact] public async Task CreateChartAsync() { Console.WriteLine("======== Using CodeInterpreter tool ========"); diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs index 22ccc749f3eb..fa257d2764b3 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs @@ -15,7 +15,7 @@ public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(outp /// /// Show how two agents are able to collaborate as agents on a single thread. /// - [Fact/*(Skip = "This test take more than 5 minutes to execute")*/] + [Fact(Skip = "This test take more than 5 minutes to execute")] public async Task RunCollaborationAsync() { IAgentThread? thread = null; @@ -66,7 +66,7 @@ public async Task RunCollaborationAsync() /// While this may achieve an equivalent result to , /// it is not using shared thread state for agent interaction. /// - [Fact/*(Skip = "This test take more than 2 minutes to execute")*/] + [Fact(Skip = "This test take more than 2 minutes to execute")] public async Task RunAsPluginsAsync() { try From 448af97d22d95663e80da6b7d4299e879abbbf03 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 12 Jul 2024 13:32:15 -0700 Subject: [PATCH 084/226] /sigh --- dotnet/Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index ac2156a1b8bf..47a1a49b4c68 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -5,7 +5,7 @@ true - + From 9fae258e9f0b32702cca6a8785c78c0395c34f32 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Jul 2024 10:32:54 +0100 Subject: [PATCH 085/226] .Net Remove Azure* redundant function calling classes (#7236) ### Motivation and Context To speed up the migration of {Azure}OpenAI connectors to the Azure.AI.OpenAI SDK v2, it made sense to duplicate classes related to the auto function calling model/functionality. However, after the connectors were migrated, there is no reason to keep two parallel hierarchies of classes instead of just one and reuse them for both chat completion connectors. ### Description This PR removes the set of Azure* classes related to function calling and updates the AzureChatCompletion connector to use the equivalent classes from the new OpenAI project. --- .../AzureOpenAIToolCallBehaviorTests.cs | 238 ------- .../Core/AzureOpenAIFunctionToolCallTests.cs | 81 --- ...reOpenAIPluginCollectionExtensionsTests.cs | 75 --- .../Extensions/ChatHistoryExtensionsTests.cs | 45 -- .../AutoFunctionInvocationFilterTests.cs | 630 ------------------ .../AzureOpenAIFunctionTests.cs | 188 ------ .../KernelFunctionMetadataExtensionsTests.cs | 256 ------- .../AzureOpenAIChatCompletionServiceTests.cs | 33 +- .../AzureOpenAIToolCallBehavior.cs | 279 -------- .../ChatHistoryExtensions.cs | 70 -- .../Connectors.AzureOpenAI.csproj | 3 +- .../Core/AzureOpenAIChatMessageContent.cs | 9 +- .../Core/AzureOpenAIFunction.cs | 178 ----- .../Core/AzureOpenAIFunctionToolCall.cs | 170 ----- ...eOpenAIKernelFunctionMetadataExtensions.cs | 54 -- .../AzureOpenAIPluginCollectionExtensions.cs | 62 -- .../Core/ClientCore.ChatCompletion.cs | 27 +- .../AzureOpenAIPromptExecutionSettings.cs | 13 +- .../Connectors.OpenAIV2.csproj | 1 + ...enAIChatCompletion_FunctionCallingTests.cs | 35 +- 20 files changed, 63 insertions(+), 2384 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs deleted file mode 100644 index 6baa78faae1e..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIToolCallBehaviorTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using OpenAI.Chat; -using static Microsoft.SemanticKernel.Connectors.AzureOpenAI.AzureOpenAIToolCallBehavior; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; - -/// -/// Unit tests for -/// -public sealed class AzureOpenAIToolCallBehaviorTests -{ - [Fact] - public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - var behavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - const int DefaultMaximumAutoInvokeAttempts = 128; - var behavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(DefaultMaximumAutoInvokeAttempts, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void EnableFunctionsReturnsEnabledFunctionsInstance() - { - // Arrange & Act - List functions = [new("Plugin", "Function", "description", [], null)]; - var behavior = AzureOpenAIToolCallBehavior.EnableFunctions(functions); - - // Assert - Assert.IsType(behavior); - } - - [Fact] - public void RequireFunctionReturnsRequiredFunctionInstance() - { - // Arrange & Act - var behavior = AzureOpenAIToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); - - // Assert - Assert.IsType(behavior); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - - // Act - var options = kernelFunctions.ConfigureOptions(null); - - // Assert - Assert.Null(options.Choice); - Assert.Null(options.Tools); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - var kernel = Kernel.CreateBuilder().Build(); - - // Act - var options = kernelFunctions.ConfigureOptions(kernel); - - // Assert - Assert.Null(options.Choice); - Assert.Null(options.Tools); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - var kernel = Kernel.CreateBuilder().Build(); - - var plugin = this.GetTestPlugin(); - - kernel.Plugins.Add(plugin); - - // Act - var options = kernelFunctions.ConfigureOptions(kernel); - - // Assert - Assert.Equal(ChatToolChoice.Auto, options.Choice); - - this.AssertTools(options.Tools); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() - { - // Arrange - var enabledFunctions = new EnabledFunctions([], autoInvoke: false); - - // Act - var options = enabledFunctions.ConfigureOptions(null); - - // Assert - Assert.Null(options.Choice); - Assert.Null(options.Tools); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null)); - Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel)); - Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool autoInvoke) - { - // Arrange - var plugin = this.GetTestPlugin(); - var functions = plugin.GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke); - var kernel = Kernel.CreateBuilder().Build(); - - kernel.Plugins.Add(plugin); - - // Act - var options = enabledFunctions.ConfigureOptions(kernel); - - // Assert - Assert.Equal(ChatToolChoice.Auto, options.Choice); - - this.AssertTools(options.Tools); - } - - [Fact] - public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - - // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null)); - Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToAzureOpenAIFunction()).First(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel)); - Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); - } - - [Fact] - public void RequiredFunctionConfigureOptionsAddsTools() - { - // Arrange - var plugin = this.GetTestPlugin(); - var function = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var kernel = new Kernel(); - kernel.Plugins.Add(plugin); - - // Act - var options = requiredFunction.ConfigureOptions(kernel); - - // Assert - Assert.NotNull(options.Choice); - - this.AssertTools(options.Tools); - } - - private KernelPlugin GetTestPlugin() - { - var function = KernelFunctionFactory.CreateFromMethod( - (string parameter1, string parameter2) => "Result1", - "MyFunction", - "Test Function", - [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], - new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); - - return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - } - - private void AssertTools(IList? tools) - { - Assert.NotNull(tools); - var tool = Assert.Single(tools); - - Assert.NotNull(tool); - - Assert.Equal("MyPlugin-MyFunction", tool.FunctionName); - Assert.Equal("Test Function", tool.FunctionDescription); - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.FunctionParameters.ToString()); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs deleted file mode 100644 index d8342b4991d4..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIFunctionToolCallTests.cs +++ /dev/null @@ -1,81 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using OpenAI.Chat; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIFunctionToolCallTests -{ - [Theory] - [InlineData("MyFunction", "MyFunction")] - [InlineData("MyPlugin_MyFunction", "MyPlugin_MyFunction")] - public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) - { - // Arrange - var toolCall = ChatToolCall.CreateFunctionToolCall("id", toolCallName, string.Empty); - var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); - - // Act & Assert - Assert.Equal(expectedName, openAIFunctionToolCall.FullyQualifiedName); - Assert.Same(openAIFunctionToolCall.FullyQualifiedName, openAIFunctionToolCall.FullyQualifiedName); - } - - [Fact] - public void ToStringReturnsCorrectValue() - { - // Arrange - var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); - var openAIFunctionToolCall = new AzureOpenAIFunctionToolCall(toolCall); - - // Act & Assert - Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", openAIFunctionToolCall.ToString()); - } - - [Fact] - public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() - { - // Arrange - var toolCallIdsByIndex = new Dictionary(); - var functionNamesByIndex = new Dictionary(); - var functionArgumentBuildersByIndex = new Dictionary(); - - // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( - ref toolCallIdsByIndex, - ref functionNamesByIndex, - ref functionArgumentBuildersByIndex); - - // Assert - Assert.Empty(toolCalls); - } - - [Fact] - public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() - { - // Arrange - var toolCallIdsByIndex = new Dictionary { { 3, "test-id" } }; - var functionNamesByIndex = new Dictionary { { 3, "test-function" } }; - var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; - - // Act - var toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( - ref toolCallIdsByIndex, - ref functionNamesByIndex, - ref functionArgumentBuildersByIndex); - - // Assert - Assert.Single(toolCalls); - - var toolCall = toolCalls[0]; - - Assert.Equal("test-id", toolCall.Id); - Assert.Equal("test-function", toolCall.FunctionName); - Assert.Equal("test-argument", toolCall.FunctionArguments); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs deleted file mode 100644 index e0642abc52e1..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIPluginCollectionExtensionsTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using OpenAI.Chat; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIPluginCollectionExtensionsTests -{ - [Fact] - public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() - { - // Arrange - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); - var plugins = new KernelPluginCollection([plugin]); - - var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.False(result); - Assert.Null(actualFunction); - Assert.Null(actualArguments); - } - - [Fact] - public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - - var plugins = new KernelPluginCollection([plugin]); - var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.True(result); - Assert.Equal(function.Name, actualFunction?.Name); - Assert.Null(actualArguments); - } - - [Fact] - public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - - var plugins = new KernelPluginCollection([plugin]); - var toolCall = ChatToolCall.CreateFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.True(result); - Assert.Equal(function.Name, actualFunction?.Name); - - Assert.NotNull(actualArguments); - - Assert.Equal("San Diego", actualArguments["location"]); - Assert.Equal("300", actualArguments["max_price"]); - - Assert.Null(actualArguments["null_argument"]); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs deleted file mode 100644 index 94fc1e5d1a5c..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Extensions; -public class ChatHistoryExtensionsTests -{ - [Fact] - public async Task ItCanAddMessageFromStreamingChatContentsAsync() - { - var metadata = new Dictionary() - { - { "message", "something" }, - }; - - var chatHistoryStreamingContents = new List - { - new(AuthorRole.User, "Hello ", metadata: metadata), - new(null, ", ", metadata: metadata), - new(null, "I ", metadata: metadata), - new(null, "am ", metadata : metadata), - new(null, "a ", metadata : metadata), - new(null, "test ", metadata : metadata), - }.ToAsyncEnumerable(); - - var chatHistory = new ChatHistory(); - var finalContent = "Hello , I am a test "; - string processedContent = string.Empty; - await foreach (var chatMessageChunk in chatHistory.AddStreamingMessageAsync(chatHistoryStreamingContents)) - { - processedContent += chatMessageChunk.Content; - } - - Assert.Single(chatHistory); - Assert.Equal(finalContent, processedContent); - Assert.Equal(finalContent, chatHistory[0].Content); - Assert.Equal(AuthorRole.User, chatHistory[0].Role); - Assert.Equal(metadata["message"], chatHistory[0].Metadata!["message"]); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs deleted file mode 100644 index 195f71e2758f..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AutoFunctionInvocationFilterTests.cs +++ /dev/null @@ -1,630 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; - -public sealed class AutoFunctionInvocationFilterTests : IDisposable -{ - private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - - public AutoFunctionInvocationFilterTests() - { - this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); - - this._httpClient = new HttpClient(this._messageHandlerStub, false); - } - - [Fact] - public async Task FiltersAreExecutedCorrectlyAsync() - { - // Arrange - int filterInvocations = 0; - int functionInvocations = 0; - int[] expectedRequestSequenceNumbers = [0, 0, 1, 1]; - int[] expectedFunctionSequenceNumbers = [0, 1, 0, 1]; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - Kernel? contextKernel = null; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - contextKernel = context.Kernel; - - if (context.ChatHistory.Last() is AzureOpenAIChatMessageContent content) - { - Assert.Equal(2, content.ToolCalls.Count); - } - - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - filterInvocations++; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(4, filterInvocations); - Assert.Equal(4, functionInvocations); - Assert.Equal(expectedRequestSequenceNumbers, requestSequenceNumbers); - Assert.Equal(expectedFunctionSequenceNumbers, functionSequenceNumbers); - Assert.Same(kernel, contextKernel); - Assert.Equal("Test chat response", result.ToString()); - } - - [Fact] - public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() - { - // Arrange - int filterInvocations = 0; - int functionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - if (context.ChatHistory.Last() is AzureOpenAIChatMessageContent content) - { - Assert.Equal(2, content.ToolCalls.Count); - } - - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - filterInvocations++; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { } - - // Assert - Assert.Equal(4, filterInvocations); - Assert.Equal(4, functionInvocations); - Assert.Equal([0, 0, 1, 1], requestSequenceNumbers); - Assert.Equal([0, 1, 0, 1], functionSequenceNumbers); - } - - [Fact] - public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); - var executionOrder = new List(); - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var filter1 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter1-Invoking"); - await next(context); - }); - - var filter2 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter2-Invoking"); - await next(context); - }); - - var builder = Kernel.CreateBuilder(); - - builder.Plugins.Add(plugin); - - builder.Services.AddSingleton((serviceProvider) => - { - return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - - // Case #1 - Add filter to services - builder.Services.AddSingleton(filter1); - - var kernel = builder.Build(); - - // Case #2 - Add filter to kernel - kernel.AutoFunctionInvocationFilters.Add(filter2); - - var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal("Filter1-Invoking", executionOrder[0]); - Assert.Equal("Filter2-Invoking", executionOrder[1]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); - var executionOrder = new List(); - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var filter1 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter1-Invoking"); - await next(context); - executionOrder.Add("Filter1-Invoked"); - }); - - var filter2 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter2-Invoking"); - await next(context); - executionOrder.Add("Filter2-Invoked"); - }); - - var filter3 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter3-Invoking"); - await next(context); - executionOrder.Add("Filter3-Invoked"); - }); - - var builder = Kernel.CreateBuilder(); - - builder.Plugins.Add(plugin); - - builder.Services.AddSingleton((serviceProvider) => - { - return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - }); - - builder.Services.AddSingleton(filter1); - builder.Services.AddSingleton(filter2); - builder.Services.AddSingleton(filter3); - - var kernel = builder.Build(); - - var arguments = new KernelArguments(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - }); - - // Act - if (isStreaming) - { - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", arguments)) - { } - } - else - { - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - await kernel.InvokePromptAsync("Test prompt", arguments); - } - - // Assert - Assert.Equal("Filter1-Invoking", executionOrder[0]); - Assert.Equal("Filter2-Invoking", executionOrder[1]); - Assert.Equal("Filter3-Invoking", executionOrder[2]); - Assert.Equal("Filter3-Invoked", executionOrder[3]); - Assert.Equal("Filter2-Invoked", executionOrder[4]); - Assert.Equal("Filter1-Invoked", executionOrder[5]); - } - - [Fact] - public async Task FilterCanOverrideArgumentsAsync() - { - // Arrange - const string NewValue = "NewValue"; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - context.Arguments!["parameter"] = NewValue; - await next(context); - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal("NewValue", result.ToString()); - } - - [Fact] - public async Task FilterCanHandleExceptionAsync() - { - // Arrange - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - try - { - await next(context); - } - catch (KernelException exception) - { - Assert.Equal("Exception from Function1", exception.Message); - context.Result = new FunctionResult(context.Result, "Result from filter"); - } - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("System message"); - - // Act - var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); - - var firstFunctionResult = chatHistory[^2].Content; - var secondFunctionResult = chatHistory[^1].Content; - - // Assert - Assert.Equal("Result from filter", firstFunctionResult); - Assert.Equal("Result from Function2", secondFunctionResult); - } - - [Fact] - public async Task FilterCanHandleExceptionOnStreamingAsync() - { - // Arrange - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - try - { - await next(context); - } - catch (KernelException) - { - context.Result = new FunctionResult(context.Result, "Result from filter"); - } - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var chatCompletion = new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - - var chatHistory = new ChatHistory(); - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) - { } - - var firstFunctionResult = chatHistory[^2].Content; - var secondFunctionResult = chatHistory[^1].Content; - - // Assert - Assert.Equal("Result from filter", firstFunctionResult); - Assert.Equal("Result from Function2", secondFunctionResult); - } - - [Fact] - public async Task FiltersCanSkipFunctionExecutionAsync() - { - // Arrange - int filterInvocations = 0; - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Filter delegate is invoked only for second function, the first one should be skipped. - if (context.Function.Name == "Function2") - { - await next(context); - } - - filterInvocations++; - }); - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(2, filterInvocations); - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(1, secondFunctionInvocations); - } - - [Fact] - public async Task PreFilterCanTerminateOperationAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Terminating before first function, so all functions won't be invoked. - context.Terminate = true; - - await next(context); - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - } - - [Fact] - public async Task PreFilterCanTerminateOperationOnStreamingAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Terminating before first function, so all functions won't be invoked. - context.Terminate = true; - - await next(context); - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { } - - // Assert - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - } - - [Fact] - public async Task PostFilterCanTerminateOperationAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - // Terminating after first function, so second function won't be invoked. - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new AzureOpenAIPromptExecutionSettings - { - ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(1, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - Assert.Equal([0], requestSequenceNumbers); - Assert.Equal([0], functionSequenceNumbers); - - // Results of function invoked before termination should be returned - var lastMessageContent = result.GetValue(); - Assert.NotNull(lastMessageContent); - - Assert.Equal("function1-value", lastMessageContent.Content); - Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); - } - - [Fact] - public async Task PostFilterCanTerminateOperationOnStreamingAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - // Terminating after first function, so second function won't be invoked. - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new AzureOpenAIPromptExecutionSettings { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; - - List streamingContent = []; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { - streamingContent.Add(item); - } - - // Assert - Assert.Equal(1, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - Assert.Equal([0], requestSequenceNumbers); - Assert.Equal([0], functionSequenceNumbers); - - // Results of function invoked before termination should be returned - Assert.Equal(3, streamingContent.Count); - - var lastMessageContent = streamingContent[^1] as StreamingChatMessageContent; - Assert.NotNull(lastMessageContent); - - Assert.Equal("function1-value", lastMessageContent.Content); - Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - #region private - -#pragma warning disable CA2000 // Dispose objects before losing scope - private static List GetFunctionCallingResponses() - { - return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_multiple_function_calls_test_response.json") }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_test_response.json") } - ]; - } - - private static List GetFunctionCallingStreamingResponses() - { - return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("filters_streaming_multiple_function_calls_test_response.txt") }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") } - ]; - } -#pragma warning restore CA2000 - - private Kernel GetKernelWithFilter( - KernelPlugin plugin, - Func, Task>? onAutoFunctionInvocation) - { - var builder = Kernel.CreateBuilder(); - var filter = new AutoFunctionInvocationFilter(onAutoFunctionInvocation); - - builder.Plugins.Add(plugin); - builder.Services.AddSingleton(filter); - - builder.Services.AddSingleton((serviceProvider) => - { - return new AzureOpenAIChatCompletionService("test-deployment", "https://endpoint", "test-api-key", "test-model-id", this._httpClient); - }); - - return builder.Build(); - } - - private sealed class AutoFunctionInvocationFilter( - Func, Task>? onAutoFunctionInvocation) : IAutoFunctionInvocationFilter - { - private readonly Func, Task>? _onAutoFunctionInvocation = onAutoFunctionInvocation; - - public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) => - this._onAutoFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs deleted file mode 100644 index cf83f89bc783..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/AzureOpenAIFunctionTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using OpenAI.Chat; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; - -public sealed class AzureOpenAIFunctionTests -{ - [Theory] - [InlineData(null, null, "", "")] - [InlineData("name", "description", "name", "description")] - public void ItInitializesOpenAIFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); - var functionParameter = new AzureOpenAIFunctionParameter(name, description, true, typeof(string), schema); - - // Assert - Assert.Equal(expectedName, functionParameter.Name); - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.True(functionParameter.IsRequired); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Theory] - [InlineData(null, "")] - [InlineData("description", "description")] - public void ItInitializesOpenAIFunctionReturnParameterCorrectly(string? description, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); - var functionParameter = new AzureOpenAIFunctionReturnParameter(description, typeof(string), schema); - - // Assert - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNoPluginName() - { - // Arrange - AzureOpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToAzureOpenAIFunction(); - - // Act - ChatTool result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal(sut.FunctionName, result.FunctionName); - Assert.Equal(sut.Description, result.FunctionDescription); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNullParameters() - { - // Arrange - AzureOpenAIFunction sut = new("plugin", "function", "description", null, null); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.FunctionParameters.ToString()); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithPluginName() - { - // Arrange - AzureOpenAIFunction sut = KernelPluginFactory.CreateFromFunctions("myplugin", new[] - { - KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.") - }).GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - - // Act - ChatTool result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal("myplugin-myfunc", result.FunctionName); - Assert.Equal(sut.Description, result.FunctionDescription); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() - { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", - "TestFunction", - "My test function") - }); - - AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - - ChatTool functionDefinition = sut.ToFunctionDefinition(); - - var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); - var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters)); - - Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); - Assert.Equal("My test function", functionDefinition.FunctionDescription); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() - { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, - "TestFunction", - "My test function") - }); - - AzureOpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToAzureOpenAIFunction(); - - ChatTool functionDefinition = sut.ToFunctionDefinition(); - - Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.FunctionName); - Assert.Equal("My test function", functionDefinition.FunctionDescription); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.FunctionParameters))); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() - { - // Arrange - AzureOpenAIFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: [new KernelParameterMetadata("param1")]).Metadata.ToAzureOpenAIFunction(); - - // Act - ChatTool result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; - - // Assert - Assert.NotNull(pd.properties); - Assert.Single(pd.properties); - Assert.Equal( - JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string" }""")), - JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescriptions() - { - // Arrange - AzureOpenAIFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToAzureOpenAIFunction(); - - // Act - ChatTool result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.FunctionParameters.ToString())!; - - // Assert - Assert.NotNull(pd.properties); - Assert.Single(pd.properties); - Assert.Equal( - JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")), - JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); - } - -#pragma warning disable CA1812 // uninstantiated internal class - private sealed class ParametersData - { - public string? type { get; set; } - public string[]? required { get; set; } - public Dictionary? properties { get; set; } - } -#pragma warning restore CA1812 -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs deleted file mode 100644 index 67cd371dfe23..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ /dev/null @@ -1,256 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Linq; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -#pragma warning disable CA1812 // Uninstantiated internal types - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.FunctionCalling; - -public sealed class KernelFunctionMetadataExtensionsTests -{ - [Fact] - public void ItCanConvertToAzureOpenAIFunctionNoParameters() - { - // Arrange - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToAzureOpenAIFunction(); - - // Assert - Assert.Equal(sut.Name, result.FunctionName); - Assert.Equal(sut.PluginName, result.PluginName); - Assert.Equal(sut.Description, result.Description); - Assert.Equal($"{sut.PluginName}-{sut.Name}", result.FullyQualifiedName); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToAzureOpenAIFunctionNoPluginName() - { - // Arrange - var sut = new KernelFunctionMetadata("foo") - { - PluginName = string.Empty, - Description = "baz", - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToAzureOpenAIFunction(); - - // Assert - Assert.Equal(sut.Name, result.FunctionName); - Assert.Equal(sut.PluginName, result.PluginName); - Assert.Equal(sut.Description, result.Description); - Assert.Equal(sut.Name, result.FullyQualifiedName); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ItCanConvertToAzureOpenAIFunctionWithParameter(bool withSchema) - { - // Arrange - var param1 = new KernelParameterMetadata("param1") - { - Description = "This is param1", - DefaultValue = "1", - ParameterType = typeof(int), - IsRequired = false, - Schema = withSchema ? KernelJsonSchema.Parse("""{"type":"integer"}""") : null, - }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToAzureOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal("This is param1 (default value: 1)", outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - Assert.NotNull(outputParam.Schema); - Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToAzureOpenAIFunctionWithParameterNoType() - { - // Arrange - var param1 = new KernelParameterMetadata("param1") { Description = "This is param1" }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToAzureOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal(param1.Description, outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToAzureOpenAIFunctionWithNoReturnParameterType() - { - // Arrange - var param1 = new KernelParameterMetadata("param1") - { - Description = "This is param1", - ParameterType = typeof(int), - }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - }; - - // Act - var result = sut.ToAzureOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal(param1.Description, outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - Assert.NotNull(outputParam.Schema); - Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); - } - - [Fact] - public void ItCanCreateValidAzureOpenAIFunctionManualForPlugin() - { - // Arrange - var kernel = new Kernel(); - kernel.Plugins.AddFromType("MyPlugin"); - - var functionMetadata = kernel.Plugins["MyPlugin"].First().Metadata; - - var sut = functionMetadata.ToAzureOpenAIFunction(); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.NotNull(result); - Assert.Equal( - """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", - result.FunctionParameters.ToString() - ); - } - - [Fact] - public void ItCanCreateValidAzureOpenAIFunctionManualForPrompt() - { - // Arrange - var promptTemplateConfig = new PromptTemplateConfig("Hello AI") - { - Description = "My sample function." - }; - promptTemplateConfig.InputVariables.Add(new InputVariable - { - Name = "parameter1", - Description = "String parameter", - JsonSchema = """{"type":"string","description":"String parameter"}""" - }); - promptTemplateConfig.InputVariables.Add(new InputVariable - { - Name = "parameter2", - Description = "Enum parameter", - JsonSchema = """{"enum":["Value1","Value2"],"description":"Enum parameter"}""" - }); - var function = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig); - var functionMetadata = function.Metadata; - var sut = functionMetadata.ToAzureOpenAIFunction(); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.NotNull(result); - Assert.Equal( - """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", - result.FunctionParameters.ToString() - ); - } - - private enum MyEnum - { - Value1, - Value2 - } - - private sealed class MyPlugin - { - [KernelFunction, Description("My sample function.")] - public string MyFunction( - [Description("String parameter")] string parameter1, - [Description("Enum parameter")] MyEnum parameter2, - [Description("DateTime parameter")] DateTime parameter3 - ) - { - return "return"; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs index 13e09bd39e71..2e639434e951 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs @@ -17,6 +17,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Moq; using OpenAI.Chat; @@ -237,7 +238,7 @@ public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(obje [Theory] [MemberData(nameof(ToolCallBehaviors))] - public async Task GetChatMessageContentsWorksCorrectlyAsync(AzureOpenAIToolCallBehavior behavior) + public async Task GetChatMessageContentsWorksCorrectlyAsync(ToolCallBehavior behavior) { // Arrange var kernel = Kernel.CreateBuilder().Build(); @@ -288,7 +289,7 @@ public async Task GetChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -324,7 +325,7 @@ public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttempt kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); @@ -356,12 +357,12 @@ public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() }, "GetCurrentWeather"); var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - var openAIFunction = plugin.GetFunctionsMetadata().First().ToAzureOpenAIFunction(); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(AzureOpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; @@ -458,7 +459,7 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_multiple_function_calls_test_response.txt") }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; @@ -502,7 +503,7 @@ public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvo kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var responses = new List(); @@ -536,12 +537,12 @@ public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() }, "GetCurrentWeather"); var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - var openAIFunction = plugin.GetFunctionsMetadata().First().ToAzureOpenAIFunction(); + var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); kernel.Plugins.Add(plugin); var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_single_function_call_test_response.txt") }; using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = AzureOpenAITestHelper.GetTestResponseAsStream("chat_completion_streaming_test_response.txt") }; @@ -700,7 +701,7 @@ public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfT var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Fake prompt"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; // Act var result = await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -770,7 +771,7 @@ public async Task FunctionCallsShouldBeReturnedToLLMAsync() new ChatMessageContent(AuthorRole.Assistant, items) ]; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -829,7 +830,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsyn ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -874,7 +875,7 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage ]) }; - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; // Act await sut.GetChatMessageContentAsync(chatHistory, settings); @@ -905,10 +906,10 @@ public void Dispose() this._messageHandlerStub.Dispose(); } - public static TheoryData ToolCallBehaviors => new() + public static TheoryData ToolCallBehaviors => new() { - AzureOpenAIToolCallBehavior.EnableKernelFunctions, - AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions + ToolCallBehavior.EnableKernelFunctions, + ToolCallBehavior.AutoInvokeKernelFunctions }; public static TheoryData ResponseFormats => new() diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs deleted file mode 100644 index e9dbd224b2a0..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/AzureOpenAIToolCallBehavior.cs +++ /dev/null @@ -1,279 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using OpenAI.Chat; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// Represents a behavior for Azure OpenAI tool calls. -public abstract class AzureOpenAIToolCallBehavior -{ - // NOTE: Right now, the only tools that are available are for function calling. In the future, - // this class can be extended to support additional kinds of tools, including composite ones: - // the OpenAIPromptExecutionSettings has a single ToolCallBehavior property, but we could - // expose a `public static ToolCallBehavior Composite(params ToolCallBehavior[] behaviors)` - // or the like to allow multiple distinct tools to be provided, should that be appropriate. - // We can also consider additional forms of tools, such as ones that dynamically examine - // the Kernel, KernelArguments, etc., and dynamically contribute tools to the ChatCompletionsOptions. - - /// - /// The default maximum number of tool-call auto-invokes that can be made in a single request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled (e.g. will behave like )). - /// This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. It is currently hardcoded, but in the future it could - /// be made configurable by the developer. Other configuration is also possible in the future, - /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure - /// to find the requested function, failure to invoke the function, etc.), with behaviors for - /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call - /// support, where the model can request multiple tools in a single response, it is significantly - /// less likely that this limit is reached, as most of the time only a single request is needed. - /// - private const int DefaultMaximumAutoInvokeAttempts = 128; - - /// - /// Gets an instance that will provide all of the 's plugins' function information. - /// Function call requests from the model will be propagated back to the caller. - /// - /// - /// If no is available, no function information will be provided to the model. - /// - public static AzureOpenAIToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); - - /// - /// Gets an instance that will both provide all of the 's plugins' function information - /// to the model and attempt to automatically handle any function call requests. - /// - /// - /// When successful, tool call requests from the model become an implementation detail, with the service - /// handling invoking any requested functions and supplying the results back to the model. - /// If no is available, no function information will be provided to the model. - /// - public static AzureOpenAIToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); - - /// Gets an instance that will provide the specified list of functions to the model. - /// The functions that should be made available to the model. - /// true to attempt to automatically handle function call requests; otherwise, false. - /// - /// The that may be set into - /// to indicate that the specified functions should be made available to the model. - /// - public static AzureOpenAIToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) - { - Verify.NotNull(functions); - return new EnabledFunctions(functions, autoInvoke); - } - - /// Gets an instance that will request the model to use the specified function. - /// The function the model should request to use. - /// true to attempt to automatically handle function call requests; otherwise, false. - /// - /// The that may be set into - /// to indicate that the specified function should be requested by the model. - /// - public static AzureOpenAIToolCallBehavior RequireFunction(AzureOpenAIFunction function, bool autoInvoke = false) - { - Verify.NotNull(function); - return new RequiredFunction(function, autoInvoke); - } - - /// Initializes the instance; prevents external instantiation. - private AzureOpenAIToolCallBehavior(bool autoInvoke) - { - this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; - } - - /// - /// Options to control tool call result serialization behavior. - /// - [Obsolete("This property is deprecated in favor of Kernel.SerializerOptions that will be introduced in one of the following releases.")] - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } - - /// Gets how many requests are part of a single interaction should include this tool in the request. - /// - /// This should be greater than or equal to . It defaults to . - /// Once this limit is reached, the tools will no longer be included in subsequent retries as part of the operation, e.g. - /// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result - /// will not include the tools for further use. - /// - internal virtual int MaximumUseAttempts => int.MaxValue; - - /// Gets how many tool call request/response roundtrips are supported with auto-invocation. - /// - /// To disable auto invocation, this can be set to 0. - /// - internal int MaximumAutoInvokeAttempts { get; } - - /// - /// Gets whether validation against a specified list is required before allowing the model to request a function from the kernel. - /// - /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. - internal virtual bool AllowAnyRequestedKernelFunction => false; - - /// Returns list of available tools and the way model should use them. - /// The used for the operation. This can be queried to determine what tools to return. - internal abstract (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel); - - /// - /// Represents a that will provide to the model all available functions from a - /// provided by the client. Setting this will have no effect if no is provided. - /// - internal sealed class KernelFunctions : AzureOpenAIToolCallBehavior - { - internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } - - public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - - internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) - { - ChatToolChoice? choice = null; - List? tools = null; - - // If no kernel is provided, we don't have any tools to provide. - if (kernel is not null) - { - // Provide all functions from the kernel. - IList functions = kernel.Plugins.GetFunctionsMetadata(); - if (functions.Count > 0) - { - choice = ChatToolChoice.Auto; - tools = []; - for (int i = 0; i < functions.Count; i++) - { - tools.Add(functions[i].ToAzureOpenAIFunction().ToFunctionDefinition()); - } - } - } - - return (tools, choice); - } - - internal override bool AllowAnyRequestedKernelFunction => true; - } - - /// - /// Represents a that provides a specified list of functions to the model. - /// - internal sealed class EnabledFunctions : AzureOpenAIToolCallBehavior - { - private readonly AzureOpenAIFunction[] _openAIFunctions; - private readonly ChatTool[] _functions; - - public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) - { - this._openAIFunctions = functions.ToArray(); - - var defs = new ChatTool[this._openAIFunctions.Length]; - for (int i = 0; i < defs.Length; i++) - { - defs[i] = this._openAIFunctions[i].ToFunctionDefinition(); - } - this._functions = defs; - } - - public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.FunctionName))}"; - - internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) - { - ChatToolChoice? choice = null; - List? tools = null; - - AzureOpenAIFunction[] openAIFunctions = this._openAIFunctions; - ChatTool[] functions = this._functions; - Debug.Assert(openAIFunctions.Length == functions.Length); - - if (openAIFunctions.Length > 0) - { - bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) - { - throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); - } - - choice = ChatToolChoice.Auto; - tools = []; - for (int i = 0; i < openAIFunctions.Length; i++) - { - // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. - if (autoInvoke) - { - Debug.Assert(kernel is not null); - AzureOpenAIFunction f = openAIFunctions[i]; - if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) - { - throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); - } - } - - // Add the function. - tools.Add(functions[i]); - } - } - - return (tools, choice); - } - } - - /// Represents a that requests the model use a specific function. - internal sealed class RequiredFunction : AzureOpenAIToolCallBehavior - { - private readonly AzureOpenAIFunction _function; - private readonly ChatTool _tool; - private readonly ChatToolChoice _choice; - - public RequiredFunction(AzureOpenAIFunction function, bool autoInvoke) : base(autoInvoke) - { - this._function = function; - this._tool = function.ToFunctionDefinition(); - this._choice = new ChatToolChoice(this._tool); - } - - public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.FunctionName}"; - - internal override (IList? Tools, ChatToolChoice? Choice) ConfigureOptions(Kernel? kernel) - { - bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) - { - throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); - } - - // Make sure that if auto-invocation is specified, the required function can be found in the kernel. - if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) - { - throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); - } - - return ([this._tool], this._choice); - } - - /// Gets how many requests are part of a single interaction should include this tool in the request. - /// - /// Unlike and , this must use 1 as the maximum - /// use attempts. Otherwise, every call back to the model _requires_ it to invoke the function (as opposed - /// to allows it), which means we end up doing the same work over and over and over until the maximum is reached. - /// Thus for "requires", we must send the tool information only once. - /// - internal override int MaximumUseAttempts => 1; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs deleted file mode 100644 index 5d49fdf91b46..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/ChatHistoryExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace Microsoft.SemanticKernel; - -/// -/// Chat history extensions. -/// -public static class ChatHistoryExtensions -{ - /// - /// Add a message to the chat history at the end of the streamed message - /// - /// Target chat history - /// list of streaming message contents - /// Returns the original streaming results with some message processing - [Experimental("SKEXP0010")] - public static async IAsyncEnumerable AddStreamingMessageAsync(this ChatHistory chatHistory, IAsyncEnumerable streamingMessageContents) - { - List messageContents = []; - - // Stream the response. - StringBuilder? contentBuilder = null; - Dictionary? toolCallIdsByIndex = null; - Dictionary? functionNamesByIndex = null; - Dictionary? functionArgumentBuildersByIndex = null; - Dictionary? metadata = null; - AuthorRole? streamedRole = null; - string? streamedName = null; - - await foreach (var chatMessage in streamingMessageContents.ConfigureAwait(false)) - { - metadata ??= (Dictionary?)chatMessage.Metadata; - - if (chatMessage.Content is { Length: > 0 } contentUpdate) - { - (contentBuilder ??= new()).Append(contentUpdate); - } - - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - - // Is always expected to have at least one chunk with the role provided from a streaming message - streamedRole ??= chatMessage.Role; - streamedName ??= chatMessage.AuthorName; - - messageContents.Add(chatMessage); - yield return chatMessage; - } - - if (messageContents.Count != 0) - { - var role = streamedRole ?? AuthorRole.Assistant; - - chatHistory.Add( - new AzureOpenAIChatMessageContent( - role, - contentBuilder?.ToString() ?? string.Empty, - messageContents[0].ModelId!, - AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), - metadata) - { AuthorName = streamedName }); - } - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 35c31788610d..ec2bb48623c3 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -12,8 +12,6 @@ - - @@ -31,5 +29,6 @@ + \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs index 8112d2c7dee4..aa075a866a10 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; using OpenAI.Chat; using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; @@ -74,16 +75,16 @@ private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList /// /// Retrieve the resulting function from the chat result. /// - /// The , or null if no function was returned by the model. - public IReadOnlyList GetFunctionToolCalls() + /// The , or null if no function was returned by the model. + public IReadOnlyList GetFunctionToolCalls() { - List? functionToolCallList = null; + List? functionToolCallList = null; foreach (var toolCall in this.ToolCalls) { if (toolCall.Kind == ChatToolCallKind.Function) { - (functionToolCallList ??= []).Add(new AzureOpenAIFunctionToolCall(toolCall)); + (functionToolCallList ??= []).Add(new OpenAIFunctionToolCall(toolCall)); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs deleted file mode 100644 index 0089b6c29041..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunction.cs +++ /dev/null @@ -1,178 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using OpenAI.Chat; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Represents a function parameter that can be passed to an AzureOpenAI function tool call. -/// -public sealed class AzureOpenAIFunctionParameter -{ - internal AzureOpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) - { - this.Name = name ?? string.Empty; - this.Description = description ?? string.Empty; - this.IsRequired = isRequired; - this.ParameterType = parameterType; - this.Schema = schema; - } - - /// Gets the name of the parameter. - public string Name { get; } - - /// Gets a description of the parameter. - public string Description { get; } - - /// Gets whether the parameter is required vs optional. - public bool IsRequired { get; } - - /// Gets the of the parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function return parameter that can be returned by a tool call to AzureOpenAI. -/// -public sealed class AzureOpenAIFunctionReturnParameter -{ - internal AzureOpenAIFunctionReturnParameter(string? description, Type? parameterType, KernelJsonSchema? schema) - { - this.Description = description ?? string.Empty; - this.Schema = schema; - this.ParameterType = parameterType; - } - - /// Gets a description of the return parameter. - public string Description { get; } - - /// Gets the of the return parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the return parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function that can be passed to the AzureOpenAI API -/// -public sealed class AzureOpenAIFunction -{ - /// - /// Cached storing the JSON for a function with no parameters. - /// - /// - /// This is an optimization to avoid serializing the same JSON Schema over and over again - /// for this relatively common case. - /// - private static readonly BinaryData s_zeroFunctionParametersSchema = new("""{"type":"object","required":[],"properties":{}}"""); - /// - /// Cached schema for a descriptionless string. - /// - private static readonly KernelJsonSchema s_stringNoDescriptionSchema = KernelJsonSchema.Parse("""{"type":"string"}"""); - - /// Initializes the OpenAIFunction. - internal AzureOpenAIFunction( - string? pluginName, - string functionName, - string? description, - IReadOnlyList? parameters, - AzureOpenAIFunctionReturnParameter? returnParameter) - { - Verify.NotNullOrWhiteSpace(functionName); - - this.PluginName = pluginName; - this.FunctionName = functionName; - this.Description = description; - this.Parameters = parameters; - this.ReturnParameter = returnParameter; - } - - /// Gets the separator used between the plugin name and the function name, if a plugin name is present. - /// This separator was previously _, but has been changed to - to better align to the behavior elsewhere in SK and in response - /// to developers who want to use underscores in their function or plugin names. We plan to make this setting configurable in the future. - public static string NameSeparator { get; set; } = "-"; - - /// Gets the name of the plugin with which the function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , this is - /// the same as . - /// - public string FullyQualifiedName => - string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{NameSeparator}{this.FunctionName}"; - - /// Gets a description of the function. - public string? Description { get; } - - /// Gets a list of parameters to the function, if any. - public IReadOnlyList? Parameters { get; } - - /// Gets the return parameter of the function, if any. - public AzureOpenAIFunctionReturnParameter? ReturnParameter { get; } - - /// - /// Converts the representation to the Azure SDK's - /// representation. - /// - /// A containing all the function information. - public ChatTool ToFunctionDefinition() - { - BinaryData resultParameters = s_zeroFunctionParametersSchema; - - IReadOnlyList? parameters = this.Parameters; - if (parameters is { Count: > 0 }) - { - var properties = new Dictionary(); - var required = new List(); - - for (int i = 0; i < parameters.Count; i++) - { - var parameter = parameters[i]; - properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); - if (parameter.IsRequired) - { - required.Add(parameter.Name); - } - } - - resultParameters = BinaryData.FromObjectAsJson(new - { - type = "object", - required, - properties, - }); - } - - return ChatTool.CreateFunctionTool - ( - functionName: this.FullyQualifiedName, - functionDescription: this.Description, - functionParameters: resultParameters - ); - } - - /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) - private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) - { - // If there's a description, incorporate it. - if (!string.IsNullOrWhiteSpace(description)) - { - return KernelJsonSchemaBuilder.Build(null, typeof(string), description); - } - - // Otherwise, we can use a cached schema for a string with no description. - return s_stringNoDescriptionSchema; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs deleted file mode 100644 index 361c617f31a0..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIFunctionToolCall.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Text.Json; -using OpenAI.Chat; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Represents an AzureOpenAI function tool call with deserialized function name and arguments. -/// -public sealed class AzureOpenAIFunctionToolCall -{ - private string? _fullyQualifiedFunctionName; - - /// Initialize the from a . - internal AzureOpenAIFunctionToolCall(ChatToolCall functionToolCall) - { - Verify.NotNull(functionToolCall); - Verify.NotNull(functionToolCall.FunctionName); - - string fullyQualifiedFunctionName = functionToolCall.FunctionName; - string functionName = fullyQualifiedFunctionName; - string? arguments = functionToolCall.FunctionArguments; - string? pluginName = null; - - int separatorPos = fullyQualifiedFunctionName.IndexOf(AzureOpenAIFunction.NameSeparator, StringComparison.Ordinal); - if (separatorPos >= 0) - { - pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString(); - functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + AzureOpenAIFunction.NameSeparator.Length).Trim().ToString(); - } - - this.Id = functionToolCall.Id; - this._fullyQualifiedFunctionName = fullyQualifiedFunctionName; - this.PluginName = pluginName; - this.FunctionName = functionName; - if (!string.IsNullOrWhiteSpace(arguments)) - { - this.Arguments = JsonSerializer.Deserialize>(arguments!); - } - } - - /// Gets the ID of the tool call. - public string? Id { get; } - - /// Gets the name of the plugin with which this function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets a name/value collection of the arguments to the function, if any. - public Dictionary? Arguments { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , - /// this is the same as . - /// - public string FullyQualifiedName => - this._fullyQualifiedFunctionName ??= - string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{AzureOpenAIFunction.NameSeparator}{this.FunctionName}"; - - /// - public override string ToString() - { - var sb = new StringBuilder(this.FullyQualifiedName); - - sb.Append('('); - if (this.Arguments is not null) - { - string separator = ""; - foreach (var arg in this.Arguments) - { - sb.Append(separator).Append(arg.Key).Append(':').Append(arg.Value); - separator = ", "; - } - } - sb.Append(')'); - - return sb.ToString(); - } - - /// - /// Tracks tooling updates from streaming responses. - /// - /// The tool call updates to incorporate. - /// Lazily-initialized dictionary mapping indices to IDs. - /// Lazily-initialized dictionary mapping indices to names. - /// Lazily-initialized dictionary mapping indices to arguments. - internal static void TrackStreamingToolingUpdate( - IReadOnlyList? updates, - ref Dictionary? toolCallIdsByIndex, - ref Dictionary? functionNamesByIndex, - ref Dictionary? functionArgumentBuildersByIndex) - { - if (updates is null) - { - // Nothing to track. - return; - } - - foreach (var update in updates) - { - // If we have an ID, ensure the index is being tracked. Even if it's not a function update, - // we want to keep track of it so we can send back an error. - if (update.Id is string id) - { - (toolCallIdsByIndex ??= [])[update.Index] = id; - } - - // Ensure we're tracking the function's name. - if (update.FunctionName is string name) - { - (functionNamesByIndex ??= [])[update.Index] = name; - } - - // Ensure we're tracking the function's arguments. - if (update.FunctionArgumentsUpdate is string argumentsUpdate) - { - if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(update.Index, out StringBuilder? arguments)) - { - functionArgumentBuildersByIndex[update.Index] = arguments = new(); - } - - arguments.Append(argumentsUpdate); - } - } - } - - /// - /// Converts the data built up by into an array of s. - /// - /// Dictionary mapping indices to IDs. - /// Dictionary mapping indices to names. - /// Dictionary mapping indices to arguments. - internal static ChatToolCall[] ConvertToolCallUpdatesToFunctionToolCalls( - ref Dictionary? toolCallIdsByIndex, - ref Dictionary? functionNamesByIndex, - ref Dictionary? functionArgumentBuildersByIndex) - { - ChatToolCall[] toolCalls = []; - if (toolCallIdsByIndex is { Count: > 0 }) - { - toolCalls = new ChatToolCall[toolCallIdsByIndex.Count]; - - int i = 0; - foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) - { - string? functionName = null; - StringBuilder? functionArguments = null; - - functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); - functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); - - toolCalls[i] = ChatToolCall.CreateFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); - i++; - } - - Debug.Assert(i == toolCalls.Length); - } - - return toolCalls; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs deleted file mode 100644 index 30f796f82ae0..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIKernelFunctionMetadataExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Extensions for specific to the AzureOpenAI connector. -/// -public static class AzureOpenAIKernelFunctionMetadataExtensions -{ - /// - /// Convert a to an . - /// - /// The object to convert. - /// An object. - public static AzureOpenAIFunction ToAzureOpenAIFunction(this KernelFunctionMetadata metadata) - { - IReadOnlyList metadataParams = metadata.Parameters; - - var openAIParams = new AzureOpenAIFunctionParameter[metadataParams.Count]; - for (int i = 0; i < openAIParams.Length; i++) - { - var param = metadataParams[i]; - - openAIParams[i] = new AzureOpenAIFunctionParameter( - param.Name, - GetDescription(param), - param.IsRequired, - param.ParameterType, - param.Schema); - } - - return new AzureOpenAIFunction( - metadata.PluginName, - metadata.Name, - metadata.Description, - openAIParams, - new AzureOpenAIFunctionReturnParameter( - metadata.ReturnParameter.Description, - metadata.ReturnParameter.ParameterType, - metadata.ReturnParameter.Schema)); - - static string GetDescription(KernelParameterMetadata param) - { - if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) - { - return $"{param.Description} (default value: {stringValue})"; - } - - return param.Description; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs deleted file mode 100644 index c903127089dd..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIPluginCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using OpenAI.Chat; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Extension methods for . -/// -public static class AzureOpenAIPluginCollectionExtensions -{ - /// - /// Given an object, tries to retrieve the corresponding and populate with its parameters. - /// - /// The plugins. - /// The object. - /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, - /// When this method returns, the arguments for the function; otherwise, - /// if the function was found; otherwise, . - public static bool TryGetFunctionAndArguments( - this IReadOnlyKernelPluginCollection plugins, - ChatToolCall functionToolCall, - [NotNullWhen(true)] out KernelFunction? function, - out KernelArguments? arguments) => - plugins.TryGetFunctionAndArguments(new AzureOpenAIFunctionToolCall(functionToolCall), out function, out arguments); - - /// - /// Given an object, tries to retrieve the corresponding and populate with its parameters. - /// - /// The plugins. - /// The object. - /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, - /// When this method returns, the arguments for the function; otherwise, - /// if the function was found; otherwise, . - public static bool TryGetFunctionAndArguments( - this IReadOnlyKernelPluginCollection plugins, - AzureOpenAIFunctionToolCall functionToolCall, - [NotNullWhen(true)] out KernelFunction? function, - out KernelArguments? arguments) - { - if (plugins.TryGetFunction(functionToolCall.PluginName, functionToolCall.FunctionName, out function)) - { - // Add parameters to arguments - arguments = null; - if (functionToolCall.Arguments is not null) - { - arguments = []; - foreach (var parameter in functionToolCall.Arguments) - { - arguments[parameter.Key] = parameter.Value?.ToString(); - } - } - - return true; - } - - // Function not found in collection - arguments = null; - return false; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index 99587b8e5a00..81614fb24419 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -14,6 +14,7 @@ using Azure.AI.OpenAI; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Diagnostics; using OpenAI.Chat; using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; @@ -233,10 +234,10 @@ internal async Task> GetChatMessageContentsAsy } // Parse the function call arguments. - AzureOpenAIFunctionToolCall? azureOpenAIFunctionToolCall; + OpenAIFunctionToolCall? openAIFunctionToolCall; try { - azureOpenAIFunctionToolCall = new(functionToolCall); + openAIFunctionToolCall = new(functionToolCall); } catch (JsonException) { @@ -248,14 +249,14 @@ internal async Task> GetChatMessageContentsAsy // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, azureOpenAIFunctionToolCall)) + !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(azureOpenAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) + if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; @@ -418,7 +419,7 @@ internal async IAsyncEnumerable GetStrea } } - AzureOpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); + OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentName, metadata); @@ -446,7 +447,7 @@ internal async IAsyncEnumerable GetStrea } // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = AzureOpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( + toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); // Translate all entries into FunctionCallContent instances for diagnostics purposes. @@ -500,7 +501,7 @@ internal async IAsyncEnumerable GetStrea } // Parse the function call arguments. - AzureOpenAIFunctionToolCall? openAIFunctionToolCall; + OpenAIFunctionToolCall? openAIFunctionToolCall; try { openAIFunctionToolCall = new(toolCall); @@ -622,7 +623,7 @@ internal async Task> GetChatAsTextContentsAsync( } /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionOptions options, AzureOpenAIFunctionToolCall ftc) + private static bool IsRequestableTool(ChatCompletionOptions options, OpenAIFunctionToolCall ftc) { IList tools = options.Tools; for (int i = 0; i < tools.Count; i++) @@ -753,7 +754,7 @@ private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string throw new NotImplementedException($"Role {chatRole} is not implemented"); } - private static List CreateRequestMessages(ChatMessageContent message, AzureOpenAIToolCallBehavior? toolCallBehavior) + private static List CreateRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) { @@ -872,7 +873,7 @@ private static List CreateRequestMessages(ChatMessageContent messag var argument = JsonSerializer.Serialize(callRequest.Arguments); - toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, AzureOpenAIFunction.NameSeparator), argument ?? string.Empty)); + toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; @@ -975,7 +976,7 @@ private List GetFunctionCallContents(IEnumerable chatMessages, ChatHisto { // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(toolCall.FunctionName, AzureOpenAIFunction.NameSeparator); + var functionName = FunctionName.Parse(toolCall.FunctionName, OpenAIFunction.NameSeparator); message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); } @@ -1059,7 +1060,7 @@ private void LogUsage(ChatTokenUsage usage) /// The result of the function call. /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, AzureOpenAIToolCallBehavior? toolCallBehavior) + private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) { if (functionResult is string stringResult) { diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs index 22141ee8aee0..289a7405f371 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs @@ -8,6 +8,7 @@ using System.Text.Json.Serialization; using Azure.AI.OpenAI.Chat; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Text; using OpenAI.Chat; @@ -191,18 +192,18 @@ public IDictionary? TokenSelectionBiases /// To disable all tool calling, set the property to null (the default). /// /// To request that the model use a specific function, set the property to an instance returned - /// from . + /// from . /// /// /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with + /// instance returned from , called with /// a list of the functions available. /// /// /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply + /// set the property to if the client should simply /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically + /// if the client should attempt to automatically /// invoke the function and send the result back to the service. /// /// @@ -213,7 +214,7 @@ public IDictionary? TokenSelectionBiases /// the function, and sending back the result. The intermediate messages will be retained in the /// if an instance was provided. /// - public AzureOpenAIToolCallBehavior? ToolCallBehavior + public ToolCallBehavior? ToolCallBehavior { get => this._toolCallBehavior; @@ -403,7 +404,7 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P private long? _seed; private object? _responseFormat; private IDictionary? _tokenSelectionBiases; - private AzureOpenAIToolCallBehavior? _toolCallBehavior; + private ToolCallBehavior? _toolCallBehavior; private string? _user; private string? _chatSystemPrompt; private bool? _logprobs; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj index 668b26204f88..d3466a87a2ea 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj @@ -23,6 +23,7 @@ + diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index f90102d62834..2b75fc3458b5 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -10,6 +10,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; using OpenAI.Chat; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; @@ -33,7 +34,7 @@ public async Task CanAutoInvokeKernelFunctionsAsync() var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); kernel.FunctionInvocationFilters.Add(filter); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); @@ -59,7 +60,7 @@ public async Task CanAutoInvokeKernelFunctionsStreamingAsync() var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); kernel.FunctionInvocationFilters.Add(filter); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var stringBuilder = new StringBuilder(); @@ -81,7 +82,7 @@ public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); @@ -97,7 +98,7 @@ public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); @@ -113,7 +114,7 @@ public async Task CanAutoInvokeKernelFunctionsWithEnumTypeParametersAsync() // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); @@ -139,7 +140,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptAsync() "Delivers up-to-date news content.", [promptFunction])); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); @@ -165,7 +166,7 @@ public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() "Delivers up-to-date news content.", [promptFunction])); - AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + AzureOpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); @@ -193,7 +194,7 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -240,7 +241,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -281,7 +282,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc chatHistory.AddSystemMessage("Add the \"Error\" keyword to the response, if you are unable to answer a question or an error has happen."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var completionService = kernel.GetRequiredService(); @@ -325,7 +326,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var completionService = kernel.GetRequiredService(); @@ -373,7 +374,7 @@ public async Task ItFailsIfNoFunctionResultProvidedAsync() var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var completionService = kernel.GetRequiredService(); @@ -397,7 +398,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -457,7 +458,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManual // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -516,7 +517,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu var chatHistory = new ChatHistory(); chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -581,7 +582,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var sut = kernel.GetRequiredService(); @@ -639,7 +640,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu // Arrange var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); - var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.EnableKernelFunctions }; + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; var sut = kernel.GetRequiredService(); From 3851576c13dd08531adb5ddd5e5e1e06d0639a03 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:31:14 +0100 Subject: [PATCH 086/226] .Net: Remove unnecessary azure chat message content classes (#7259) ### Motivation and Context Two new classes, `AzureOpenAIChatMessageContent` and `AzureOpenAIStreamingChatMessageContent`, were introduced during the migration of AI connectors to the Azure.AI.OpenAI SDK v2 to speed up the migration. However, it appeared that these classes are identical to their non-Azure counterparts, `OpenAIChatMessageContent` and `OpenAIStreamingChatMessageContent`. As a result, considering that they do not provide any additional functionality over the OpenAI equivalents at the moment, it was decided to drop them for now and use the OpenAI ones instead. These classes might be added later if there is a need to communicate Azure-specific details. ### Description This PR removes the `AzureOpenAIChatMessageContent` and `AzureOpenAIStreamingChatMessageContent` classes and refactors the `ClientCore` to use the non-Azure equivalents. --- .../AzureOpenAIChatMessageContentTests.cs | 117 --------------- .../Core/AzureOpenAIChatMessageContent.cs | 135 ------------------ .../AzureOpenAIStreamingChatMessageContent.cs | 104 -------------- .../Core/ClientCore.ChatCompletion.cs | 26 ++-- ...enAIChatCompletion_FunctionCallingTests.cs | 6 +- 5 files changed, 16 insertions(+), 372 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs deleted file mode 100644 index 49832b221978..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Core/AzureOpenAIChatMessageContentTests.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using OpenAI.Chat; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Core; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIChatMessageContentTests -{ - [Fact] - public void ConstructorsWorkCorrectly() - { - // Arrange - List toolCalls = [ChatToolCall.CreateFunctionToolCall("id", "name", "args")]; - - // Act - var content1 = new AzureOpenAIChatMessageContent(ChatMessageRole.User, "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; - var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); - - // Assert - this.AssertChatMessageContent(AuthorRole.User, "content1", "model-id1", toolCalls, content1, "Fred"); - this.AssertChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, content2); - } - - [Fact] - public void GetOpenAIFunctionToolCallsReturnsCorrectList() - { - // Arrange - List toolCalls = [ - ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), - ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; - - var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); - var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); - - // Act - var actualToolCalls1 = content1.GetFunctionToolCalls(); - var actualToolCalls2 = content2.GetFunctionToolCalls(); - - // Assert - Assert.Equal(2, actualToolCalls1.Count); - Assert.Equal("id1", actualToolCalls1[0].Id); - Assert.Equal("id2", actualToolCalls1[1].Id); - - Assert.Empty(actualToolCalls2); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) - { - // Arrange - IReadOnlyDictionary metadata = readOnlyMetadata ? - new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : - new Dictionary { { "key", "value" } }; - - List toolCalls = [ - ChatToolCall.CreateFunctionToolCall("id1", "name", string.Empty), - ChatToolCall.CreateFunctionToolCall("id2", "name", string.Empty)]; - - // Act - var content1 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); - var content2 = new AzureOpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, metadata); - - // Assert - Assert.NotNull(content1.Metadata); - Assert.Single(content1.Metadata); - - Assert.NotNull(content2.Metadata); - Assert.Equal(2, content2.Metadata.Count); - Assert.Equal("value", content2.Metadata["key"]); - - Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); - - var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; - Assert.NotNull(actualToolCalls); - - Assert.Equal(2, actualToolCalls.Count); - Assert.Equal("id1", actualToolCalls[0].Id); - Assert.Equal("id2", actualToolCalls[1].Id); - } - - private void AssertChatMessageContent( - AuthorRole expectedRole, - string expectedContent, - string expectedModelId, - IReadOnlyList expectedToolCalls, - AzureOpenAIChatMessageContent actualContent, - string? expectedName = null) - { - Assert.Equal(expectedRole, actualContent.Role); - Assert.Equal(expectedContent, actualContent.Content); - Assert.Equal(expectedName, actualContent.AuthorName); - Assert.Equal(expectedModelId, actualContent.ModelId); - Assert.Same(expectedToolCalls, actualContent.ToolCalls); - } - - private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> - { - public TValue this[TKey key] => dictionary[key]; - public IEnumerable Keys => dictionary.Keys; - public IEnumerable Values => dictionary.Values; - public int Count => dictionary.Count; - public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); - public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => dictionary.TryGetValue(key, out value); - IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs deleted file mode 100644 index aa075a866a10..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIChatMessageContent.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using OpenAI.Chat; -using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// AzureOpenAI specialized chat message content -/// -public sealed class AzureOpenAIChatMessageContent : ChatMessageContent -{ - /// - /// Gets the metadata key for the tool id. - /// - public static string ToolIdProperty => "ChatCompletionsToolCall.Id"; - - /// - /// Gets the metadata key for the list of . - /// - internal static string FunctionToolCallsProperty => "ChatResponseMessage.FunctionToolCalls"; - - /// - /// Initializes a new instance of the class. - /// - internal AzureOpenAIChatMessageContent(OpenAIChatCompletion completion, string modelId, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(completion.Role.ToString()), CreateContentItems(completion.Content), modelId, completion, System.Text.Encoding.UTF8, CreateMetadataDictionary(completion.ToolCalls, metadata)) - { - this.ToolCalls = completion.ToolCalls; - } - - /// - /// Initializes a new instance of the class. - /// - internal AzureOpenAIChatMessageContent(ChatMessageRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) - { - this.ToolCalls = toolCalls; - } - - /// - /// Initializes a new instance of the class. - /// - internal AzureOpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) - { - this.ToolCalls = toolCalls; - } - - private static ChatMessageContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) - { - ChatMessageContentItemCollection collection = []; - - foreach (var part in contentUpdate) - { - // We only support text content for now. - if (part.Kind == ChatMessageContentPartKind.Text) - { - collection.Add(new TextContent(part.Text)); - } - } - - return collection; - } - - /// - /// A list of the tools called by the model. - /// - public IReadOnlyList ToolCalls { get; } - - /// - /// Retrieve the resulting function from the chat result. - /// - /// The , or null if no function was returned by the model. - public IReadOnlyList GetFunctionToolCalls() - { - List? functionToolCallList = null; - - foreach (var toolCall in this.ToolCalls) - { - if (toolCall.Kind == ChatToolCallKind.Function) - { - (functionToolCallList ??= []).Add(new OpenAIFunctionToolCall(toolCall)); - } - } - - if (functionToolCallList is not null) - { - return functionToolCallList; - } - - return []; - } - - private static IReadOnlyDictionary? CreateMetadataDictionary( - IReadOnlyList toolCalls, - IReadOnlyDictionary? original) - { - // We only need to augment the metadata if there are any tool calls. - if (toolCalls.Count > 0) - { - Dictionary newDictionary; - if (original is null) - { - // There's no existing metadata to clone; just allocate a new dictionary. - newDictionary = new Dictionary(1); - } - else if (original is IDictionary origIDictionary) - { - // Efficiently clone the old dictionary to a new one. - newDictionary = new Dictionary(origIDictionary); - } - else - { - // There's metadata to clone but we have to do so one item at a time. - newDictionary = new Dictionary(original.Count + 1); - foreach (var kvp in original) - { - newDictionary[kvp.Key] = kvp.Value; - } - } - - // Add the additional entry. - newDictionary.Add(FunctionToolCallsProperty, toolCalls.Where(ctc => ctc.Kind == ChatToolCallKind.Function).ToList()); - - return newDictionary; - } - - return original; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs deleted file mode 100644 index fce885482899..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureOpenAIStreamingChatMessageContent.cs +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; -using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI.Chat; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Azure OpenAI specialized streaming chat message content. -/// -/// -/// Represents a chat message content chunk that was streamed from the remote model. -/// -public sealed class AzureOpenAIStreamingChatMessageContent : StreamingChatMessageContent -{ - /// - /// The reason why the completion finished. - /// - public ChatFinishReason? FinishReason { get; set; } - - /// - /// Create a new instance of the class. - /// - /// Internal Azure SDK Message update representation - /// Index of the choice - /// The model ID used to generate the content - /// Additional metadata - internal AzureOpenAIStreamingChatMessageContent( - StreamingChatCompletionUpdate chatUpdate, - int choiceIndex, - string modelId, - IReadOnlyDictionary? metadata = null) - : base( - chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, - null, - chatUpdate, - choiceIndex, - modelId, - Encoding.UTF8, - metadata) - { - this.ToolCallUpdates = chatUpdate.ToolCallUpdates; - this.FinishReason = chatUpdate.FinishReason; - this.Items = CreateContentItems(chatUpdate.ContentUpdate); - } - - /// - /// Create a new instance of the class. - /// - /// Author role of the message - /// Content of the message - /// Tool call updates - /// Completion finish reason - /// Index of the choice - /// The model ID used to generate the content - /// Additional metadata - internal AzureOpenAIStreamingChatMessageContent( - AuthorRole? authorRole, - string? content, - IReadOnlyList? toolCallUpdates = null, - ChatFinishReason? completionsFinishReason = null, - int choiceIndex = 0, - string? modelId = null, - IReadOnlyDictionary? metadata = null) - : base( - authorRole, - content, - null, - choiceIndex, - modelId, - Encoding.UTF8, - metadata) - { - this.ToolCallUpdates = toolCallUpdates; - this.FinishReason = completionsFinishReason; - } - - /// Gets any update information in the message about a tool call. - public IReadOnlyList? ToolCallUpdates { get; } - - /// - public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); - - /// - public override string ToString() => this.Content ?? string.Empty; - - private static StreamingKernelContentItemCollection CreateContentItems(IReadOnlyList contentUpdate) - { - StreamingKernelContentItemCollection collection = []; - - foreach (var content in contentUpdate) - { - // We only support text content for now. - if (content.Kind == ChatMessageContentPartKind.Text) - { - collection.Add(new StreamingTextContent(content.Text)); - } - } - - return collection; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index 81614fb24419..2974acc3e993 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -159,7 +159,7 @@ internal async Task> GetChatMessageContentsAsy // Make the request. OpenAIChatCompletion? chatCompletion = null; - AzureOpenAIChatMessageContent chatMessageContent; + OpenAIChatMessageContent chatMessageContent; using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chat, chatExecutionSettings)) { try @@ -323,7 +323,7 @@ internal async Task> GetChatMessageContentsAsy } } - internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( ChatHistory chat, PromptExecutionSettings? executionSettings, Kernel? kernel, @@ -384,7 +384,7 @@ internal async IAsyncEnumerable GetStrea } var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; + List? streamedContents = activity is not null ? [] : null; try { while (true) @@ -422,7 +422,7 @@ internal async IAsyncEnumerable GetStrea OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new AzureOpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentName, metadata); + var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentName, metadata); foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) { @@ -586,7 +586,7 @@ internal async IAsyncEnumerable GetStrea var lastChatMessage = chat.Last(); - yield return new AzureOpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); + yield return new OpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); yield break; } } @@ -765,7 +765,7 @@ private static List CreateRequestMessages(ChatMessageContent messag { // Handling function results represented by the TextContent type. // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) - if (message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && + if (message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && toolId?.ToString() is string toolIdString) { return [new ToolChatMessage(toolIdString, message.Content)]; @@ -825,8 +825,8 @@ private static List CreateRequestMessages(ChatMessageContent messag // Handling function calls supplied via either: // ChatCompletionsToolCall.ToolCalls collection items or // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as AzureOpenAIChatMessageContent)?.ToolCalls; - if (tools is null && message.Metadata?.TryGetValue(AzureOpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) + IEnumerable? tools = (message as OpenAIChatMessageContent)?.ToolCalls; + if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) { tools = toolCallsObject as IEnumerable; if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) @@ -917,18 +917,18 @@ private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private AzureOpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) + private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) { - var message = new AzureOpenAIChatMessageContent(completion, this.DeploymentName, GetChatCompletionMetadata(completion)); + var message = new OpenAIChatMessageContent(completion, this.DeploymentName, GetChatCompletionMetadata(completion)); message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); return message; } - private AzureOpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) + private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) { - var message = new AzureOpenAIChatMessageContent(chatRole, content, this.DeploymentName, toolCalls, metadata) + var message = new OpenAIChatMessageContent(chatRole, content, this.DeploymentName, toolCalls, metadata) { AuthorName = authorName, }; @@ -1009,7 +1009,7 @@ private static void AddResponseMessage(List chatMessages, ChatHisto chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); + var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); if (toolCall.Kind == ChatToolCallKind.Function) { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 2b75fc3458b5..2053208037ad 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -202,7 +202,7 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); // Current way of handling function calls manually using connector specific chat message content class. - var toolCalls = ((AzureOpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); while (toolCalls.Count > 0) { @@ -220,12 +220,12 @@ public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFu chatHistory.Add(new ChatMessageContent( AuthorRole.Tool, content, - metadata: new Dictionary(1) { { AzureOpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); + metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); } // Sending the functions invocation results back to the LLM to get the final response result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - toolCalls = ((AzureOpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); + toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); } // Assert From 4c6b99b618d1a46ff10cf51456e1a5be27626e92 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 16 Jul 2024 09:10:08 +0100 Subject: [PATCH 087/226] .Net: Minimize *prompt execution settings duplication (#7265) ### Motivation and Context While migrating Azure-related AI connectors functionality to the Azure.AI.OpenAI SDK v2, a few duplicates of prompt execution settings for the new connectors were added to speed up the migration process. Having the connectors migrated, it makes sense to review their prompt execution settings classes, remove duplicates, and inherit AzureOpenAI* execution settings from OpenAI* ones to maintain backward compatibility with the existing API and be able to communicate Azure-specific details. ### Description 1. This PR removes `AzureOpenAIAudioToTextExecutionSettings` and `AzureOpenAITextToAudioExecutionSettings` because they are exact copies of the `OpenAIAudioToTextExecutionSettings` and `OpenAITextToAudioExecutionSettings` classes, which can be used instead. Later, when there is a need to supply Azure-specific details via prompt execution settings, the Azure-specific prompt execution settings classes inherited from the OpenAI ones can easily be added. 2. The PR inherits `AzureOpenAIAudioToTextExecutionSettings` from the `OpenAIAudioToTextExecutionSettings` class to preserve backward compatibility and avoid breaking changes. --- ...AzureOpenAIPromptExecutionSettingsTests.cs | 270 -------------- .../AzureOpenAIAudioToTextServiceTests.cs | 13 +- .../AzureOpenAITextToAudioServiceTests.cs | 15 +- ...OpenAIAudioToTextExecutionSettingsTests.cs | 121 ------- ...AzureOpenAIPromptExecutionSettingsTests.cs | 29 +- ...OpenAITextToAudioExecutionSettingsTests.cs | 107 ------ .../Core/ClientCore.AudioToText.cs | 9 +- .../Core/ClientCore.TextToAudio.cs | 5 +- ...AzureOpenAIAudioToTextExecutionSettings.cs | 168 --------- .../AzureOpenAIPromptExecutionSettings.cs | 333 +----------------- ...AzureOpenAITextToAudioExecutionSettings.cs | 130 ------- .../Settings/OpenAIPromptExecutionSettings.cs | 54 +-- .../OpenAITextToAudioExecutionSettings.cs | 2 +- 13 files changed, 92 insertions(+), 1164 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs deleted file mode 100644 index 7b50e36c5587..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/AzureOpenAIPromptExecutionSettingsTests.cs +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests; - -/// -/// Unit tests of AzureOpenAIPromptExecutionSettingsTests -/// -public class AzureOpenAIPromptExecutionSettingsTests -{ - [Fact] - public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() - { - // Arrange - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(1, executionSettings.Temperature); - Assert.Equal(1, executionSettings.TopP); - Assert.Equal(0, executionSettings.FrequencyPenalty); - Assert.Equal(0, executionSettings.PresencePenalty); - Assert.Null(executionSettings.StopSequences); - Assert.Null(executionSettings.TokenSelectionBiases); - Assert.Null(executionSettings.TopLogprobs); - Assert.Null(executionSettings.Logprobs); - Assert.Null(executionSettings.AzureChatDataSource); - Assert.Equal(128, executionSettings.MaxTokens); - } - - [Fact] - public void ItUsesExistingOpenAIExecutionSettings() - { - // Arrange - AzureOpenAIPromptExecutionSettings actualSettings = new() - { - Temperature = 0.7, - TopP = 0.7, - FrequencyPenalty = 0.7, - PresencePenalty = 0.7, - StopSequences = new string[] { "foo", "bar" }, - ChatSystemPrompt = "chat system prompt", - MaxTokens = 128, - Logprobs = true, - TopLogprobs = 5, - TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, - }; - - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(actualSettings, executionSettings); - } - - [Fact] - public void ItCanUseOpenAIExecutionSettings() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() { - { "max_tokens", 1000 }, - { "temperature", 0 } - } - }; - - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(1000, executionSettings.MaxTokens); - Assert.Equal(0, executionSettings.Temperature); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() - { - { "temperature", 0.7 }, - { "top_p", 0.7 }, - { "frequency_penalty", 0.7 }, - { "presence_penalty", 0.7 }, - { "results_per_prompt", 2 }, - { "stop_sequences", new [] { "foo", "bar" } }, - { "chat_system_prompt", "chat system prompt" }, - { "max_tokens", 128 }, - { "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } }, - { "seed", 123456 }, - { "logprobs", true }, - { "top_logprobs", 5 }, - } - }; - - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() - { - { "temperature", "0.7" }, - { "top_p", "0.7" }, - { "frequency_penalty", "0.7" }, - { "presence_penalty", "0.7" }, - { "results_per_prompt", "2" }, - { "stop_sequences", new [] { "foo", "bar" } }, - { "chat_system_prompt", "chat system prompt" }, - { "max_tokens", "128" }, - { "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } }, - { "seed", 123456 }, - { "logprobs", true }, - { "top_logprobs", 5 } - } - }; - - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase() - { - // Arrange - var json = """ - { - "temperature": 0.7, - "top_p": 0.7, - "frequency_penalty": 0.7, - "presence_penalty": 0.7, - "results_per_prompt": 2, - "stop_sequences": [ "foo", "bar" ], - "chat_system_prompt": "chat system prompt", - "token_selection_biases": { "1": 2, "3": 4 }, - "max_tokens": 128, - "seed": 123456, - "logprobs": true, - "top_logprobs": 5 - } - """; - var actualSettings = JsonSerializer.Deserialize(json); - - // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Theory] - [InlineData("", "")] - [InlineData("System prompt", "System prompt")] - public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) - { - // Arrange & Act - var settings = new AzureOpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; - - // Assert - Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); - } - - [Fact] - public void PromptExecutionSettingsCloneWorksAsExpected() - { - // Arrange - string configPayload = """ - { - "max_tokens": 60, - "temperature": 0.5, - "top_p": 0.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0 - } - """; - var executionSettings = JsonSerializer.Deserialize(configPayload); - - // Act - var clone = executionSettings!.Clone(); - - // Assert - Assert.NotNull(clone); - Assert.Equal(executionSettings.ModelId, clone.ModelId); - Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); - } - - [Fact] - public void PromptExecutionSettingsFreezeWorksAsExpected() - { - // Arrange - string configPayload = """ - { - "max_tokens": 60, - "temperature": 0.5, - "top_p": 0.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0, - "stop_sequences": [ "DONE" ], - "token_selection_biases": { "1": 2, "3": 4 } - } - """; - var executionSettings = JsonSerializer.Deserialize(configPayload); - - // Act - executionSettings!.Freeze(); - - // Assert - Assert.True(executionSettings.IsFrozen); - Assert.Throws(() => executionSettings.ModelId = "gpt-4"); - Assert.Throws(() => executionSettings.Temperature = 1); - Assert.Throws(() => executionSettings.TopP = 1); - Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); - Assert.Throws(() => executionSettings.TokenSelectionBiases?.Add(5, 6)); - - executionSettings!.Freeze(); // idempotent - Assert.True(executionSettings.IsFrozen); - } - - [Fact] - public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() - { - // Arrange - var executionSettings = new AzureOpenAIPromptExecutionSettings { StopSequences = [] }; - - // Act -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - var executionSettingsWithData = AzureOpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings); -#pragma warning restore CS0618 - // Assert - Assert.Null(executionSettingsWithData.StopSequences); - } - - private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings executionSettings) - { - Assert.NotNull(executionSettings); - Assert.Equal(0.7, executionSettings.Temperature); - Assert.Equal(0.7, executionSettings.TopP); - Assert.Equal(0.7, executionSettings.FrequencyPenalty); - Assert.Equal(0.7, executionSettings.PresencePenalty); - Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); - Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); - Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); - Assert.Equal(128, executionSettings.MaxTokens); - Assert.Equal(123456, executionSettings.Seed); - Assert.Equal(true, executionSettings.Logprobs); - Assert.Equal(5, executionSettings.TopLogprobs); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs index 46439311ccdc..89642f1345c0 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Services; using Moq; @@ -91,7 +92,7 @@ public void ItThrowsIfDeploymentNameIsNotProvided() [Theory] [MemberData(nameof(ExecutionSettings))] - public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(AzureOpenAIAudioToTextExecutionSettings? settings, Type expectedExceptionType) + public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(OpenAIAudioToTextExecutionSettings? settings, Type expectedExceptionType) { // Arrange var service = new AzureOpenAIAudioToTextService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); @@ -123,7 +124,7 @@ public async Task ItRespectResultFormatExecutionSettingAsync(string format) }; // Act - var settings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") { ResponseFormat = format }; + var settings = new OpenAIAudioToTextExecutionSettings("file.mp3") { ResponseFormat = format }; var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings); // Assert @@ -147,7 +148,7 @@ public async Task GetTextContentByDefaultWorksCorrectlyAsync() }; // Act - var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new AzureOpenAIAudioToTextExecutionSettings("file.mp3")); + var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("file.mp3")); // Assert Assert.NotNull(result); @@ -160,9 +161,9 @@ public void Dispose() this._messageHandlerStub.Dispose(); } - public static TheoryData ExecutionSettings => new() + public static TheoryData ExecutionSettings => new() { - { new AzureOpenAIAudioToTextExecutionSettings(""), typeof(ArgumentException) }, - { new AzureOpenAIAudioToTextExecutionSettings("file"), typeof(ArgumentException) } + { new OpenAIAudioToTextExecutionSettings(""), typeof(ArgumentException) }, + { new OpenAIAudioToTextExecutionSettings("file"), typeof(ArgumentException) } }; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs index b1f69110bf21..c087b7a28d41 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToAudioServiceTests.cs @@ -10,6 +10,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Moq; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Services; @@ -58,7 +59,7 @@ public void ItThrowsIfModelIdIsNotProvided() public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync() { // Arrange - var settingsWithInvalidVoice = new AzureOpenAITextToAudioExecutionSettings(""); + var settingsWithInvalidVoice = new OpenAITextToAudioExecutionSettings(""); var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); await using var stream = new MemoryStream(new byte[] { 0x00, 0x00, 0xFF, 0x7F }); @@ -87,7 +88,7 @@ public async Task GetAudioContentByDefaultWorksCorrectlyAsync() }; // Act - var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova")); + var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("Nova")); // Assert var audioData = result[0].Data!.Value; @@ -115,7 +116,7 @@ public async Task GetAudioContentVoicesWorksCorrectlyAsync(string voice, string }; // Act - var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings(voice) { ResponseFormat = format }); + var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings(voice) { ResponseFormat = format }); // Assert var requestBody = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent!); @@ -137,7 +138,7 @@ public async Task GetAudioContentThrowsWhenVoiceIsNotSupportedAsync() var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); // Act & Assert - await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("voice"))); + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice"))); } [Fact] @@ -149,7 +150,7 @@ public async Task GetAudioContentThrowsWhenFormatIsNotSupportedAsync() var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); // Act & Assert - await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" })); + await Assert.ThrowsAsync(async () => await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings() { ResponseFormat = "not supported" })); } [Theory] @@ -174,7 +175,7 @@ public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAdd }; // Act - var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova")); + var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("Nova")); // Assert Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); @@ -199,7 +200,7 @@ public async Task GetAudioContentPrioritizesModelIdOverDeploymentNameAsync(strin }; // Act - var result = await service.GetAudioContentsAsync("Some text", new AzureOpenAITextToAudioExecutionSettings("Nova") { ModelId = modelInSettings }); + var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("Nova") { ModelId = modelInSettings }); // Assert var requestBody = JsonSerializer.Deserialize(this._messageHandlerStub.RequestContent!); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs deleted file mode 100644 index 5f7f89be988f..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIAudioToTextExecutionSettingsTests.cs +++ /dev/null @@ -1,121 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIAudioToTextExecutionSettingsTests -{ - [Fact] - public void ItReturnsDefaultSettingsWhenSettingsAreNull() - { - Assert.NotNull(AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(null)); - } - - [Fact] - public void ItReturnsValidOpenAIAudioToTextExecutionSettings() - { - // Arrange - var audioToTextSettings = new AzureOpenAIAudioToTextExecutionSettings("file.mp3") - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "json", - Temperature = 0.2f - }; - - // Act - var settings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(audioToTextSettings); - - // Assert - Assert.Same(audioToTextSettings, settings); - } - - [Fact] - public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() - { - // Arrange - var json = """ - { - "model_id": "model_id", - "language": "en", - "filename": "file.mp3", - "prompt": "prompt", - "response_format": "verbose_json", - "temperature": 0.2 - } - """; - - var executionSettings = JsonSerializer.Deserialize(json); - - // Act - var settings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); - - // Assert - Assert.NotNull(settings); - Assert.Equal("model_id", settings.ModelId); - Assert.Equal("en", settings.Language); - Assert.Equal("file.mp3", settings.Filename); - Assert.Equal("prompt", settings.Prompt); - Assert.Equal("verbose_json", settings.ResponseFormat); - Assert.Equal(0.2f, settings.Temperature); - } - - [Fact] - public void ItClonesAllProperties() - { - var settings = new AzureOpenAIAudioToTextExecutionSettings() - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "vtt", - Temperature = 0.2f, - Filename = "something.mp3", - }; - - var clone = (AzureOpenAIAudioToTextExecutionSettings)settings.Clone(); - Assert.NotSame(settings, clone); - - Assert.Equal("model_id", clone.ModelId); - Assert.Equal("en", clone.Language); - Assert.Equal("prompt", clone.Prompt); - Assert.Equal("vtt", clone.ResponseFormat); - Assert.Equal(0.2f, clone.Temperature); - Assert.Equal("something.mp3", clone.Filename); - } - - [Fact] - public void ItFreezesAndPreventsMutation() - { - var settings = new AzureOpenAIAudioToTextExecutionSettings() - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "srt", - Temperature = 0.2f, - Filename = "something.mp3", - }; - - settings.Freeze(); - Assert.True(settings.IsFrozen); - - Assert.Throws(() => settings.ModelId = "new_model"); - Assert.Throws(() => settings.Language = "some_format"); - Assert.Throws(() => settings.Prompt = "prompt"); - Assert.Throws(() => settings.ResponseFormat = "srt"); - Assert.Throws(() => settings.Temperature = 0.2f); - Assert.Throws(() => settings.Filename = "something"); - - settings.Freeze(); // idempotent - Assert.True(settings.IsFrozen); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs index e67ecbd0572e..d187d7a49fb8 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs @@ -5,6 +5,7 @@ using System.Text.Json; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; @@ -91,7 +92,6 @@ public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() { "top_p", 0.7 }, { "frequency_penalty", 0.7 }, { "presence_penalty", 0.7 }, - { "results_per_prompt", 2 }, { "stop_sequences", new [] { "foo", "bar" } }, { "chat_system_prompt", "chat system prompt" }, { "max_tokens", 128 }, @@ -121,7 +121,6 @@ public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() { "top_p", "0.7" }, { "frequency_penalty", "0.7" }, { "presence_penalty", "0.7" }, - { "results_per_prompt", "2" }, { "stop_sequences", new [] { "foo", "bar" } }, { "chat_system_prompt", "chat system prompt" }, { "max_tokens", "128" }, @@ -248,6 +247,32 @@ public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() Assert.Null(executionSettingsWithData.StopSequences); } + [Fact] + public void FromExecutionSettingsCreateAzureOpenAIPromptExecutionSettingsFromOpenAIPromptExecutionSettings() + { + // Arrange + OpenAIPromptExecutionSettings originalSettings = new() + { + Temperature = 0.7, + TopP = 0.7, + FrequencyPenalty = 0.7, + PresencePenalty = 0.7, + StopSequences = new string[] { "foo", "bar" }, + ChatSystemPrompt = "chat system prompt", + TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, + MaxTokens = 128, + Logprobs = true, + Seed = 123456, + TopLogprobs = 5 + }; + + // Act + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(originalSettings); + + // Assert + AssertExecutionSettings(executionSettings); + } + private static void AssertExecutionSettings(AzureOpenAIPromptExecutionSettings executionSettings) { Assert.NotNull(executionSettings); diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs deleted file mode 100644 index 3eadbe124e10..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAITextToAudioExecutionSettingsTests.cs +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAITextToAudioExecutionSettingsTests -{ - [Fact] - public void ItReturnsDefaultSettingsWhenSettingsAreNull() - { - Assert.NotNull(AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(null)); - } - - [Fact] - public void ItReturnsValidOpenAITextToAudioExecutionSettings() - { - // Arrange - var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings("voice") - { - ModelId = "model_id", - ResponseFormat = "mp3", - Speed = 1.0f - }; - - // Act - var settings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(textToAudioSettings); - - // Assert - Assert.Same(textToAudioSettings, settings); - } - - [Fact] - public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() - { - // Arrange - var json = """ - { - "model_id": "model_id", - "voice": "voice", - "response_format": "mp3", - "speed": 1.2 - } - """; - - var executionSettings = JsonSerializer.Deserialize(json); - - // Act - var settings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - - // Assert - Assert.NotNull(settings); - Assert.Equal("model_id", settings.ModelId); - Assert.Equal("voice", settings.Voice); - Assert.Equal("mp3", settings.ResponseFormat); - Assert.Equal(1.2f, settings.Speed); - } - - [Fact] - public void ItClonesAllProperties() - { - var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings() - { - ModelId = "some_model", - ResponseFormat = "some_format", - Speed = 3.14f, - Voice = "something" - }; - - var clone = (AzureOpenAITextToAudioExecutionSettings)textToAudioSettings.Clone(); - Assert.NotSame(textToAudioSettings, clone); - - Assert.Equal("some_model", clone.ModelId); - Assert.Equal("some_format", clone.ResponseFormat); - Assert.Equal(3.14f, clone.Speed); - Assert.Equal("something", clone.Voice); - } - - [Fact] - public void ItFreezesAndPreventsMutation() - { - var textToAudioSettings = new AzureOpenAITextToAudioExecutionSettings() - { - ModelId = "some_model", - ResponseFormat = "some_format", - Speed = 3.14f, - Voice = "something" - }; - - textToAudioSettings.Freeze(); - Assert.True(textToAudioSettings.IsFrozen); - - Assert.Throws(() => textToAudioSettings.ModelId = "new_model"); - Assert.Throws(() => textToAudioSettings.ResponseFormat = "some_format"); - Assert.Throws(() => textToAudioSettings.Speed = 3.14f); - Assert.Throws(() => textToAudioSettings.Voice = "something"); - - textToAudioSettings.Freeze(); // idempotent - Assert.True(textToAudioSettings.IsFrozen); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index 5e3aa0565b93..83a283490305 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -5,6 +5,7 @@ using System.IO; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.OpenAI; using OpenAI.Audio; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -31,7 +32,7 @@ internal async Task> GetTextFromAudioContentsAsync( throw new ArgumentException("The input audio content is not readable.", nameof(input)); } - AzureOpenAIAudioToTextExecutionSettings audioExecutionSettings = AzureOpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; + OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; AudioTranscriptionOptions audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); Verify.ValidFilename(audioExecutionSettings?.Filename); @@ -44,11 +45,11 @@ internal async Task> GetTextFromAudioContentsAsync( } /// - /// Converts to type. + /// Converts to type. /// - /// Instance of . + /// Instance of . /// Instance of . - private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(AzureOpenAIAudioToTextExecutionSettings executionSettings) + private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) => new() { Granularities = AudioTimestampGranularities.Default, diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs index 4cb78c74d658..0742727ac46b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Connectors.OpenAI; using OpenAI.Audio; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -30,7 +31,7 @@ internal async Task> GetAudioContentsAsync( { Verify.NotNullOrWhiteSpace(prompt); - AzureOpenAITextToAudioExecutionSettings audioExecutionSettings = AzureOpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + OpenAITextToAudioExecutionSettings audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings.ResponseFormat); @@ -71,7 +72,7 @@ private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeec _ => throw new NotSupportedException($"The format '{format}' is not supported.") }; - private string GetModelId(AzureOpenAITextToAudioExecutionSettings executionSettings, string? modelId) + private string GetModelId(OpenAITextToAudioExecutionSettings executionSettings, string? modelId) { return !string.IsNullOrWhiteSpace(modelId) ? modelId! : diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs deleted file mode 100644 index f09c4bb8072a..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIAudioToTextExecutionSettings.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Execution settings for Azure OpenAI audio-to-text request. -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAIAudioToTextExecutionSettings : PromptExecutionSettings -{ - /// - /// Filename or identifier associated with audio data. - /// Should be in format {filename}.{extension} - /// - [JsonPropertyName("filename")] - public string Filename - { - get => this._filename; - - set - { - this.ThrowIfFrozen(); - this._filename = value; - } - } - - /// - /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es'). - /// - [JsonPropertyName("language")] - public string? Language - { - get => this._language; - - set - { - this.ThrowIfFrozen(); - this._language = value; - } - } - - /// - /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. - /// - [JsonPropertyName("prompt")] - public string? Prompt - { - get => this._prompt; - - set - { - this.ThrowIfFrozen(); - this._prompt = value; - } - } - - /// - /// The format of the transcript output, in one of these options: json, srt, verbose_json, or vtt. Default is 'json'. - /// - [JsonPropertyName("response_format")] - public string ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The sampling temperature, between 0 and 1. - /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. - /// If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. - /// Default is 0. - /// - [JsonPropertyName("temperature")] - public float Temperature - { - get => this._temperature; - - set - { - this.ThrowIfFrozen(); - this._temperature = value; - } - } - - /// - /// Creates an instance of class with default filename - "file.mp3". - /// - public AzureOpenAIAudioToTextExecutionSettings() - : this(DefaultFilename) - { - } - - /// - /// Creates an instance of class. - /// - /// Filename or identifier associated with audio data. Should be in format {filename}.{extension} - public AzureOpenAIAudioToTextExecutionSettings(string filename) - { - this._filename = filename; - } - - /// - public override PromptExecutionSettings Clone() - { - return new AzureOpenAIAudioToTextExecutionSettings(this.Filename) - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Temperature = this.Temperature, - ResponseFormat = this.ResponseFormat, - Language = this.Language, - Prompt = this.Prompt - }; - } - - /// - /// Converts to derived type. - /// - /// Instance of . - /// Instance of . - public static AzureOpenAIAudioToTextExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) - { - if (executionSettings is null) - { - return new AzureOpenAIAudioToTextExecutionSettings(); - } - - if (executionSettings is AzureOpenAIAudioToTextExecutionSettings settings) - { - return settings; - } - - var json = JsonSerializer.Serialize(executionSettings); - - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - - if (openAIExecutionSettings is not null) - { - return openAIExecutionSettings; - } - - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIAudioToTextExecutionSettings)}", nameof(executionSettings)); - } - - #region private ================================================================================ - - private const string DefaultFilename = "file.mp3"; - - private float _temperature = 0; - private string _responseFormat = "json"; - private string _filename; - private string? _language; - private string? _prompt; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs index 289a7405f371..2a83e1756f1c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs @@ -1,16 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; using System.Diagnostics.CodeAnalysis; using System.Text.Json; using System.Text.Json.Serialization; using Azure.AI.OpenAI.Chat; -using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Text; -using OpenAI.Chat; namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; @@ -18,260 +14,8 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// Execution settings for an AzureOpenAI completion request. /// [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] -public sealed class AzureOpenAIPromptExecutionSettings : PromptExecutionSettings +public sealed class AzureOpenAIPromptExecutionSettings : OpenAIPromptExecutionSettings { - /// - /// Temperature controls the randomness of the completion. - /// The higher the temperature, the more random the completion. - /// Default is 1.0. - /// - [JsonPropertyName("temperature")] - public double Temperature - { - get => this._temperature; - - set - { - this.ThrowIfFrozen(); - this._temperature = value; - } - } - - /// - /// TopP controls the diversity of the completion. - /// The higher the TopP, the more diverse the completion. - /// Default is 1.0. - /// - [JsonPropertyName("top_p")] - public double TopP - { - get => this._topP; - - set - { - this.ThrowIfFrozen(); - this._topP = value; - } - } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens - /// based on whether they appear in the text so far, increasing the - /// model's likelihood to talk about new topics. - /// - [JsonPropertyName("presence_penalty")] - public double PresencePenalty - { - get => this._presencePenalty; - - set - { - this.ThrowIfFrozen(); - this._presencePenalty = value; - } - } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens - /// based on their existing frequency in the text so far, decreasing - /// the model's likelihood to repeat the same line verbatim. - /// - [JsonPropertyName("frequency_penalty")] - public double FrequencyPenalty - { - get => this._frequencyPenalty; - - set - { - this.ThrowIfFrozen(); - this._frequencyPenalty = value; - } - } - - /// - /// The maximum number of tokens to generate in the completion. - /// - [JsonPropertyName("max_tokens")] - public int? MaxTokens - { - get => this._maxTokens; - - set - { - this.ThrowIfFrozen(); - this._maxTokens = value; - } - } - - /// - /// Sequences where the completion will stop generating further tokens. - /// - [JsonPropertyName("stop_sequences")] - public IList? StopSequences - { - get => this._stopSequences; - - set - { - this.ThrowIfFrozen(); - this._stopSequences = value; - } - } - - /// - /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the - /// same seed and parameters should return the same result. Determinism is not guaranteed. - /// - [JsonPropertyName("seed")] - public long? Seed - { - get => this._seed; - - set - { - this.ThrowIfFrozen(); - this._seed = value; - } - } - - /// - /// Gets or sets the response format to use for the completion. - /// - /// - /// Possible values are: "json_object", "text", object. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("response_format")] - public object? ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The system prompt to use when generating text using a chat model. - /// Defaults to "Assistant is a large language model." - /// - [JsonPropertyName("chat_system_prompt")] - public string? ChatSystemPrompt - { - get => this._chatSystemPrompt; - - set - { - this.ThrowIfFrozen(); - this._chatSystemPrompt = value; - } - } - - /// - /// Modify the likelihood of specified tokens appearing in the completion. - /// - [JsonPropertyName("token_selection_biases")] - public IDictionary? TokenSelectionBiases - { - get => this._tokenSelectionBiases; - - set - { - this.ThrowIfFrozen(); - this._tokenSelectionBiases = value; - } - } - - /// - /// Gets or sets the behavior for how tool calls are handled. - /// - /// - /// - /// To disable all tool calling, set the property to null (the default). - /// - /// To request that the model use a specific function, set the property to an instance returned - /// from . - /// - /// - /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with - /// a list of the functions available. - /// - /// - /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply - /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically - /// invoke the function and send the result back to the service. - /// - /// - /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service - /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to - /// resolve that function from the functions available in the , and if found, rather - /// than returning the response back to the caller, it will handle the request automatically, invoking - /// the function, and sending back the result. The intermediate messages will be retained in the - /// if an instance was provided. - /// - public ToolCallBehavior? ToolCallBehavior - { - get => this._toolCallBehavior; - - set - { - this.ThrowIfFrozen(); - this._toolCallBehavior = value; - } - } - - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse - /// - public string? User - { - get => this._user; - - set - { - this.ThrowIfFrozen(); - this._user = value; - } - } - - /// - /// Whether to return log probabilities of the output tokens or not. - /// If true, returns the log probabilities of each output token returned in the `content` of `message`. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("logprobs")] - public bool? Logprobs - { - get => this._logprobs; - - set - { - this.ThrowIfFrozen(); - this._logprobs = value; - } - } - - /// - /// An integer specifying the number of most likely tokens to return at each token position, each with an associated log probability. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("top_logprobs")] - public int? TopLogprobs - { - get => this._topLogprobs; - - set - { - this.ThrowIfFrozen(); - this._topLogprobs = value; - } - } - /// /// An abstraction of additional settings for chat completion, see https://learn.microsoft.com/en-us/dotnet/api/azure.ai.openai.azurechatextensionsoptions. /// This property is compatible only with Azure OpenAI. @@ -289,64 +33,21 @@ public AzureChatDataSource? AzureChatDataSource } } - /// - public override void Freeze() - { - if (this.IsFrozen) - { - return; - } - - base.Freeze(); - - if (this._stopSequences is not null) - { - this._stopSequences = new ReadOnlyCollection(this._stopSequences); - } - - if (this._tokenSelectionBiases is not null) - { - this._tokenSelectionBiases = new ReadOnlyDictionary(this._tokenSelectionBiases); - } - } - /// public override PromptExecutionSettings Clone() { - return new AzureOpenAIPromptExecutionSettings() - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Temperature = this.Temperature, - TopP = this.TopP, - PresencePenalty = this.PresencePenalty, - FrequencyPenalty = this.FrequencyPenalty, - MaxTokens = this.MaxTokens, - StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - Seed = this.Seed, - ResponseFormat = this.ResponseFormat, - TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, - ToolCallBehavior = this.ToolCallBehavior, - User = this.User, - ChatSystemPrompt = this.ChatSystemPrompt, - Logprobs = this.Logprobs, - TopLogprobs = this.TopLogprobs, - AzureChatDataSource = this.AzureChatDataSource, - }; + var settings = base.Clone(); + settings.AzureChatDataSource = this.AzureChatDataSource; + return settings; } - /// - /// Default max tokens for a text generation - /// - internal static int DefaultTextMaxTokens { get; } = 256; - /// /// Create a new settings object with the values from another settings object. /// /// Template configuration /// Default max tokens /// An instance of OpenAIPromptExecutionSettings - public static AzureOpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) + public static new AzureOpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) { if (executionSettings is null) { @@ -361,12 +62,14 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettings(PromptExe return settings; } - var json = JsonSerializer.Serialize(executionSettings); + // Having the object as the type of the value to serialize is important to ensure all properties of the settings are serialized. + // Otherwise, only the properties ServiceId and ModelId from the public API of the PromptExecutionSettings class will be serialized. + var json = JsonSerializer.Serialize(executionSettings); - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - if (openAIExecutionSettings is not null) + var azureOpenAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); + if (azureOpenAIExecutionSettings is not null) { - return openAIExecutionSettings; + return azureOpenAIExecutionSettings; } throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIPromptExecutionSettings)}", nameof(executionSettings)); @@ -395,20 +98,6 @@ public static AzureOpenAIPromptExecutionSettings FromExecutionSettingsWithData(P #region private ================================================================================ - private double _temperature = 1; - private double _topP = 1; - private double _presencePenalty; - private double _frequencyPenalty; - private int? _maxTokens; - private IList? _stopSequences; - private long? _seed; - private object? _responseFormat; - private IDictionary? _tokenSelectionBiases; - private ToolCallBehavior? _toolCallBehavior; - private string? _user; - private string? _chatSystemPrompt; - private bool? _logprobs; - private int? _topLogprobs; private AzureChatDataSource? _azureChatDataSource; #endregion diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs deleted file mode 100644 index 1552d56f26ce..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAITextToAudioExecutionSettings.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Execution settings for Azure OpenAI text-to-audio request. -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAITextToAudioExecutionSettings : PromptExecutionSettings -{ - /// - /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. - /// - [JsonPropertyName("voice")] - public string Voice - { - get => this._voice; - - set - { - this.ThrowIfFrozen(); - this._voice = value; - } - } - - /// - /// The format to audio in. Supported formats are mp3, opus, aac, and flac. - /// - [JsonPropertyName("response_format")] - public string ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. - /// - [JsonPropertyName("speed")] - public float Speed - { - get => this._speed; - - set - { - this.ThrowIfFrozen(); - this._speed = value; - } - } - - /// - /// Creates an instance of class with default voice - "alloy". - /// - public AzureOpenAITextToAudioExecutionSettings() - : this(DefaultVoice) - { - } - - /// - /// Creates an instance of class. - /// - /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. - public AzureOpenAITextToAudioExecutionSettings(string voice) - { - this._voice = voice; - } - - /// - public override PromptExecutionSettings Clone() - { - return new AzureOpenAITextToAudioExecutionSettings(this.Voice) - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Speed = this.Speed, - ResponseFormat = this.ResponseFormat - }; - } - - /// - /// Converts to derived type. - /// - /// Instance of . - /// Instance of . - public static AzureOpenAITextToAudioExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) - { - if (executionSettings is null) - { - return new AzureOpenAITextToAudioExecutionSettings(); - } - - if (executionSettings is AzureOpenAITextToAudioExecutionSettings settings) - { - return settings; - } - - var json = JsonSerializer.Serialize(executionSettings); - - var azureOpenAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - - if (azureOpenAIExecutionSettings is not null) - { - return azureOpenAIExecutionSettings; - } - - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAITextToAudioExecutionSettings)}", nameof(executionSettings)); - } - - #region private ================================================================================ - - private const string DefaultVoice = "alloy"; - - private float _speed = 1.0f; - private string _responseFormat = "mp3"; - private string _voice; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs index fe911f32d627..f83e401c0e55 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs @@ -11,15 +11,11 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; -/* Phase 06 -- Drop FromExecutionSettingsWithData Azure specific method -*/ - /// /// Execution settings for an OpenAI completion request. /// [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] -public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings +public class OpenAIPromptExecutionSettings : PromptExecutionSettings { /// /// Temperature controls the randomness of the completion. @@ -297,25 +293,7 @@ public override void Freeze() /// public override PromptExecutionSettings Clone() { - return new OpenAIPromptExecutionSettings() - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Temperature = this.Temperature, - TopP = this.TopP, - PresencePenalty = this.PresencePenalty, - FrequencyPenalty = this.FrequencyPenalty, - MaxTokens = this.MaxTokens, - StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - Seed = this.Seed, - ResponseFormat = this.ResponseFormat, - TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, - ToolCallBehavior = this.ToolCallBehavior, - User = this.User, - ChatSystemPrompt = this.ChatSystemPrompt, - Logprobs = this.Logprobs, - TopLogprobs = this.TopLogprobs - }; + return this.Clone(); } /// @@ -351,6 +329,34 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio return openAIExecutionSettings!; } + /// + /// Clone the settings object. + /// + /// The type of the settings object to clone. + /// A new instance of the settings object. + protected T Clone() where T : OpenAIPromptExecutionSettings, new() + { + return new T() + { + ModelId = this.ModelId, + ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, + Temperature = this.Temperature, + TopP = this.TopP, + PresencePenalty = this.PresencePenalty, + FrequencyPenalty = this.FrequencyPenalty, + MaxTokens = this.MaxTokens, + StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, + Seed = this.Seed, + ResponseFormat = this.ResponseFormat, + TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, + ToolCallBehavior = this.ToolCallBehavior, + User = this.User, + ChatSystemPrompt = this.ChatSystemPrompt, + Logprobs = this.Logprobs, + TopLogprobs = this.TopLogprobs + }; + } + #region private ================================================================================ private double _temperature = 1; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs index 8fca703901eb..07e3305e69df 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs @@ -98,7 +98,7 @@ public override PromptExecutionSettings Clone() /// /// Instance of . /// Instance of . - public static OpenAITextToAudioExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings) + public static OpenAITextToAudioExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings) { if (executionSettings is null) { From 44f27a214a01da083cf4da57abd594ee1fb747d7 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:00:21 +0100 Subject: [PATCH 088/226] .Net: Cleanup (#7266) Cleanup: 1. Align the handling of the result of the `AzureOpenAIPromptExecutionSettings` class deserialization with the way it is handled in the new OpenAI prompt execution settings classes. There's no need to check the result of the `JsonSerializer.Deserialize` method for null because it can't return null in this particular case. 2. Remove the forgotten comment and unused enum. --- .../AzureOpenAIPromptExecutionSettings.cs | 8 ++---- .../OpenAIAudioToTextExecutionSettings.cs | 26 ------------------- 2 files changed, 2 insertions(+), 32 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs index 2a83e1756f1c..4cfbdf0bb72c 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs @@ -66,13 +66,9 @@ public override PromptExecutionSettings Clone() // Otherwise, only the properties ServiceId and ModelId from the public API of the PromptExecutionSettings class will be serialized. var json = JsonSerializer.Serialize(executionSettings); - var azureOpenAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - if (azureOpenAIExecutionSettings is not null) - { - return azureOpenAIExecutionSettings; - } + var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(AzureOpenAIPromptExecutionSettings)}", nameof(executionSettings)); + return openAIExecutionSettings!; } /// diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs index ce3366059763..d41bdcc7ae96 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -148,32 +148,6 @@ public override PromptExecutionSettings Clone() return openAIExecutionSettings!; } - /// - /// Specifies the format of the audio transcription. - /// - public enum AudioTranscriptionFormat - { - /// - /// Response body that is a JSON object containing a single 'text' field for the transcription. - /// - Simple, - - /// - /// Use a response body that is a JSON object containing transcription text along with timing, segments, and other metadata. - /// - Verbose, - - /// - /// Response body that is plain text in SubRip (SRT) format that also includes timing information. - /// - Srt, - - /// - /// Response body that is plain text in Web Video Text Tracks (VTT) format that also includes timing information. - /// - Vtt, - } - #region private ================================================================================ private const string DefaultFilename = "file.mp3"; From c425b7871d7e2a2287998e25e084dec602f59844 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:29:31 +0100 Subject: [PATCH 089/226] .Net: OpenAI V2 - Concepts Migration - Phase 2.0 (#7233) ### Motivation and Context - Added files back to compilation - Migrated Planners.OpenAI to V2 - Disabled IntegrationTests with Planner V2 to avoid collision with other packages using the OpenAI V1. - Breaking changes updated folders - Concepts - Dependency Injection - Functions - [FunctionResult_StronglyTyped.cs](https://github.com/microsoft/semantic-kernel/blob/b4a4caa111397d0172bc1e9023907919311df6c0/dotnet/samples/Concepts/Functions/FunctionResult_StronglyTyped.cs) (bigger) - Memory - LearnResources - AIServices --------- Co-authored-by: SergeyMenshykh Co-authored-by: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> --- dotnet/samples/Concepts/Concepts.csproj | 110 +----------------- .../DependencyInjection/Kernel_Injecting.cs | 2 +- .../Functions/FunctionResult_StronglyTyped.cs | 8 +- .../Memory/TextChunkingAndEmbedding.cs | 2 +- ...ugin_RecallJsonSerializationWithOptions.cs | 2 +- .../LearnResources/LearnResources.csproj | 5 +- .../MicrosoftLearn/AIServices.cs | 14 --- .../OpenAIMemoryBuilderExtensions.cs | 44 +++++++ .../Connectors/OpenAI/OpenAIToolsTests.cs | 3 +- .../IntegrationTests/IntegrationTests.csproj | 8 +- .../Planners.OpenAI/Planners.OpenAI.csproj | 2 +- 11 files changed, 64 insertions(+), 136 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 63dabdd45eb6..a11241024bf9 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -73,7 +73,7 @@ - + @@ -120,8 +120,6 @@ - - @@ -145,58 +143,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -231,8 +177,6 @@ - - @@ -256,58 +200,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/samples/Concepts/DependencyInjection/Kernel_Injecting.cs b/dotnet/samples/Concepts/DependencyInjection/Kernel_Injecting.cs index 4c6e38452fc6..21abae070cf0 100644 --- a/dotnet/samples/Concepts/DependencyInjection/Kernel_Injecting.cs +++ b/dotnet/samples/Concepts/DependencyInjection/Kernel_Injecting.cs @@ -14,7 +14,7 @@ public async Task RunAsync() { ServiceCollection collection = new(); collection.AddLogging(c => c.AddConsole().SetMinimumLevel(LogLevel.Information)); - collection.AddOpenAITextGeneration(TestConfiguration.OpenAI.ModelId, TestConfiguration.OpenAI.ApiKey); + collection.AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); collection.AddSingleton(); // Registering class that uses Kernel to execute a plugin diff --git a/dotnet/samples/Concepts/Functions/FunctionResult_StronglyTyped.cs b/dotnet/samples/Concepts/Functions/FunctionResult_StronglyTyped.cs index 0b50562583ea..79826de22bec 100644 --- a/dotnet/samples/Concepts/Functions/FunctionResult_StronglyTyped.cs +++ b/dotnet/samples/Concepts/Functions/FunctionResult_StronglyTyped.cs @@ -2,8 +2,8 @@ using System.Diagnostics; using System.Text.Json; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; +using OpenAI.Chat; namespace Functions; @@ -79,11 +79,11 @@ public FunctionResultTestDataGen(FunctionResult functionResult, long executionTi private TokenCounts? ParseTokenCounts() { - CompletionsUsage? usage = FunctionResult.Metadata?["Usage"] as CompletionsUsage; + var usage = FunctionResult.Metadata?["Usage"] as ChatTokenUsage; return new TokenCounts( - completionTokens: usage?.CompletionTokens ?? 0, - promptTokens: usage?.PromptTokens ?? 0, + completionTokens: usage?.OutputTokens ?? 0, + promptTokens: usage?.InputTokens ?? 0, totalTokens: usage?.TotalTokens ?? 0); } diff --git a/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs b/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs index 013bb4961621..96b3cb9431db 100644 --- a/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs +++ b/dotnet/samples/Concepts/Memory/TextChunkingAndEmbedding.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.ML.Tokenizers; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Text; namespace Memory; diff --git a/dotnet/samples/Concepts/Memory/TextMemoryPlugin_RecallJsonSerializationWithOptions.cs b/dotnet/samples/Concepts/Memory/TextMemoryPlugin_RecallJsonSerializationWithOptions.cs index fbc313adebf4..883195b68df9 100644 --- a/dotnet/samples/Concepts/Memory/TextMemoryPlugin_RecallJsonSerializationWithOptions.cs +++ b/dotnet/samples/Concepts/Memory/TextMemoryPlugin_RecallJsonSerializationWithOptions.cs @@ -4,7 +4,7 @@ using System.Text.Json; using System.Text.Unicode; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Memory; using Microsoft.SemanticKernel.Plugins.Memory; diff --git a/dotnet/samples/LearnResources/LearnResources.csproj b/dotnet/samples/LearnResources/LearnResources.csproj index d210f8effa91..72cff80ad017 100644 --- a/dotnet/samples/LearnResources/LearnResources.csproj +++ b/dotnet/samples/LearnResources/LearnResources.csproj @@ -51,7 +51,8 @@ - + + @@ -68,6 +69,6 @@ - + \ No newline at end of file diff --git a/dotnet/samples/LearnResources/MicrosoftLearn/AIServices.cs b/dotnet/samples/LearnResources/MicrosoftLearn/AIServices.cs index a56e6591f8ad..d957358cac77 100644 --- a/dotnet/samples/LearnResources/MicrosoftLearn/AIServices.cs +++ b/dotnet/samples/LearnResources/MicrosoftLearn/AIServices.cs @@ -45,25 +45,11 @@ public async Task RunAsync() .Build(); // - // You could instead create a kernel with a legacy Azure OpenAI text completion service - // - kernel = Kernel.CreateBuilder() - .AddAzureOpenAITextGeneration(textModelId, endpoint, apiKey) - .Build(); - // - // You can also create a kernel with a (non-Azure) OpenAI chat completion service // kernel = Kernel.CreateBuilder() .AddOpenAIChatCompletion(openAImodelId, openAIapiKey) .Build(); // - - // Or a kernel with a legacy OpenAI text completion service - // - kernel = Kernel.CreateBuilder() - .AddOpenAITextGeneration(openAItextModelId, openAIapiKey) - .Build(); - // } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs new file mode 100644 index 000000000000..0ac425a15593 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Diagnostics.CodeAnalysis; +using System.Net.Http; +using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Memory; + +namespace Microsoft.SemanticKernel.Connectors.OpenAI; + +/// +/// Provides extension methods for the class to configure OpenAI connector. +/// +public static class OpenAIMemoryBuilderExtensions +{ + /// + /// Adds the OpenAI text embeddings service. + /// See https://platform.openai.com/docs for service details. + /// + /// The instance + /// OpenAI model name, see https://platform.openai.com/docs/models + /// OpenAI API key, see https://platform.openai.com/account/api-keys + /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. + /// Custom for HTTP requests. + /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. + /// Self instance + [Experimental("SKEXP0010")] + public static MemoryBuilder WithOpenAITextEmbeddingGeneration( + this MemoryBuilder builder, + string modelId, + string apiKey, + string? orgId = null, + HttpClient? httpClient = null, + int? dimensions = null) + { + return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => + new OpenAITextEmbeddingGenerationService( + modelId, + apiKey, + orgId, + HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), + loggerFactory, + dimensions)); + } +} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs index 049287fbbc14..243526fdfc82 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs @@ -13,7 +13,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.Planners.Stepwise; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; @@ -740,7 +739,7 @@ private Kernel InitializeKernel(bool importHelperPlugin = false) .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() - .AddUserSecrets() + .AddUserSecrets() .Build(); /// diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index df5afa473ce7..6d741d390c2e 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -18,6 +18,7 @@ + @@ -75,8 +76,9 @@ + - + @@ -151,6 +153,10 @@ + + + + Always diff --git a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj b/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj index 194753a700ad..d6f5f1bb08e1 100644 --- a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj +++ b/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj @@ -32,7 +32,7 @@ - + From f356b9d0296fe04ca25823d4dfd6987441c9d2e8 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 17 Jul 2024 14:29:31 +0100 Subject: [PATCH 090/226] .Net: Chat history serialization test + bug fix (#7305) ### Motivation, Context and Description This PR adds a few integration tests to ensure that the chat history, filled by existing {Azure}OpenAI chat completion connectors and serialized, is backward-compatible with the migrated {Azure}OpenAI chat completion connectors, so it can be deserialized and used as is. Additionally, this PR fixes the issue caused by using the wrong constructor of the `AssistantChatMessage` class. A corresponding integration test is added to test this scenario. Closes: https://github.com/microsoft/semantic-kernel/issues/7055 --- .github/_typos.toml | 1 + .../Core/ClientCore.ChatCompletion.cs | 7 + .../Core/ClientCore.ChatCompletion.cs | 7 + ...enAIChatCompletion_FunctionCallingTests.cs | 103 +++++++++++++++ ...enAIChatCompletion_FunctionCallingTests.cs | 103 +++++++++++++++ .../serializedChatHistoryV1_15_1.json | 125 ++++++++++++++++++ 6 files changed, 346 insertions(+) create mode 100644 dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json diff --git a/.github/_typos.toml b/.github/_typos.toml index 917745e1ae83..08b4ab37f906 100644 --- a/.github/_typos.toml +++ b/.github/_typos.toml @@ -16,6 +16,7 @@ extend-exclude = [ "test_code_tokenizer.py", "*response.json", "test_content.txt", + "serializedChatHistoryV1_15_1.json" ] [default.extend-words] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index 2974acc3e993..c341ac29bd3e 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -876,6 +876,13 @@ private static List CreateRequestMessages(ChatMessageContent messag toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } + // This check is necessary to prevent an exception that will be thrown if the toolCalls collection is empty. + // HTTP 400 (invalid_request_error:) [] should be non-empty - 'messages.3.tool_calls' + if (toolCalls.Count == 0) + { + return [new AssistantChatMessage(message.Content) { ParticipantName = message.AuthorName }]; + } + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs index 97258077c589..ddcce86e00f7 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -861,6 +861,13 @@ private static List CreateRequestMessages(ChatMessageContent messag toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); } + // This check is necessary to prevent an exception that will be thrown if the toolCalls collection is empty. + // HTTP 400 (invalid_request_error:) [] should be non-empty - 'messages.3.tool_calls' + if (toolCalls.Count == 0) + { + return [new AssistantChatMessage(message.Content) { ParticipantName = message.AuthorName }]; + } + return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; } diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 2053208037ad..001f502414c6 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Text.Json; @@ -699,6 +700,108 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu Assert.Contains("tornado", result, StringComparison.InvariantCultureIgnoreCase); } + [Fact] + public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistoryByPreviousVersionOfSKAsync() + { + // Arrange + var chatHistory = JsonSerializer.Deserialize(File.ReadAllText("./TestData/serializedChatHistoryV1_15_1.json")); + + // Remove connector-agnostic function-calling items to check if the old function-calling model, which relies on function information in metadata, is handled correctly. + foreach (var chatMessage in chatHistory!) + { + var index = 0; + while (index < chatMessage.Items.Count) + { + var item = chatMessage.Items[index]; + if (item is FunctionCallContent || item is FunctionResultContent) + { + chatMessage.Items.Remove(item); + continue; + } + index++; + } + } + + string? emailBody = null, emailRecipient = null; + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); + + // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. + chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal("abc@domain.com", emailRecipient); + Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + } + + [Fact] + public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistoryByPreviousVersionOfSKAsync() + { + // Arrange + var chatHistory = JsonSerializer.Deserialize(File.ReadAllText("./TestData/serializedChatHistoryV1_15_1.json")); + + // Remove metadata related to the old function-calling model to check if the new model, which relies on function call content/result classes, is handled correctly. + foreach (var chatMessage in chatHistory!) + { + if (chatMessage.Metadata is not null) + { + var metadata = new Dictionary(chatMessage.Metadata); + metadata.Remove(OpenAIChatMessageContent.ToolIdProperty); + metadata.Remove("ChatResponseMessage.FunctionToolCalls"); + chatMessage.Metadata = metadata; + } + } + + string? emailBody = null, emailRecipient = null; + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); + + // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. + chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal("abc@domain.com", emailRecipient); + Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + } + + /// + /// This test verifies that the connector can handle the scenario where the assistant response message is added to the chat history. + /// The assistant response message with no function calls added to chat history caused the error: HTTP 400 (invalid_request_error:) [] should be non-empty - 'messages.3.tool_calls' + /// + [Fact] + public async Task AssistantResponseAddedToChatHistoryShouldBeHandledCorrectlyAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var assistanceResponse = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(assistanceResponse); // Adding assistance response to chat history. + chatHistory.AddUserMessage("Return only the color name."); + + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + } + private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) { var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs index dc503960eaf4..5cb6c8d4a0b9 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Text; using System.Text.Json; @@ -698,6 +699,108 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFu Assert.Contains("tornado", result, StringComparison.InvariantCultureIgnoreCase); } + [Fact] + public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistoryByPreviousVersionOfSKAsync() + { + // Arrange + var chatHistory = JsonSerializer.Deserialize(File.ReadAllText("./TestData/serializedChatHistoryV1_15_1.json")); + + // Remove connector-agnostic function-calling items to check if the old function-calling model, which relies on function information in metadata, is handled correctly. + foreach (var chatMessage in chatHistory!) + { + var index = 0; + while (index < chatMessage.Items.Count) + { + var item = chatMessage.Items[index]; + if (item is FunctionCallContent || item is FunctionResultContent) + { + chatMessage.Items.Remove(item); + continue; + } + index++; + } + } + + string? emailBody = null, emailRecipient = null; + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); + + // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. + chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal("abc@domain.com", emailRecipient); + Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + } + + [Fact] + public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistoryByPreviousVersionOfSKAsync() + { + // Arrange + var chatHistory = JsonSerializer.Deserialize(File.ReadAllText("./TestData/serializedChatHistoryV1_15_1.json")); + + // Remove metadata related to the old function-calling model to check if the new model, which relies on function call content/result classes, is handled correctly. + foreach (var chatMessage in chatHistory!) + { + if (chatMessage.Metadata is not null) + { + var metadata = new Dictionary(chatMessage.Metadata); + metadata.Remove(OpenAIChatMessageContent.ToolIdProperty); + metadata.Remove("ChatResponseMessage.FunctionToolCalls"); + chatMessage.Metadata = metadata; + } + } + + string? emailBody = null, emailRecipient = null; + + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); + + // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. + chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + // Act + var result = await kernel.GetRequiredService().GetChatMessageContentAsync(chatHistory, settings, kernel); + + // Assert + Assert.Equal("abc@domain.com", emailRecipient); + Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + } + + /// + /// This test verifies that the connector can handle the scenario where the assistant response message is added to the chat history. + /// The assistant response message with no function calls added to chat history caused the error: HTTP 400 (invalid_request_error:) [] should be non-empty - 'messages.3.tool_calls' + /// + [Fact] + public async Task AssistantResponseAddedToChatHistoryShouldBeHandledCorrectlyAsync() + { + // Arrange + var kernel = this.CreateAndInitializeKernel(importHelperPlugin: true); + + var chatHistory = new ChatHistory(); + chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); + + var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; + + var sut = kernel.GetRequiredService(); + + // Act + var assistanceResponse = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + + chatHistory.Add(assistanceResponse); // Adding assistance response to chat history. + chatHistory.AddUserMessage("Return only the color name."); + + await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); + } + private Kernel CreateAndInitializeKernel(bool importHelperPlugin = false) { var OpenAIConfiguration = this._configuration.GetSection("OpenAI").Get(); diff --git a/dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json b/dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json new file mode 100644 index 000000000000..7da4cfe721d4 --- /dev/null +++ b/dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json @@ -0,0 +1,125 @@ +[ + { + "Role": { + "Label": "user" + }, + "Items": [ + { + "$type": "TextContent", + "Text": "Given the current time of day and weather, what is the likely color of the sky in Boston?" + } + ] + }, + { + "Role": { + "Label": "assistant" + }, + "Items": [ + { + "$type": "FunctionCallContent", + "Id": "call_q5FoU2fpfEyZmvC6iqtIXPYQ", + "PluginName": "HelperFunctions", + "FunctionName": "Get_Weather_For_City", + "Arguments": { + "cityName": "Boston" + } + } + ], + "ModelId": "gpt-4", + "Metadata": { + "Id": "chatcmpl-9lf5Qgx7xquKec3tc6lTn27y8Lmkz", + "Created": "2024-07-16T16:13:00+00:00", + "PromptFilterResults": [], + "SystemFingerprint": null, + "Usage": { + "CompletionTokens": 23, + "PromptTokens": 196, + "TotalTokens": 219 + }, + "ContentFilterResults": null, + "FinishReason": "tool_calls", + "FinishDetails": null, + "LogProbabilityInfo": null, + "Index": 0, + "Enhancements": null, + "ChatResponseMessage.FunctionToolCalls": [ + { + "Name": "HelperFunctions-Get_Weather_For_City", + "Arguments": "{\n \u0022cityName\u0022: \u0022Boston\u0022\n}", + "Id": "call_q5FoU2fpfEyZmvC6iqtIXPYQ" + } + ] + } + }, + { + "Role": { + "Label": "tool" + }, + "Items": [ + { + "$type": "TextContent", + "Text": "61 and rainy", + "Metadata": { + "ChatCompletionsToolCall.Id": "call_q5FoU2fpfEyZmvC6iqtIXPYQ" + } + }, + { + "$type": "FunctionResultContent", + "CallId": "call_q5FoU2fpfEyZmvC6iqtIXPYQ", + "PluginName": "HelperFunctions", + "FunctionName": "Get_Weather_For_City", + "Result": "61 and rainy" + } + ], + "Metadata": { + "ChatCompletionsToolCall.Id": "call_q5FoU2fpfEyZmvC6iqtIXPYQ" + } + }, + { + "Role": { + "Label": "assistant" + }, + "Items": [ + { + "$type": "TextContent", + "Text": "Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", + "ModelId": "gpt-4", + "Metadata": { + "Id": "chatcmpl-9lf5RibNr9h4bzq7JJjUXj6ITz7wN", + "Created": "2024-07-16T16:13:01+00:00", + "PromptFilterResults": [], + "SystemFingerprint": null, + "Usage": { + "CompletionTokens": 34, + "PromptTokens": 237, + "TotalTokens": 271 + }, + "ContentFilterResults": null, + "FinishReason": "stop", + "FinishDetails": null, + "LogProbabilityInfo": null, + "Index": 0, + "Enhancements": null + } + } + ], + "ModelId": "gpt-4", + "Metadata": { + "Id": "chatcmpl-9lf5RibNr9h4bzq7JJjUXj6ITz7wN", + "Created": "2024-07-16T16:13:01+00:00", + "PromptFilterResults": [], + "SystemFingerprint": null, + "Usage": { + "CompletionTokens": 34, + "PromptTokens": 237, + "TotalTokens": 271 + }, + "ContentFilterResults": null, + "FinishReason": "stop", + "FinishDetails": null, + "LogProbabilityInfo": null, + "Index": 0, + "Enhancements": null + } + } +] \ No newline at end of file From c92f87fa29feab934edbc3984d9ec8f947ddd2cd Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 17 Jul 2024 10:21:57 -0700 Subject: [PATCH 091/226] Fix merge --- dotnet/samples/Concepts/Concepts.csproj | 1 + dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 61b47a1c62eb..2858e6d54429 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -153,6 +153,7 @@ + diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs index b0affa1e68a2..eb5ed86a77e3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; namespace GettingStarted; @@ -26,7 +27,7 @@ public async Task UseChatCompletionWithPluginAgentAsync() Instructions = HostInstructions, Name = HostName, Kernel = this.CreateKernelWithChatCompletion(), - ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = AzureOpenAIToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). From 8797fc93baca0a946f359f357f18923b9ef9de27 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 17 Jul 2024 11:06:06 -0700 Subject: [PATCH 092/226] Fix merge (exclude new concept sample and demo) --- dotnet/SK-dotnet.sln | 19 +------------------ dotnet/samples/Concepts/Concepts.csproj | 14 +++++++++----- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index d8e97bb9dfa9..93936ced5bc9 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -344,7 +344,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -845,24 +845,9 @@ Global {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.Build.0 = Debug|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.Build.0 = Release|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Publish|Any CPU.Build.0 = Debug|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1D4667B9-9381-4E32-895F-123B94253EE8}.Release|Any CPU.Build.0 = Release|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Publish|Any CPU.Build.0 = Debug|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF}.Release|Any CPU.Build.0 = Release|Any CPU {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.Build.0 = Debug|Any CPU {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.Build.0 = Debug|Any CPU {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -979,8 +964,6 @@ Global {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} - {1D4667B9-9381-4E32-895F-123B94253EE8} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} - {E92AE954-8F3A-4A6F-A4F9-DC12017E5AAF} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 2583618f265e..fd06e4a0dc25 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -99,12 +99,18 @@ PreserveNewest + + + Always + + Always + @@ -143,6 +149,7 @@ + @@ -162,6 +169,7 @@ + @@ -200,6 +208,7 @@ + @@ -218,9 +227,4 @@ - - - Always - - From 9205cec1dbf03d3e8948426297b5b08c8aa6e6b3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 17 Jul 2024 11:20:16 -0700 Subject: [PATCH 093/226] Fix build --- .../OpenAI/Internal/AssistantThreadActions.cs | 8 ++++---- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +- .../Agents/ChatCompletionAgentTests.cs | 15 +-------------- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index ca012ca9b268..901017e94fe7 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -42,10 +42,10 @@ internal static class AssistantThreadActions /// The message to add /// The to monitor for cancellation requests. The default is . /// if a system message is present, without taking any other action + public static async Task CreateMessageAsync(AssistantClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) + { if (string.IsNullOrEmpty(message.Content) || message.Items.Any(i => i is FunctionCallContent)) - - if (string.IsNullOrWhiteSpace(message.Content)) { return; } @@ -224,10 +224,10 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist ChatMessageContent? content = null; // Process code-interpreter content + if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter) + { content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput); isVisible = true; - { - content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); } // Process function result content else if (toolCall.ToolKind == RunStepToolCallKind.Function) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 00ad90c05422..f6555ebff6d7 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -247,7 +247,7 @@ public async IAsyncEnumerable InvokeAsync( { this.ThrowIfDeleted(); - await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, cancellationToken).ConfigureAwait(false)) + await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, settings, this.Logger, cancellationToken).ConfigureAwait(false)) { if (isVisible) { diff --git a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs index 91796c1970b0..fa4f75a34331 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs @@ -5,20 +5,18 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -using Xunit.Abstractions; namespace SemanticKernel.IntegrationTests.Agents.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class ChatCompletionAgentTests(ITestOutputHelper output) : IDisposable +public sealed class ChatCompletionAgentTests() { private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() @@ -42,8 +40,6 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - this._kernelBuilder.Services.AddSingleton(this._logger); - this._kernelBuilder.AddAzureOpenAIChatCompletion( configuration.ChatDeploymentName!, configuration.Endpoint, @@ -94,15 +90,6 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns Assert.Contains(expectedAnswerContains, messages.Single().Content, StringComparison.OrdinalIgnoreCase); } - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] From 712c826b9022fee5e88c48ecc61d189d2f87f5e0 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 17 Jul 2024 11:35:25 -0700 Subject: [PATCH 094/226] Fix merge --- .../Concepts/Agents/ChatCompletion_FunctionTermination.cs | 5 +++-- .../IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs index f344dae432b9..f90f38587131 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -3,6 +3,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Agents; @@ -22,7 +23,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() { Instructions = "Answer questions about the menu.", Kernel = CreateKernelWithChatCompletion(), - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; KernelPlugin plugin = KernelPluginFactory.CreateFromType(); @@ -75,7 +76,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync() { Instructions = "Answer questions about the menu.", Kernel = CreateKernelWithChatCompletion(), - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; KernelPlugin plugin = KernelPluginFactory.CreateFromType(); diff --git a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs index fa4f75a34331..e6f06b766053 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs @@ -8,6 +8,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; @@ -59,7 +60,7 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns { Kernel = kernel, Instructions = "Answer questions about the menu.", - ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; AgentGroupChat chat = new(); From 3b8e54f3e8dbff9f9fcd4c6145cf5636b66704d3 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Wed, 17 Jul 2024 22:34:09 +0100 Subject: [PATCH 095/226] .Net: Refactor samples to use new {Azure}OpenAI connectors (#7334) ### Motivation, Context and Description This PR migrates another portion of samples to the recently introduced {Azure}OpenAI* connectors. It also fixes the regression in the OpenAI text-to-image connector, which caused requests to the service to fail if no modelId was provided. --- .../AzureOpenAIWithData_ChatCompletion.cs | 27 ++-- .../ChatCompletion/ChatHistoryAuthorName.cs | 1 + .../ChatCompletion/OpenAI_ChatCompletion.cs | 1 + .../OpenAI_ChatCompletionMultipleChoices.cs | 133 ------------------ .../OpenAI_ChatCompletionStreaming.cs | 1 + ..._ChatCompletionStreamingMultipleChoices.cs | 114 --------------- .../OpenAI_CustomAzureOpenAIClient.cs | 10 +- dotnet/samples/Concepts/Concepts.csproj | 82 ----------- .../Planners/AutoFunctionCallingPlanning.cs | 4 +- .../OpenAI_TextGenerationStreaming.cs | 9 +- .../StepwisePlannerMigration.csproj | 2 +- .../Services/OpenAITextToImageServiceTests.cs | 9 -- .../Core/ClientCore.TextToImage.cs | 6 +- .../Services/OpenAITextToImageService.cs | 1 - .../OpenAI/OpenAITextToImageTests.cs | 21 +++ 15 files changed, 53 insertions(+), 368 deletions(-) delete mode 100644 dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs delete mode 100644 dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreamingMultipleChoices.cs diff --git a/dotnet/samples/Concepts/ChatCompletion/AzureOpenAIWithData_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/AzureOpenAIWithData_ChatCompletion.cs index dcfdf7b511f0..39ce395b27b7 100644 --- a/dotnet/samples/Concepts/ChatCompletion/AzureOpenAIWithData_ChatCompletion.cs +++ b/dotnet/samples/Concepts/ChatCompletion/AzureOpenAIWithData_ChatCompletion.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; +using Azure.AI.OpenAI.Chat; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using xRetry; namespace ChatCompletion; @@ -47,8 +47,8 @@ public async Task ExampleWithChatCompletionAsync() chatHistory.AddUserMessage(ask); // Chat Completion example - var chatExtensionsOptions = GetAzureChatExtensionsOptions(); - var promptExecutionSettings = new OpenAIPromptExecutionSettings { AzureChatExtensionsOptions = chatExtensionsOptions }; + var dataSource = GetAzureSearchDataSource(); + var promptExecutionSettings = new AzureOpenAIPromptExecutionSettings { AzureChatDataSource = dataSource }; var chatCompletion = kernel.GetRequiredService(); @@ -98,8 +98,8 @@ public async Task ExampleWithKernelAsync() var function = kernel.CreateFunctionFromPrompt("Question: {{$input}}"); - var chatExtensionsOptions = GetAzureChatExtensionsOptions(); - var promptExecutionSettings = new OpenAIPromptExecutionSettings { AzureChatExtensionsOptions = chatExtensionsOptions }; + var dataSource = GetAzureSearchDataSource(); + var promptExecutionSettings = new AzureOpenAIPromptExecutionSettings { AzureChatDataSource = dataSource }; // First question without previous context based on uploaded content. var response = await kernel.InvokeAsync(function, new(promptExecutionSettings) { ["input"] = ask }); @@ -125,20 +125,15 @@ public async Task ExampleWithKernelAsync() } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - private static AzureChatExtensionsOptions GetAzureChatExtensionsOptions() + private static AzureSearchChatDataSource GetAzureSearchDataSource() { - var azureSearchExtensionConfiguration = new AzureSearchChatExtensionConfiguration + return new AzureSearchChatDataSource { - SearchEndpoint = new Uri(TestConfiguration.AzureAISearch.Endpoint), - Authentication = new OnYourDataApiKeyAuthenticationOptions(TestConfiguration.AzureAISearch.ApiKey), + Endpoint = new Uri(TestConfiguration.AzureAISearch.Endpoint), + Authentication = DataSourceAuthentication.FromApiKey(TestConfiguration.AzureAISearch.ApiKey), IndexName = TestConfiguration.AzureAISearch.IndexName }; - - return new AzureChatExtensionsOptions - { - Extensions = { azureSearchExtensionConfiguration } - }; } } diff --git a/dotnet/samples/Concepts/ChatCompletion/ChatHistoryAuthorName.cs b/dotnet/samples/Concepts/ChatCompletion/ChatHistoryAuthorName.cs index 05346974da2f..2d08c507aa4c 100644 --- a/dotnet/samples/Concepts/ChatCompletion/ChatHistoryAuthorName.cs +++ b/dotnet/samples/Concepts/ChatCompletion/ChatHistoryAuthorName.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace ChatCompletion; diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs index 22b6eec9baaf..758af2acc389 100644 --- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs +++ b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletion.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace ChatCompletion; diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs deleted file mode 100644 index 9534cac09a63..000000000000 --- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionMultipleChoices.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace ChatCompletion; - -/// -/// The following example shows how to use Semantic Kernel with multiple chat completion results. -/// -public class OpenAI_ChatCompletionMultipleChoices(ITestOutputHelper output) : BaseTest(output) -{ - /// - /// Example with multiple chat completion results using . - /// - [Fact] - public async Task MultipleChatCompletionResultsUsingKernelAsync() - { - var kernel = Kernel - .CreateBuilder() - .AddOpenAIChatCompletion( - modelId: TestConfiguration.OpenAI.ChatModelId, - apiKey: TestConfiguration.OpenAI.ApiKey) - .Build(); - - // Execution settings with configured ResultsPerPrompt property. - var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; - - var contents = await kernel.InvokePromptAsync>("Write a paragraph about why AI is awesome", new(executionSettings)); - - foreach (var content in contents!) - { - Console.Write(content.ToString() ?? string.Empty); - Console.WriteLine("\n-------------\n"); - } - } - - /// - /// Example with multiple chat completion results using . - /// - [Fact] - public async Task MultipleChatCompletionResultsUsingChatCompletionServiceAsync() - { - var kernel = Kernel - .CreateBuilder() - .AddOpenAIChatCompletion( - modelId: TestConfiguration.OpenAI.ChatModelId, - apiKey: TestConfiguration.OpenAI.ApiKey) - .Build(); - - // Execution settings with configured ResultsPerPrompt property. - var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Write a paragraph about why AI is awesome"); - - var chatCompletionService = kernel.GetRequiredService(); - - foreach (var chatMessageContent in await chatCompletionService.GetChatMessageContentsAsync(chatHistory, executionSettings)) - { - Console.Write(chatMessageContent.Content ?? string.Empty); - Console.WriteLine("\n-------------\n"); - } - } - - /// - /// This example shows how to handle multiple results in case if prompt template contains a call to another prompt function. - /// is used for result selection. - /// - [Fact] - public async Task MultipleChatCompletionResultsInPromptTemplateAsync() - { - var kernel = Kernel - .CreateBuilder() - .AddOpenAIChatCompletion( - modelId: TestConfiguration.OpenAI.ChatModelId, - apiKey: TestConfiguration.OpenAI.ApiKey) - .Build(); - - var executionSettings = new OpenAIPromptExecutionSettings { MaxTokens = 200, ResultsPerPrompt = 3 }; - - // Initializing a function with execution settings for multiple results. - // We ask AI to write one paragraph, but in execution settings we specified that we want 3 different results for this request. - var function = KernelFunctionFactory.CreateFromPrompt("Write a paragraph about why AI is awesome", executionSettings, "GetParagraph"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - - kernel.Plugins.Add(plugin); - - // Add function result selection filter. - kernel.FunctionInvocationFilters.Add(new FunctionResultSelectionFilter(this.Output)); - - // Inside our main request, we call MyPlugin.GetParagraph function for text summarization. - // Taking into account that MyPlugin.GetParagraph function produces 3 results, for text summarization we need to choose only one of them. - // Registered filter will be invoked during execution, which will select and return only 1 result, and this result will be inserted in our main request for summarization. - var result = await kernel.InvokePromptAsync("Summarize this text: {{MyPlugin.GetParagraph}}"); - - // It's possible to check what prompt was rendered for our main request. - Console.WriteLine($"Rendered prompt: '{result.RenderedPrompt}'"); - - // Output: - // Rendered prompt: 'Summarize this text: AI is awesome because...' - } - - /// - /// Example of filter which is responsible for result selection in case if some function produces multiple results. - /// - private sealed class FunctionResultSelectionFilter(ITestOutputHelper output) : IFunctionInvocationFilter - { - public async Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) - { - await next(context); - - // Selection logic for function which is expected to produce multiple results. - if (context.Function.Name == "GetParagraph") - { - // Get multiple results from function invocation - var contents = context.Result.GetValue>()!; - - output.WriteLine("Multiple results:"); - - foreach (var content in contents) - { - output.WriteLine(content.ToString()); - } - - // Select first result for correct prompt rendering - var selectedContent = contents[0]; - context.Result = new FunctionResult(context.Function, selectedContent, context.Kernel.Culture, selectedContent.Metadata); - } - } - } -} diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreaming.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreaming.cs index 4836dcf03d9f..bd1285e29af3 100644 --- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreaming.cs +++ b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreaming.cs @@ -2,6 +2,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace ChatCompletion; diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreamingMultipleChoices.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreamingMultipleChoices.cs deleted file mode 100644 index 6a23a43ae9f8..000000000000 --- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_ChatCompletionStreamingMultipleChoices.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace ChatCompletion; - -// The following example shows how to use Semantic Kernel with multiple streaming chat completion results. -public class OpenAI_ChatCompletionStreamingMultipleChoices(ITestOutputHelper output) : BaseTest(output) -{ - [Fact] - public Task AzureOpenAIMultiStreamingChatCompletionAsync() - { - Console.WriteLine("======== Azure OpenAI - Multiple Chat Completions - Raw Streaming ========"); - - AzureOpenAIChatCompletionService chatCompletionService = new( - deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, - endpoint: TestConfiguration.AzureOpenAI.Endpoint, - apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ChatModelId); - - return StreamingChatCompletionAsync(chatCompletionService, 3); - } - - [Fact] - public Task OpenAIMultiStreamingChatCompletionAsync() - { - Console.WriteLine("======== OpenAI - Multiple Chat Completions - Raw Streaming ========"); - - OpenAIChatCompletionService chatCompletionService = new( - modelId: TestConfiguration.OpenAI.ChatModelId, - apiKey: TestConfiguration.OpenAI.ApiKey); - - return StreamingChatCompletionAsync(chatCompletionService, 3); - } - - /// - /// Streams the results of a chat completion request to the console. - /// - /// Chat completion service to use - /// Number of results to get for each chat completion request - private async Task StreamingChatCompletionAsync(IChatCompletionService chatCompletionService, - int numResultsPerPrompt) - { - var executionSettings = new OpenAIPromptExecutionSettings() - { - MaxTokens = 200, - FrequencyPenalty = 0, - PresencePenalty = 0, - Temperature = 1, - TopP = 0.5, - ResultsPerPrompt = numResultsPerPrompt - }; - - var consoleLinesPerResult = 10; - - // Uncomment this if you want to use a console app to display the results - // ClearDisplayByAddingEmptyLines(); - - var prompt = "Hi, I'm looking for 5 random title names for sci-fi books"; - - await ProcessStreamAsyncEnumerableAsync(chatCompletionService, prompt, executionSettings, consoleLinesPerResult); - - Console.WriteLine(); - - // Set cursor position to after displayed results - // Console.SetCursorPosition(0, executionSettings.ResultsPerPrompt * consoleLinesPerResult); - - Console.WriteLine(); - } - - /// - /// Does the actual streaming and display of the chat completion. - /// - private async Task ProcessStreamAsyncEnumerableAsync(IChatCompletionService chatCompletionService, string prompt, - OpenAIPromptExecutionSettings executionSettings, int consoleLinesPerResult) - { - var messagesPerChoice = new Dictionary(); - var chatHistory = new ChatHistory(prompt); - - // For each chat completion update - await foreach (StreamingChatMessageContent chatUpdate in chatCompletionService.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings)) - { - // Set cursor position to the beginning of where this choice (i.e. this result of - // a single multi-result request) is to be displayed. - // Console.SetCursorPosition(0, chatUpdate.ChoiceIndex * consoleLinesPerResult + 1); - - // The first time around, start choice text with role information - if (!messagesPerChoice.ContainsKey(chatUpdate.ChoiceIndex)) - { - messagesPerChoice[chatUpdate.ChoiceIndex] = $"Role: {chatUpdate.Role ?? new AuthorRole()}\n"; - Console.Write($"Choice index: {chatUpdate.ChoiceIndex}, Role: {chatUpdate.Role ?? new AuthorRole()}"); - } - - // Add latest completion bit, if any - if (chatUpdate.Content is { Length: > 0 }) - { - messagesPerChoice[chatUpdate.ChoiceIndex] += chatUpdate.Content; - } - - // Overwrite what is currently in the console area for the updated choice - // Console.Write(messagesPerChoice[chatUpdate.ChoiceIndex]); - Console.Write($"Choice index: {chatUpdate.ChoiceIndex}, Content: {chatUpdate.Content}"); - } - - // Display the aggregated results - foreach (string message in messagesPerChoice.Values) - { - Console.WriteLine("-------------------"); - Console.WriteLine(message); - } - } -} diff --git a/dotnet/samples/Concepts/ChatCompletion/OpenAI_CustomAzureOpenAIClient.cs b/dotnet/samples/Concepts/ChatCompletion/OpenAI_CustomAzureOpenAIClient.cs index 9e63e4b46975..64228f692799 100644 --- a/dotnet/samples/Concepts/ChatCompletion/OpenAI_CustomAzureOpenAIClient.cs +++ b/dotnet/samples/Concepts/ChatCompletion/OpenAI_CustomAzureOpenAIClient.cs @@ -1,8 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel.Primitives; using Azure; using Azure.AI.OpenAI; -using Azure.Core.Pipeline; using Microsoft.SemanticKernel; namespace ChatCompletion; @@ -28,12 +28,12 @@ public async Task RunAsync() var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Add("My-Custom-Header", "My Custom Value"); - // Configure OpenAIClient to use the customized HttpClient - var clientOptions = new OpenAIClientOptions + // Configure AzureOpenAIClient to use the customized HttpClient + var clientOptions = new AzureOpenAIClientOptions { - Transport = new HttpClientTransport(httpClient), + Transport = new HttpClientPipelineTransport(httpClient), }; - var openAIClient = new OpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey), clientOptions); + var openAIClient = new AzureOpenAIClient(new Uri(endpoint), new AzureKeyCredential(apiKey), clientOptions); IKernelBuilder builder = Kernel.CreateBuilder(); builder.AddAzureOpenAIChatCompletion(deploymentName, openAIClient); diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index fd06e4a0dc25..666b84f0cf6d 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -126,47 +126,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -185,46 +144,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs b/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs index 4c287a63a216..38e3e53a0e74 100644 --- a/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs +++ b/dotnet/samples/Concepts/Planners/AutoFunctionCallingPlanning.cs @@ -7,13 +7,13 @@ using System.Security.Cryptography; using System.Text; using System.Text.Json; -using Azure.AI.OpenAI; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Planning; +using OpenAI.Chat; namespace Planners; @@ -328,7 +328,7 @@ private int GetChatHistoryTokens(ChatHistory? chatHistory) { if (message.Metadata is not null && message.Metadata.TryGetValue("Usage", out object? usage) && - usage is CompletionsUsage completionsUsage && + usage is ChatTokenUsage completionsUsage && completionsUsage is not null) { tokens += completionsUsage.TotalTokens; diff --git a/dotnet/samples/Concepts/TextGeneration/OpenAI_TextGenerationStreaming.cs b/dotnet/samples/Concepts/TextGeneration/OpenAI_TextGenerationStreaming.cs index 44b7806a1355..bb906bb6d05c 100644 --- a/dotnet/samples/Concepts/TextGeneration/OpenAI_TextGenerationStreaming.cs +++ b/dotnet/samples/Concepts/TextGeneration/OpenAI_TextGenerationStreaming.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.TextGeneration; @@ -22,11 +23,11 @@ public Task AzureOpenAITextGenerationStreamAsync() { Console.WriteLine("======== Azure OpenAI - Text Generation - Raw Streaming ========"); - var textGeneration = new AzureOpenAITextGenerationService( - deploymentName: TestConfiguration.AzureOpenAI.DeploymentName, + var textGeneration = new AzureOpenAIChatCompletionService( + deploymentName: TestConfiguration.AzureOpenAI.ChatDeploymentName, endpoint: TestConfiguration.AzureOpenAI.Endpoint, apiKey: TestConfiguration.AzureOpenAI.ApiKey, - modelId: TestConfiguration.AzureOpenAI.ModelId); + modelId: TestConfiguration.AzureOpenAI.ChatModelId); return this.TextGenerationStreamAsync(textGeneration); } @@ -36,7 +37,7 @@ public Task OpenAITextGenerationStreamAsync() { Console.WriteLine("======== Open AI - Text Generation - Raw Streaming ========"); - var textGeneration = new OpenAITextGenerationService("gpt-3.5-turbo-instruct", TestConfiguration.OpenAI.ApiKey); + var textGeneration = new OpenAIChatCompletionService(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); return this.TextGenerationStreamAsync(textGeneration); } diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj index 1475397e7eb2..adeeb1f6471b 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj +++ b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj @@ -9,7 +9,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs index f59fea554eda..1528986b9064 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs @@ -46,15 +46,6 @@ public void ConstructorWorksCorrectly() Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); } - [Fact] - public void ItThrowsIfModelIdIsNotProvided() - { - // Act & Assert - Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: " ")); - Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: string.Empty)); - Assert.Throws(() => new OpenAITextToImageService("apikey", modelId: null!)); - } - [Theory] [InlineData(256, 256, "dall-e-2")] [InlineData(512, 512, "dall-e-2")] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs index 26d8480fd004..cb6a681ca0e1 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs @@ -45,7 +45,11 @@ internal async Task GenerateImageAsync( ResponseFormat = GeneratedImageFormat.Uri }; - ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.ModelId).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + // The model is not required by the OpenAI API and defaults to the DALL-E 2 server-side - https://platform.openai.com/docs/api-reference/images/create#images-create-model. + // However, considering that the model is required by the OpenAI SDK and the ModelId property is optional, it defaults to DALL-E 2 in the line below. + var model = string.IsNullOrEmpty(this.ModelId) ? "dall-e-2" : this.ModelId; + + ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(model).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); var generatedImage = response.Value; return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index cca9073bfe9c..5bbff66c761e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -50,7 +50,6 @@ public OpenAITextToImageService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - Verify.NotNullOrWhiteSpace(modelId, nameof(modelId)); this._client = new(modelId, apiKey, organization, null, httpClient, loggerFactory?.CreateLogger(this.GetType())); } diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs index b2addba05188..85512760dcd0 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs @@ -39,4 +39,25 @@ public async Task OpenAITextToImageByModelTestAsync(string modelId, int width, i Assert.NotNull(result); Assert.NotEmpty(result); } + + [Fact] + public async Task OpenAITextToImageUseDallE2ByDefaultAsync() + { + // Arrange + OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToImage").Get(); + Assert.NotNull(openAIConfiguration); + + var kernel = Kernel.CreateBuilder() + .AddOpenAITextToImage(apiKey: openAIConfiguration.ApiKey, modelId: null) + .Build(); + + var service = kernel.GetRequiredService(); + + // Act + var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", 256, 256); + + // Assert + Assert.NotNull(result); + Assert.NotEmpty(result); + } } From d436430e37f7401bb86bec1f74cee3aed01d7cde Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 18 Jul 2024 10:24:36 +0100 Subject: [PATCH 096/226] .Net: Test execution settings compatibility (#7337) ### Motivation, Context and Description This PR adds a test that verifies the `OpenAIPromptExecutionSettings.FromExecutionSettings` method can handle arguments of type `AzureOpenAIPromptExecutionSettings`. Additionally, it fixes the issue found by @crickman when the `AzureOpenAIChatCompletionService.GetChatMessageContentsAsync` method is called with `OpenAIPromptExecutionSettings` instead of `AzureOpenAIPromptExecutionSettings`. Closes https://github.com/microsoft/semantic-kernel/issues/7110 --- ...AzureOpenAIPromptExecutionSettingsTests.cs | 5 +- .../OpenAIPromptExecutionSettingsTests.cs | 63 +++++++++++++++++++ .../AzureOpenAIPromptExecutionSettings.cs | 5 ++ .../Settings/OpenAIPromptExecutionSettings.cs | 2 +- 4 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs index d187d7a49fb8..40d0e36fc1b6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs @@ -248,7 +248,7 @@ public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() } [Fact] - public void FromExecutionSettingsCreateAzureOpenAIPromptExecutionSettingsFromOpenAIPromptExecutionSettings() + public void ItCanCreateAzureOpenAIPromptExecutionSettingsFromOpenAIPromptExecutionSettings() { // Arrange OpenAIPromptExecutionSettings originalSettings = new() @@ -263,7 +263,8 @@ public void FromExecutionSettingsCreateAzureOpenAIPromptExecutionSettingsFromOpe MaxTokens = 128, Logprobs = true, Seed = 123456, - TopLogprobs = 5 + TopLogprobs = 5, + ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; // Act diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs new file mode 100644 index 000000000000..100b0b1901d8 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System; +using System.Collections.Generic; +using Azure.AI.OpenAI.Chat; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace SemanticKernel.Connectors.AzureOpenAI.UnitTests.Settings; + +/// +/// Unit tests for class. +/// +public class OpenAIPromptExecutionSettingsTests +{ + [Fact] + public void ItCanCreateOpenAIPromptExecutionSettingsFromAzureOpenAIPromptExecutionSettings() + { + // Arrange + AzureOpenAIPromptExecutionSettings originalSettings = new() + { + Temperature = 0.7, + TopP = 0.7, + FrequencyPenalty = 0.7, + PresencePenalty = 0.7, + StopSequences = new string[] { "foo", "bar" }, + ChatSystemPrompt = "chat system prompt", + TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, + MaxTokens = 128, + Logprobs = true, + Seed = 123456, + TopLogprobs = 5, + AzureChatDataSource = new AzureSearchChatDataSource + { + Endpoint = new Uri("https://test-host"), + Authentication = DataSourceAuthentication.FromApiKey("api-key"), + IndexName = "index-name" + } + }; + + // Act + OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(originalSettings); + + // Assert + AssertExecutionSettings(executionSettings); + } + + private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings) + { + Assert.NotNull(executionSettings); + Assert.Equal(0.7, executionSettings.Temperature); + Assert.Equal(0.7, executionSettings.TopP); + Assert.Equal(0.7, executionSettings.FrequencyPenalty); + Assert.Equal(0.7, executionSettings.PresencePenalty); + Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); + Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); + Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); + Assert.Equal(128, executionSettings.MaxTokens); + Assert.Equal(123456, executionSettings.Seed); + Assert.Equal(true, executionSettings.Logprobs); + Assert.Equal(5, executionSettings.TopLogprobs); + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs index 4cfbdf0bb72c..90a20d3435b7 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Settings/AzureOpenAIPromptExecutionSettings.cs @@ -62,6 +62,11 @@ public override PromptExecutionSettings Clone() return settings; } + if (executionSettings is OpenAIPromptExecutionSettings openAISettings) + { + return openAISettings.Clone(); + } + // Having the object as the type of the value to serialize is important to ensure all properties of the settings are serialized. // Otherwise, only the properties ServiceId and ModelId from the public API of the PromptExecutionSettings class will be serialized. var json = JsonSerializer.Serialize(executionSettings); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs index f83e401c0e55..d3e78b9a3c11 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs @@ -334,7 +334,7 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio /// /// The type of the settings object to clone. /// A new instance of the settings object. - protected T Clone() where T : OpenAIPromptExecutionSettings, new() + protected internal T Clone() where T : OpenAIPromptExecutionSettings, new() { return new T() { From c03cc7fd7a2867d098633d0d504e26ec57799e81 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:57:04 +0100 Subject: [PATCH 097/226] .Net: Migrate remaining samples to new {Azure}OpenAI services (#7353) ### Motivation, Context, and Description This PR migrates the remaining samples related to agents to the new {Azure}OpenAI services. The agent samples will be updated again by this PR - https://github.com/microsoft/semantic-kernel/pull/7126 to use {Azure}OpenAI SDKs for operations with files and not use the deprecated file service. CC: @crickman --- .../Agents/ComplexChat_NestedShopper.cs | 4 +- .../Concepts/Agents/Legacy_AgentCharts.cs | 3 ++ .../Concepts/Agents/Legacy_AgentTools.cs | 6 ++- .../OpenAIAssistant_FileManipulation.cs | 3 ++ .../Agents/OpenAIAssistant_FileService.cs | 3 ++ .../Agents/OpenAIAssistant_Retrieval.cs | 3 +- dotnet/samples/Concepts/Concepts.csproj | 40 +------------------ .../Agents/Experimental.Agents.csproj | 2 +- .../Agents/Extensions/OpenAIRestExtensions.cs | 2 +- 9 files changed, 22 insertions(+), 44 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index aae984906ba3..81b2914ade3b 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -1,10 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Chat; using Resources; namespace Agents; @@ -98,7 +98,7 @@ public async Task NestedChatWithAggregatorAgentAsync() { Console.WriteLine($"! {Model}"); - OpenAIPromptExecutionSettings jsonSettings = new() { ResponseFormat = ChatCompletionsResponseFormat.JsonObject }; + OpenAIPromptExecutionSettings jsonSettings = new() { ResponseFormat = ChatResponseFormat.JsonObject }; OpenAIPromptExecutionSettings autoInvokeSettings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; ChatCompletionAgent internalLeaderAgent = CreateAgent(InternalLeaderName, InternalLeaderInstructions); diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index 877ba0971710..b64f183adbc8 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -91,13 +91,16 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi } } +#pragma warning disable CS0618 // Type or member is obsolete private static OpenAIFileService CreateFileService() + { return ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? new OpenAIFileService(TestConfiguration.OpenAI.ApiKey) : new OpenAIFileService(new Uri(TestConfiguration.AzureOpenAI.Endpoint), apiKey: TestConfiguration.AzureOpenAI.ApiKey); } +#pragma warning restore CS0618 // Type or member is obsolete private static AgentBuilder CreateAgentBuilder() { diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs index 66d93ecc88d9..c75a5e403cea 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs @@ -80,11 +80,13 @@ public async Task RunRetrievalToolAsync() } Kernel kernel = CreateFileEnabledKernel(); +#pragma warning disable CS0618 // Type or member is obsolete var fileService = kernel.GetRequiredService(); var result = await fileService.UploadContentAsync( new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); +#pragma warning restore CS0618 // Type or member is obsolete var fileId = result.Id; Console.WriteLine($"! {fileId}"); @@ -167,10 +169,12 @@ async Task InvokeAgentAsync(IAgent agent, string question) private static Kernel CreateFileEnabledKernel() { +#pragma warning disable CS0618 // Type or member is obsolete return ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() : - Kernel.CreateBuilder().AddAzureOpenAIFiles(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ApiKey).Build(); + throw new NotImplementedException("The file service is being deprecated and was not moved to AzureOpenAI connector."); +#pragma warning restore CS0618 // Type or member is obsolete } private static AgentBuilder CreateAgentBuilder() diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 8e64006ee9d3..f99130790eef 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -22,6 +22,7 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { +#pragma warning disable CS0618 // Type or member is obsolete OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); OpenAIFileReference uploadFile = @@ -29,6 +30,8 @@ await fileService.UploadContentAsync( new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); +#pragma warning restore CS0618 // Type or member is obsolete + Console.WriteLine(this.ApiKey); // Define the agent diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs index 7537f53da726..38bac46f648a 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs @@ -18,6 +18,7 @@ public class OpenAIAssistant_FileService(ITestOutputHelper output) : BaseTest(ou [Fact] public async Task UploadAndRetrieveFilesAsync() { +#pragma warning disable CS0618 // Type or member is obsolete OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); BinaryContent[] files = [ @@ -62,5 +63,7 @@ public async Task UploadAndRetrieveFilesAsync() // Delete the test file remotely await fileService.DeleteFileAsync(fileReference.Id); } + +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs index 6f30b6974ff7..71acf3db0e85 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs @@ -21,12 +21,13 @@ public class OpenAIAssistant_Retrieval(ITestOutputHelper output) : BaseTest(outp [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { +#pragma warning disable CS0618 // Type or member is obsolete OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); OpenAIFileReference uploadFile = await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); - +#pragma warning restore CS0618 // Type or member is obsolete // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 666b84f0cf6d..aca9ceb8887e 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -48,7 +48,7 @@ - + @@ -64,7 +64,7 @@ - + @@ -109,40 +109,4 @@ Always - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/dotnet/src/Experimental/Agents/Experimental.Agents.csproj b/dotnet/src/Experimental/Agents/Experimental.Agents.csproj index b5038dbabde9..648d6b7fd02f 100644 --- a/dotnet/src/Experimental/Agents/Experimental.Agents.csproj +++ b/dotnet/src/Experimental/Agents/Experimental.Agents.csproj @@ -20,7 +20,7 @@ - + diff --git a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs index aa4f324490d8..a8d446dad360 100644 --- a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs +++ b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs @@ -92,7 +92,7 @@ private static void AddHeaders(this HttpRequestMessage request, OpenAIRestContex { request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIFileService))); + request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIChatCompletionService))); if (context.HasVersion) { From 974dc996c575043400f8a29ac4724fa7bb454c8b Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 22 Jul 2024 13:11:13 +0100 Subject: [PATCH 098/226] .Net: OpenAI V2 - Demos Migration (#7384) ### Motivation and Context This change moves all references from OpenAI V1 and Azure to the new OpenAI V2 and AzureOpenAI packages. Resolves Partially #6876 --- .../BookingRestaurant.csproj | 2 +- .../CodeInterpreterPlugin.csproj | 2 +- .../Demos/ContentSafety/ContentSafety.csproj | 2 +- .../Demos/CreateChatGptPlugin/README.md | 23 ++++----- .../Solution/CreateChatGptPlugin.csproj | 6 ++- .../config/KernelBuilderExtensions.cs | 47 +++++-------------- .../FunctionInvocationApproval.csproj | 2 +- .../HomeAutomation/HomeAutomation.csproj | 2 +- .../{AzureOpenAI.cs => AzureOpenAIOptions.cs} | 2 +- .../samples/Demos/HomeAutomation/Program.cs | 17 +++---- .../TelemetryWithAppInsights.csproj | 2 +- .../Demos/TimePlugin/TimePlugin.csproj | 2 +- 12 files changed, 44 insertions(+), 65 deletions(-) rename dotnet/samples/Demos/HomeAutomation/Options/{AzureOpenAI.cs => AzureOpenAIOptions.cs} (91%) diff --git a/dotnet/samples/Demos/BookingRestaurant/BookingRestaurant.csproj b/dotnet/samples/Demos/BookingRestaurant/BookingRestaurant.csproj index 2f744127417e..678819305a93 100644 --- a/dotnet/samples/Demos/BookingRestaurant/BookingRestaurant.csproj +++ b/dotnet/samples/Demos/BookingRestaurant/BookingRestaurant.csproj @@ -22,7 +22,7 @@ - + diff --git a/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj b/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj index 8df5f889470e..fadc608dbda2 100644 --- a/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj +++ b/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj @@ -18,7 +18,7 @@ - + diff --git a/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj b/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj index f891f0d85a5c..7065ed5b64b4 100644 --- a/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj +++ b/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/Demos/CreateChatGptPlugin/README.md b/dotnet/samples/Demos/CreateChatGptPlugin/README.md index 3394ad2b1693..e9e035272d3d 100644 --- a/dotnet/samples/Demos/CreateChatGptPlugin/README.md +++ b/dotnet/samples/Demos/CreateChatGptPlugin/README.md @@ -16,17 +16,16 @@ The sample can be configured by using the command line with .NET [Secret Manager This sample has been tested with the following models: -| Service | Model type | Model | Model version | Supported | -| ------------ | --------------- | ---------------- | ------------: | --------- | -| OpenAI | Text Completion | text-davinci-003 | 1 | ❌ | -| OpenAI | Chat Completion | gpt-3.5-turbo | 1 | ❌ | -| OpenAI | Chat Completion | gpt-3.5-turbo | 0301 | ❌ | -| Azure OpenAI | Chat Completion | gpt-3.5-turbo | 0613 | ✅ | -| Azure OpenAI | Chat Completion | gpt-3.5-turbo | 1106 | ✅ | -| OpenAI | Chat Completion | gpt-4 | 1 | ❌ | -| OpenAI | Chat Completion | gpt-4 | 0314 | ❌ | -| Azure OpenAI | Chat Completion | gpt-4 | 0613 | ✅ | -| Azure OpenAI | Chat Completion | gpt-4 | 1106 | ✅ | +| Service | Model | Model version | Supported | +| ------------ | ---------------- | ------------: | --------- | +| OpenAI | gpt-3.5-turbo | 1 | ❌ | +| OpenAI | gpt-3.5-turbo | 0301 | ❌ | +| Azure OpenAI | gpt-3.5-turbo | 0613 | ✅ | +| Azure OpenAI | gpt-3.5-turbo | 1106 | ✅ | +| OpenAI | gpt-4 | 1 | ❌ | +| OpenAI | gpt-4 | 0314 | ❌ | +| Azure OpenAI | gpt-4 | 0613 | ✅ | +| Azure OpenAI | gpt-4 | 1106 | ✅ | This sample uses function calling, so it only works on models newer than 0613. @@ -39,7 +38,6 @@ cd 14-Create-ChatGPT-Plugin/Solution dotnet user-secrets set "Global:LlmService" "OpenAI" -dotnet user-secrets set "OpenAI:ModelType" "chat-completion" dotnet user-secrets set "OpenAI:ChatCompletionModelId" "gpt-4" dotnet user-secrets set "OpenAI:ApiKey" "... your OpenAI key ..." dotnet user-secrets set "OpenAI:OrgId" "... your ord ID ..." @@ -52,7 +50,6 @@ cd 14-Create-ChatGPT-Plugin/Solution dotnet user-secrets set "Global:LlmService" "AzureOpenAI" -dotnet user-secrets set "AzureOpenAI:DeploymentType" "chat-completion" dotnet user-secrets set "AzureOpenAI:ChatCompletionDeploymentName" "gpt-35-turbo" dotnet user-secrets set "AzureOpenAI:ChatCompletionModelId" "gpt-3.5-turbo-0613" dotnet user-secrets set "AzureOpenAI:Endpoint" "... your Azure OpenAI endpoint ..." diff --git a/dotnet/samples/Demos/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj b/dotnet/samples/Demos/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj index a81e39b415e4..a663838e564b 100644 --- a/dotnet/samples/Demos/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj +++ b/dotnet/samples/Demos/CreateChatGptPlugin/Solution/CreateChatGptPlugin.csproj @@ -16,8 +16,8 @@ + - @@ -26,4 +26,8 @@ + + + + diff --git a/dotnet/samples/Demos/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs b/dotnet/samples/Demos/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs index 3ba36e2bbdb8..a823ac316880 100644 --- a/dotnet/samples/Demos/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs +++ b/dotnet/samples/Demos/CreateChatGptPlugin/Solution/config/KernelBuilderExtensions.cs @@ -14,47 +14,24 @@ internal static IKernelBuilder WithCompletionService(this IKernelBuilder kernelB switch (Env.Var("Global:LlmService")!) { case "AzureOpenAI": - if (Env.Var("AzureOpenAI:DeploymentType") == "text-completion") - { - kernelBuilder.Services.AddAzureOpenAITextGeneration( - deploymentName: Env.Var("AzureOpenAI:TextCompletionDeploymentName")!, - modelId: Env.Var("AzureOpenAI:TextCompletionModelId"), - endpoint: Env.Var("AzureOpenAI:Endpoint")!, - apiKey: Env.Var("AzureOpenAI:ApiKey")! - ); - } - else if (Env.Var("AzureOpenAI:DeploymentType") == "chat-completion") - { - kernelBuilder.Services.AddAzureOpenAIChatCompletion( - deploymentName: Env.Var("AzureOpenAI:ChatCompletionDeploymentName")!, - modelId: Env.Var("AzureOpenAI:ChatCompletionModelId"), - endpoint: Env.Var("AzureOpenAI:Endpoint")!, - apiKey: Env.Var("AzureOpenAI:ApiKey")! - ); - } + kernelBuilder.Services.AddAzureOpenAIChatCompletion( + deploymentName: Env.Var("AzureOpenAI:ChatCompletionDeploymentName")!, + modelId: Env.Var("AzureOpenAI:ChatCompletionModelId"), + endpoint: Env.Var("AzureOpenAI:Endpoint")!, + apiKey: Env.Var("AzureOpenAI:ApiKey")! + ); break; case "OpenAI": - if (Env.Var("OpenAI:ModelType") == "text-completion") - { - kernelBuilder.Services.AddOpenAITextGeneration( - modelId: Env.Var("OpenAI:TextCompletionModelId")!, - apiKey: Env.Var("OpenAI:ApiKey")!, - orgId: Env.Var("OpenAI:OrgId") - ); - } - else if (Env.Var("OpenAI:ModelType") == "chat-completion") - { - kernelBuilder.Services.AddOpenAIChatCompletion( - modelId: Env.Var("OpenAI:ChatCompletionModelId")!, - apiKey: Env.Var("OpenAI:ApiKey")!, - orgId: Env.Var("OpenAI:OrgId") - ); - } + kernelBuilder.Services.AddOpenAIChatCompletion( + modelId: Env.Var("OpenAI:ChatCompletionModelId")!, + apiKey: Env.Var("OpenAI:ApiKey")!, + orgId: Env.Var("OpenAI:OrgId") + ); break; default: - throw new ArgumentException($"Invalid service type value: {Env.Var("OpenAI:ModelType")}"); + throw new ArgumentException($"Invalid service type value: {Env.Var("Global:LlmService")}"); } return kernelBuilder; diff --git a/dotnet/samples/Demos/FunctionInvocationApproval/FunctionInvocationApproval.csproj b/dotnet/samples/Demos/FunctionInvocationApproval/FunctionInvocationApproval.csproj index ead3b5036cb4..e39a7f5b795d 100644 --- a/dotnet/samples/Demos/FunctionInvocationApproval/FunctionInvocationApproval.csproj +++ b/dotnet/samples/Demos/FunctionInvocationApproval/FunctionInvocationApproval.csproj @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj b/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj index 06dfceda8b48..562d0cc883aa 100644 --- a/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj +++ b/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAI.cs b/dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAIOptions.cs similarity index 91% rename from dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAI.cs rename to dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAIOptions.cs index f4096b5e95d5..ef20853597cc 100644 --- a/dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAI.cs +++ b/dotnet/samples/Demos/HomeAutomation/Options/AzureOpenAIOptions.cs @@ -7,7 +7,7 @@ namespace HomeAutomation.Options; /// /// Azure OpenAI settings. /// -public sealed class AzureOpenAI +public sealed class AzureOpenAIOptions { [Required] public string ChatDeploymentName { get; set; } = string.Empty; diff --git a/dotnet/samples/Demos/HomeAutomation/Program.cs b/dotnet/samples/Demos/HomeAutomation/Program.cs index e55279405ceb..8f4882e3303f 100644 --- a/dotnet/samples/Demos/HomeAutomation/Program.cs +++ b/dotnet/samples/Demos/HomeAutomation/Program.cs @@ -32,24 +32,25 @@ internal static async Task Main(string[] args) builder.Services.AddHostedService(); // Get configuration - builder.Services.AddOptions() - .Bind(builder.Configuration.GetSection(nameof(AzureOpenAI))) + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection(nameof(AzureOpenAIOptions))) .ValidateDataAnnotations() .ValidateOnStart(); // Chat completion service that kernels will use builder.Services.AddSingleton(sp => { - AzureOpenAI options = sp.GetRequiredService>().Value; + OpenAIOptions options = sp.GetRequiredService>().Value; // A custom HttpClient can be provided to this constructor - return new AzureOpenAIChatCompletionService(options.ChatDeploymentName, options.Endpoint, options.ApiKey); + return new OpenAIChatCompletionService(options.ChatModelId, options.ApiKey); - /* Alternatively, you can use plain, non-Azure OpenAI after loading OpenAIOptions instead - of AzureOpenAI options with builder.Services.AddOptions: - OpenAI options = sp.GetRequiredService>().Value; + /* Alternatively, you can use plain, Azure OpenAI after loading AzureOpenAIOptions instead + of OpenAI options with builder.Services.AddOptions: - return new OpenAIChatCompletionService(options.ChatModelId, options.ApiKey);*/ + AzureOpenAIOptions options = sp.GetRequiredService>().Value; + + return new AzureOpenAIChatCompletionService(options.ChatDeploymentName, options.Endpoint, options.ApiKey); */ }); // Add plugins that can be used by kernels diff --git a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj index aaf0e5545b76..ac5b79837338 100644 --- a/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj +++ b/dotnet/samples/Demos/TelemetryWithAppInsights/TelemetryWithAppInsights.csproj @@ -18,8 +18,8 @@ + - diff --git a/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj b/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj index 37a777d6a97e..cbbe6d95b6cc 100644 --- a/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj +++ b/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj @@ -15,7 +15,7 @@ - + From 58d01e760b1d71db3b76b57f0c862ed5f725f961 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 08:51:18 -0700 Subject: [PATCH 099/226] Definition property update --- dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 53546d44fb5f..f07983a755a5 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -11,7 +11,7 @@ public sealed class OpenAIAssistantDefinition /// /// Identifies the AI model targeted by the agent. /// - public string? ModelName { get; init; } + public string ModelName { get; init; } = string.Empty; /// /// The description of the assistant. @@ -21,7 +21,7 @@ public sealed class OpenAIAssistantDefinition /// /// The assistant's unique id. (Ignored on create.) /// - public string? Id { get; init; } + public string Id { get; init; } = string.Empty; /// /// The system instructions for the assistant to use. From 080d21e295c2fb054264e07583799e8313a4f230 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 08:59:08 -0700 Subject: [PATCH 100/226] UT update --- .../Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index e692166986eb..f56f6da193aa 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -18,7 +18,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() { OpenAIAssistantDefinition definition = new(); - Assert.Null(definition.Id); + Assert.Equal(string.Empty, definition.Id); Assert.Null(definition.Name); Assert.Null(definition.ModelName); Assert.Null(definition.Instructions); From 31e30517da92dd6fc79be0ea974e9a471157803a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 08:59:47 -0700 Subject: [PATCH 101/226] More UT --- .../Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index f56f6da193aa..2cf5abb3f48e 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -19,8 +19,8 @@ public void VerifyOpenAIAssistantDefinitionInitialState() OpenAIAssistantDefinition definition = new(); Assert.Equal(string.Empty, definition.Id); + Assert.Equal(string.Empty, definition.ModelName); Assert.Null(definition.Name); - Assert.Null(definition.ModelName); Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); From a33dc5e4c8013d4839da28d7132fd8bf974fb655 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 09:21:47 -0700 Subject: [PATCH 102/226] Rename --- .../OpenAI/Internal/AssistantThreadActions.cs | 16 ++++++++-------- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 16 ++++++++-------- .../Agents/OpenAI/OpenAIAssistantDefinition.cs | 4 ++-- ...ngs.cs => OpenAIAssistantExecutionOptions.cs} | 6 +++--- ...gs.cs => OpenAIAssistantInvocationOptions.cs} | 4 ++-- ...ettings.cs => OpenAIThreadCreationOptions.cs} | 4 ++-- ...lingConfiguration.cs => RunPollingOptions.cs} | 2 +- .../OpenAI/OpenAIAssistantAgentTests.cs | 2 +- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 14 +++++++------- ... => OpenAIAssistantInvocationOptionsTests.cs} | 8 ++++---- ...ts.cs => OpenAIThreadCreationOptionsTests.cs} | 8 ++++---- 11 files changed, 42 insertions(+), 42 deletions(-) rename dotnet/src/Agents/OpenAI/{OpenAIAssistantExecutionSettings.cs => OpenAIAssistantExecutionOptions.cs} (83%) rename dotnet/src/Agents/OpenAI/{OpenAIAssistantInvocationSettings.cs => OpenAIAssistantInvocationOptions.cs} (94%) rename dotnet/src/Agents/OpenAI/{OpenAIThreadCreationSettings.cs => OpenAIThreadCreationOptions.cs} (93%) rename dotnet/src/Agents/OpenAI/{RunPollingConfiguration.cs => RunPollingOptions.cs} (97%) rename dotnet/src/Agents/UnitTests/OpenAI/{OpenAIAssistantInvocationSettingsTests.cs => OpenAIAssistantInvocationOptionsTests.cs} (90%) rename dotnet/src/Agents/UnitTests/OpenAI/{OpenAIThreadCreationSettingsTests.cs => OpenAIThreadCreationOptionsTests.cs} (86%) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 901017e94fe7..4cde6904b44a 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -145,7 +145,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist OpenAIAssistantAgent agent, AssistantClient client, string threadId, - OpenAIAssistantInvocationSettings? invocationSettings, + OpenAIAssistantInvocationOptions? invocationSettings, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -286,7 +286,7 @@ async Task PollRunStatusAsync() do { // Reduce polling frequency after a couple attempts - await Task.Delay(count >= 2 ? agent.Polling.RunPollingInterval : agent.Polling.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + await Task.Delay(count >= 2 ? agent.PollingOptions.RunPollingInterval : agent.PollingOptions.RunPollingBackoff, cancellationToken).ConfigureAwait(false); ++count; #pragma warning disable CA1031 // Do not catch general exception types @@ -355,7 +355,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R if (retry) { - await Task.Delay(agent.Polling.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); } ++count; @@ -489,18 +489,18 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } - private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantAgent agent, OpenAIAssistantInvocationSettings? invocationSettings) + private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantAgent agent, OpenAIAssistantInvocationOptions? invocationSettings) { - int? truncationMessageCount = ResolveExecutionSetting(invocationSettings?.TruncationMessageCount, agent.Definition.ExecutionSettings?.TruncationMessageCount); + int? truncationMessageCount = ResolveExecutionSetting(invocationSettings?.TruncationMessageCount, agent.Definition.ExecutionOptions?.TruncationMessageCount); RunCreationOptions options = new() { - MaxCompletionTokens = ResolveExecutionSetting(invocationSettings?.MaxCompletionTokens, agent.Definition.ExecutionSettings?.MaxCompletionTokens), - MaxPromptTokens = ResolveExecutionSetting(invocationSettings?.MaxPromptTokens, agent.Definition.ExecutionSettings?.MaxPromptTokens), + MaxCompletionTokens = ResolveExecutionSetting(invocationSettings?.MaxCompletionTokens, agent.Definition.ExecutionOptions?.MaxCompletionTokens), + MaxPromptTokens = ResolveExecutionSetting(invocationSettings?.MaxPromptTokens, agent.Definition.ExecutionOptions?.MaxPromptTokens), ModelOverride = invocationSettings?.ModelName, NucleusSamplingFactor = ResolveExecutionSetting(invocationSettings?.TopP, agent.Definition.TopP), - ParallelToolCallsEnabled = ResolveExecutionSetting(invocationSettings?.ParallelToolCallsEnabled, agent.Definition.ExecutionSettings?.ParallelToolCallsEnabled), + ParallelToolCallsEnabled = ResolveExecutionSetting(invocationSettings?.ParallelToolCallsEnabled, agent.Definition.ExecutionOptions?.ParallelToolCallsEnabled), ResponseFormat = ResolveExecutionSetting(invocationSettings?.EnableJsonResponse, agent.Definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, Temperature = ResolveExecutionSetting(invocationSettings?.Temperature, agent.Definition.Temperature), //ToolConstraint // %%% TODO diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f6555ebff6d7..48c17a03f745 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -36,7 +36,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// /// Defines polling behavior for run processing /// - public RunPollingConfiguration Polling { get; } = new(); + public RunPollingOptions PollingOptions { get; } = new(); /// /// Expose predefined tools merged with available kernel functions. @@ -139,7 +139,7 @@ public Task CreateThreadAsync(CancellationToken cancellationToken = defa /// %%% /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(OpenAIThreadCreationSettings? settings, CancellationToken cancellationToken = default) + public async Task CreateThreadAsync(OpenAIThreadCreationOptions? settings, CancellationToken cancellationToken = default) { ThreadCreationOptions options = new() @@ -242,7 +242,7 @@ public IAsyncEnumerable InvokeAsync( /// Asynchronous enumeration of messages. public async IAsyncEnumerable InvokeAsync( string threadId, - OpenAIAssistantInvocationSettings? settings, + OpenAIAssistantInvocationOptions? settings, [EnumeratorCancellation] CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); @@ -309,11 +309,11 @@ private OpenAIAssistantAgent( private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model) { - OpenAIAssistantExecutionSettings? settings = null; + OpenAIAssistantExecutionOptions? settings = null; if (model.Metadata.TryGetValue(SettingsMetadataKey, out string? settingsJson)) { - settings = JsonSerializer.Deserialize(settingsJson); + settings = JsonSerializer.Deserialize(settingsJson); } IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds; @@ -335,7 +335,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, VectorStoreId = vectorStoreId, - ExecutionSettings = settings, + ExecutionOptions = settings, }; } @@ -361,9 +361,9 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss } } - if (definition.ExecutionSettings != null) + if (definition.ExecutionOptions != null) { - string settingsJson = JsonSerializer.Serialize(definition.ExecutionSettings); + string settingsJson = JsonSerializer.Serialize(definition.ExecutionOptions); assistantCreationOptions.Metadata[SettingsMetadataKey] = settingsJson; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index f07983a755a5..ae66aab1502b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -76,7 +76,7 @@ public sealed class OpenAIAssistantDefinition public string? VectorStoreId { get; init; } /// - /// Default execution settings for each agent invocation. + /// Default execution options for each agent invocation. /// - public OpenAIAssistantExecutionSettings? ExecutionSettings { get; init; } + public OpenAIAssistantExecutionOptions? ExecutionOptions { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs similarity index 83% rename from dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs rename to dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index 6969310ad83c..48f327a46489 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -2,12 +2,12 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Defines agent execution settings for each invocation. +/// Defines agent execution options for each invocation. /// /// -/// These settings are persisted as a single entry of the agent's metadata with key: "__settings" +/// These options are persisted as a single entry of the agent's metadata with key: "__settings" /// -public sealed class OpenAIAssistantExecutionSettings +public sealed class OpenAIAssistantExecutionOptions { /// /// The maximum number of completion tokens that may be used over the course of the run. diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs similarity index 94% rename from dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs rename to dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs index 2e9c61eb05e3..2fee5dd84503 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs @@ -4,12 +4,12 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Defines per invocation execution settings that override the assistant's default settings. +/// Defines per invocation execution options that override the assistant definition. /// /// /// Not applicable to usage. /// -public sealed class OpenAIAssistantInvocationSettings +public sealed class OpenAIAssistantInvocationOptions { /// /// Override the AI model targeted by the agent. diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs similarity index 93% rename from dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs rename to dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs index 614ad64f6ba2..56b3aa167284 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationSettings.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs @@ -4,9 +4,9 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Thread creation settings. +/// Thread creation options. /// -public sealed class OpenAIThreadCreationSettings +public sealed class OpenAIThreadCreationOptions { /// /// Optional file-ids made available to the code_interpreter tool, if enabled. diff --git a/dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs similarity index 97% rename from dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs rename to dotnet/src/Agents/OpenAI/RunPollingOptions.cs index e534128a4e49..7dd71cfdd8b2 100644 --- a/dotnet/src/Agents/OpenAI/RunPollingConfiguration.cs +++ b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs @@ -6,7 +6,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Configuration and defaults associated with polling behavior for Assistant API run processing. /// -public sealed class RunPollingConfiguration +public sealed class RunPollingOptions { /// /// The default polling interval when monitoring thread-run status. diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 3d67537cb590..8eab72d37805 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -164,7 +164,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEverything3Async() // %%% NA EnableJsonResponse = true, CodeInterpterFileIds = ["file1", "file2"], Metadata = new Dictionary() { { "a", "1" } }, - ExecutionSettings = new(), + ExecutionOptions = new(), }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 2cf5abb3f48e..bf38207c3026 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -24,7 +24,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); - Assert.Null(definition.ExecutionSettings); + Assert.Null(definition.ExecutionOptions); Assert.Null(definition.Temperature); Assert.Null(definition.TopP); Assert.Null(definition.VectorStoreId); @@ -51,7 +51,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Metadata = new Dictionary() { { "a", "1" } }, Temperature = 2, TopP = 0, - ExecutionSettings = + ExecutionOptions = new() { MaxCompletionTokens = 1000, @@ -72,11 +72,11 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("#vs", definition.VectorStoreId); Assert.Equal(2, definition.Temperature); Assert.Equal(0, definition.TopP); - Assert.NotNull(definition.ExecutionSettings); - Assert.Equal(1000, definition.ExecutionSettings.MaxCompletionTokens); - Assert.Equal(1000, definition.ExecutionSettings.MaxPromptTokens); - Assert.Equal(12, definition.ExecutionSettings.TruncationMessageCount); - Assert.False(definition.ExecutionSettings.ParallelToolCallsEnabled); + Assert.NotNull(definition.ExecutionOptions); + Assert.Equal(1000, definition.ExecutionOptions.MaxCompletionTokens); + Assert.Equal(1000, definition.ExecutionOptions.MaxPromptTokens); + Assert.Equal(12, definition.ExecutionOptions.TruncationMessageCount); + Assert.False(definition.ExecutionOptions.ParallelToolCallsEnabled); Assert.Single(definition.Metadata); Assert.Single(definition.CodeInterpterFileIds); Assert.True(definition.EnableCodeInterpreter); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs similarity index 90% rename from dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs rename to dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs index ac9ae051bf4d..8a8669e30f07 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationSettingsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs @@ -6,9 +6,9 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI; /// -/// Unit testing of . +/// Unit testing of . /// -public class OpenAIAssistantInvocationSettingsTests +public class OpenAIAssistantInvocationOptionsTests { /// /// Verify initial state. @@ -16,7 +16,7 @@ public class OpenAIAssistantInvocationSettingsTests [Fact] public void OpenAIAssistantInvocationSettingsInitialState() { - OpenAIAssistantInvocationSettings settings = new(); + OpenAIAssistantInvocationOptions settings = new(); Assert.Null(settings.ModelName); Assert.Null(settings.Metadata); @@ -37,7 +37,7 @@ public void OpenAIAssistantInvocationSettingsInitialState() [Fact] public void OpenAIAssistantInvocationSettingsAssignment() { - OpenAIAssistantInvocationSettings settings = + OpenAIAssistantInvocationOptions settings = new() { ModelName = "testmodel", diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs similarity index 86% rename from dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs rename to dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index 0c096c0ba62e..545a338f5217 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationSettingsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -8,9 +8,9 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI; /// -/// Unit testing of . +/// Unit testing of . /// -public class OpenAIThreadCreationSettingsTests +public class OpenAIThreadCreationOptionsTests { /// /// Verify initial state. @@ -18,7 +18,7 @@ public class OpenAIThreadCreationSettingsTests [Fact] public void OpenAIThreadCreationSettingsInitialState() { - OpenAIThreadCreationSettings settings = new(); + OpenAIThreadCreationOptions settings = new(); Assert.Null(settings.Messages); Assert.Null(settings.Metadata); @@ -33,7 +33,7 @@ public void OpenAIThreadCreationSettingsInitialState() [Fact] public void OpenAIThreadCreationSettingsAssignment() { - OpenAIThreadCreationSettings definition = + OpenAIThreadCreationOptions definition = new() { Messages = [new ChatMessageContent(AuthorRole.User, "test")], From 84c0a1f2bdd283d2663c4932ce6d9b83502f97d6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 11:46:28 -0700 Subject: [PATCH 103/226] Rename and UT stubs --- .../OpenAIAssistant_FileManipulation.cs | 17 +- .../Agents/OpenAIAssistant_FileSearch.cs | 34 +-- dotnet/samples/Concepts/Concepts.csproj | 2 +- .../OpenAIServiceConfigurationExtensions.cs | 36 +++ .../Extensions/RunPollingOptionsExtensions.cs | 24 ++ .../Internal/AssistantRunOptionsFactory.cs | 12 + .../OpenAI/Internal/AssistantThreadActions.cs | 36 +-- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 40 +-- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 2 +- .../OpenAI/OpenAIAssistantExecutionOptions.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs | 88 ------- .../Agents/OpenAI/OpenAIVectorStoreBuilder.cs | 142 ----------- ...enAIServiceConfigurationExtensionsTests.cs | 15 ++ .../AssistantRunOptionsFactoryTests.cs | 15 ++ .../OpenAIAssistantInvocationOptionsTests.cs | 52 ++-- .../OpenAIThreadCreationOptionsTests.cs | 16 +- .../OpenAI/OpenAIVectorStoreBuilderTests.cs | 129 ---------- .../OpenAI/OpenAIVectorStoreTests.cs | 241 ------------------ .../OpenAI/RunPollingOptionsTests.cs | 52 ++++ 19 files changed, 249 insertions(+), 706 deletions(-) create mode 100644 dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs create mode 100644 dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs delete mode 100644 dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs delete mode 100644 dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 0272ed1eb8de..e325a672ac5e 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -4,6 +4,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Files; @@ -24,7 +25,9 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - FileClient fileClient = CreateFileClient(); + OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + + FileClient fileClient = config.CreateFileClient(); OpenAIFileInfo uploadFile = await fileClient.UploadFileAsync( @@ -36,7 +39,7 @@ await fileClient.UploadFileAsync( OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: GetOpenAIConfiguration(), + config, new() { CodeInterpterFileIds = [uploadFile.Id], @@ -86,14 +89,4 @@ private OpenAIServiceConfiguration GetOpenAIConfiguration() this.UseOpenAIConfig ? OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); - - private FileClient CreateFileClient() - { - OpenAIClient client = - this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : - new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); - - return client.GetFileClient(); - } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 157fb671ac13..091ce3f52a3f 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -3,6 +3,7 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Files; @@ -24,7 +25,9 @@ public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(out [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { - FileClient fileClient = CreateFileClient(); + OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + + FileClient fileClient = config.CreateFileClient(); OpenAIFileInfo uploadFile = await fileClient.UploadFileAsync( @@ -32,18 +35,19 @@ await fileClient.UploadFileAsync( "travelinfo.txt", FileUploadPurpose.Assistants); - VectorStore vectorStore = - await new OpenAIVectorStoreBuilder(GetOpenAIConfiguration()) - .AddFile(uploadFile.Id) - .CreateAsync(); - - OpenAIVectorStore openAIStore = new(vectorStore.Id, GetOpenAIConfiguration()); + VectorStoreClient vectorStoreClient = config.CreateVectorStoreClient(); + VectorStoreCreationOptions vectorStoreOptions = + new() + { + FileIds = [uploadFile.Id] + }; + VectorStore vectorStore = await vectorStoreClient.CreateVectorStoreAsync(vectorStoreOptions); // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: GetOpenAIConfiguration(), + config, new() { ModelName = this.Model, @@ -63,8 +67,8 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); - await openAIStore.DeleteAsync(); - await fileClient.DeleteFileAsync(uploadFile.Id); + await vectorStoreClient.DeleteVectorStoreAsync(vectorStore); + await fileClient.DeleteFileAsync(uploadFile); } // Local function to invoke agent and display the conversation messages. @@ -86,14 +90,4 @@ private OpenAIServiceConfiguration GetOpenAIConfiguration() this.UseOpenAIConfig ? OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); - - private FileClient CreateFileClient() - { - OpenAIClient client = - this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : - new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); - - return client.GetFileClient(); - } } diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 854d52f7eadb..5001100fae7d 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -8,7 +8,7 @@ false true - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs new file mode 100644 index 000000000000..79a60452d2fd --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft. All rights reserved. +using OpenAI; +using OpenAI.Files; +using OpenAI.VectorStores; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Extensions; + +/// +/// %%% +/// +public static class OpenAIServiceConfigurationExtensions +{ + /// + /// %%% + /// + /// + /// + public static FileClient CreateFileClient(this OpenAIServiceConfiguration configuration) + { + OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); + + return client.GetFileClient(); + } + + /// + /// %%% + /// + /// + /// + public static VectorStoreClient CreateVectorStoreClient(this OpenAIServiceConfiguration configuration) + { + OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); + + return client.GetVectorStoreClient(); + } +} diff --git a/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs new file mode 100644 index 000000000000..0bbe3d17ac97 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Extensions; + +internal static class RunPollingOptionsExtensions +{ + /// + /// %%% + /// + /// + /// + /// + /// + public async static Task WaitAsync(this RunPollingOptions pollingOptions, int pollIterationCount, CancellationToken cancellationToken) + { + await Task.Delay( + pollIterationCount >= 2 ? + pollingOptions.RunPollingInterval : + pollingOptions.RunPollingBackoff, + cancellationToken).ConfigureAwait(false); + } +} diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs new file mode 100644 index 000000000000..d28574bf2bcb --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -0,0 +1,12 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +internal class AssistantRunOptionsFactory +{ +} diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 4cde6904b44a..c5ebd60e6236 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using Azure; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; @@ -137,7 +138,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// The assistant agent to interact with the thread. /// The assistant client /// The thread identifier - /// Optional settings to utilize for the invocation + /// Options to utilize for the invocation /// The logger to utilize (might be agent or channel scoped) /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. @@ -145,7 +146,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist OpenAIAssistantAgent agent, AssistantClient client, string threadId, - OpenAIAssistantInvocationOptions? invocationSettings, + OpenAIAssistantInvocationOptions? invocationOptions, ILogger logger, [EnumeratorCancellation] CancellationToken cancellationToken) { @@ -156,7 +157,10 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId); - RunCreationOptions options = GenerateRunCreationOptions(agent, invocationSettings); + RunCreationOptions options = GenerateRunCreationOptions(agent.Definition, invocationOptions); + + options.ToolsOverride.AddRange(agent.Tools); // %%% + ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId); @@ -286,7 +290,7 @@ async Task PollRunStatusAsync() do { // Reduce polling frequency after a couple attempts - await Task.Delay(count >= 2 ? agent.PollingOptions.RunPollingInterval : agent.PollingOptions.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + await agent.PollingOptions.WaitAsync(count, cancellationToken).ConfigureAwait(false); ++count; #pragma warning disable CA1031 // Do not catch general exception types @@ -489,29 +493,27 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } - private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantAgent agent, OpenAIAssistantInvocationOptions? invocationSettings) + private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions) { - int? truncationMessageCount = ResolveExecutionSetting(invocationSettings?.TruncationMessageCount, agent.Definition.ExecutionOptions?.TruncationMessageCount); + int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount); RunCreationOptions options = new() { - MaxCompletionTokens = ResolveExecutionSetting(invocationSettings?.MaxCompletionTokens, agent.Definition.ExecutionOptions?.MaxCompletionTokens), - MaxPromptTokens = ResolveExecutionSetting(invocationSettings?.MaxPromptTokens, agent.Definition.ExecutionOptions?.MaxPromptTokens), - ModelOverride = invocationSettings?.ModelName, - NucleusSamplingFactor = ResolveExecutionSetting(invocationSettings?.TopP, agent.Definition.TopP), - ParallelToolCallsEnabled = ResolveExecutionSetting(invocationSettings?.ParallelToolCallsEnabled, agent.Definition.ExecutionOptions?.ParallelToolCallsEnabled), - ResponseFormat = ResolveExecutionSetting(invocationSettings?.EnableJsonResponse, agent.Definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, - Temperature = ResolveExecutionSetting(invocationSettings?.Temperature, agent.Definition.Temperature), + MaxCompletionTokens = ResolveExecutionSetting(invocationOptions?.MaxCompletionTokens, definition.ExecutionOptions?.MaxCompletionTokens), + MaxPromptTokens = ResolveExecutionSetting(invocationOptions?.MaxPromptTokens, definition.ExecutionOptions?.MaxPromptTokens), + ModelOverride = invocationOptions?.ModelName, + NucleusSamplingFactor = ResolveExecutionSetting(invocationOptions?.TopP, definition.TopP), + ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), + ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, + Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), //ToolConstraint // %%% TODO TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, }; - options.ToolsOverride.AddRange(agent.Tools); - - if (invocationSettings?.Metadata != null) + if (invocationOptions?.Metadata != null) { - foreach (var metadata in invocationSettings.Metadata) + foreach (var metadata in invocationOptions.Metadata) { options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty); } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 48c17a03f745..59df7c9d439d 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -16,7 +16,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// public sealed class OpenAIAssistantAgent : KernelAgent { - private const string SettingsMetadataKey = "__settings"; + private const string OptionsMetadataKey = "__run_options"; private readonly Assistant _assistant; private readonly AssistantClient _client; @@ -131,33 +131,33 @@ public static async Task RetrieveAsync( /// The to monitor for cancellation requests. The default is . /// The thread identifier public Task CreateThreadAsync(CancellationToken cancellationToken = default) - => this.CreateThreadAsync(settings: null, cancellationToken); + => this.CreateThreadAsync(options: null, cancellationToken); /// /// Create a new assistant thread. /// - /// %%% + /// %%% /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(OpenAIThreadCreationOptions? settings, CancellationToken cancellationToken = default) + public async Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) { - ThreadCreationOptions options = + ThreadCreationOptions createOptions = new() { - ToolResources = GenerateToolResources(settings?.VectorStoreId, settings?.CodeInterpterFileIds), + ToolResources = GenerateToolResources(options?.VectorStoreId, options?.CodeInterpterFileIds), }; //options.InitialMessages, // %%% TODO - if (settings?.Metadata != null) + if (options?.Metadata != null) { - foreach (KeyValuePair item in settings.Metadata) + foreach (KeyValuePair item in options.Metadata) { - options.Metadata[item.Key] = item.Value; + createOptions.Metadata[item.Key] = item.Value; } } - AssistantThread thread = await this._client.CreateThreadAsync(options, cancellationToken).ConfigureAwait(false); + AssistantThread thread = await this._client.CreateThreadAsync(createOptions, cancellationToken).ConfigureAwait(false); return thread.Id; } @@ -231,23 +231,23 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul public IAsyncEnumerable InvokeAsync( string threadId, CancellationToken cancellationToken = default) - => this.InvokeAsync(threadId, settings: null, cancellationToken); + => this.InvokeAsync(threadId, options: null, cancellationToken); /// /// Invoke the assistant on the specified thread. /// /// The thread identifier - /// Optional invocation settings + /// Optional invocation options /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. public async IAsyncEnumerable InvokeAsync( string threadId, - OpenAIAssistantInvocationOptions? settings, + OpenAIAssistantInvocationOptions? options, [EnumeratorCancellation] CancellationToken cancellationToken = default) { this.ThrowIfDeleted(); - await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, settings, this.Logger, cancellationToken).ConfigureAwait(false)) + await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, options, this.Logger, cancellationToken).ConfigureAwait(false)) { if (isVisible) { @@ -309,11 +309,11 @@ private OpenAIAssistantAgent( private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model) { - OpenAIAssistantExecutionOptions? settings = null; + OpenAIAssistantExecutionOptions? options = null; - if (model.Metadata.TryGetValue(SettingsMetadataKey, out string? settingsJson)) + if (model.Metadata.TryGetValue(OptionsMetadataKey, out string? optionsJson)) { - settings = JsonSerializer.Deserialize(settingsJson); + options = JsonSerializer.Deserialize(optionsJson); } IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds; @@ -335,7 +335,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, VectorStoreId = vectorStoreId, - ExecutionOptions = settings, + ExecutionOptions = options, }; } @@ -363,8 +363,8 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss if (definition.ExecutionOptions != null) { - string settingsJson = JsonSerializer.Serialize(definition.ExecutionOptions); - assistantCreationOptions.Metadata[SettingsMetadataKey] = settingsJson; + string optionsJson = JsonSerializer.Serialize(definition.ExecutionOptions); + assistantCreationOptions.Metadata[OptionsMetadataKey] = optionsJson; } if (definition.EnableCodeInterpreter) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 49a808bcf356..8531178543e4 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -31,7 +31,7 @@ protected override async Task ReceiveAsync(IEnumerable histo { agent.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationSettings: null, this.Logger, cancellationToken); + return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationOptions: null, this.Logger, cancellationToken); } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index 48f327a46489..9208d8184d82 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -5,7 +5,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// Defines agent execution options for each invocation. /// /// -/// These options are persisted as a single entry of the agent's metadata with key: "__settings" +/// These options are persisted as a single entry of the agent's metadata with key: "__run_options" /// public sealed class OpenAIAssistantExecutionOptions { diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs deleted file mode 100644 index a882729cde14..000000000000 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStore.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; -using OpenAI; -using OpenAI.VectorStores; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Supports management operations for a >. -/// -public sealed class OpenAIVectorStore -{ - private readonly VectorStoreClient _client; - - /// - /// The identifier of the targeted vector store - /// - public string VectorStoreId { get; } - - /// - /// List all vector stores. - /// - /// Configuration for accessing the vector-store service. - /// The to monitor for cancellation requests. The default is . - /// An enumeration of models. - public static IAsyncEnumerable GetVectorStoresAsync(OpenAIServiceConfiguration config, CancellationToken cancellationToken = default) - { - OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); - VectorStoreClient client = openAIClient.GetVectorStoreClient(); - - return client.GetVectorStoresAsync(ListOrder.NewestFirst, cancellationToken); - } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier of the targeted vector store - /// Configuration for accessing the vector-store service. - public OpenAIVectorStore(string vectorStoreId, OpenAIServiceConfiguration config) - { - OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); - this._client = openAIClient.GetVectorStoreClient(); - - this.VectorStoreId = vectorStoreId; - } - - /// - /// Add a file from the vector store. - /// - /// The file to add, by identifier. - /// The to monitor for cancellation requests. The default is . - /// - public async Task AddFileAsync(string fileId, CancellationToken cancellationToken = default) => - await this._client.AddFileToVectorStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); - - /// - /// Deletes the entire vector store. - /// - /// The to monitor for cancellation requests. The default is . - /// - public async Task DeleteAsync(CancellationToken cancellationToken = default) => - await this._client.DeleteVectorStoreAsync(this.VectorStoreId, cancellationToken).ConfigureAwait(false); - - /// - /// List the files (by identifier) in the vector store. - /// - /// The to monitor for cancellation requests. The default is . - /// - public async IAsyncEnumerable GetFilesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (VectorStoreFileAssociation file in this._client.GetFileAssociationsAsync(this.VectorStoreId, ListOrder.NewestFirst, filter: null, cancellationToken).ConfigureAwait(false)) - { - yield return file.FileId; - } - } - - /// - /// Remove a file from the vector store. - /// - /// The file to remove, by identifier. - /// The to monitor for cancellation requests. The default is . - /// - public async Task RemoveFileAsync(string fileId, CancellationToken cancellationToken = default) => - await this._client.RemoveFileFromStoreAsync(this.VectorStoreId, fileId, cancellationToken).ConfigureAwait(false); -} diff --git a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs b/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs deleted file mode 100644 index 65e9bf5c7496..000000000000 --- a/dotnet/src/Agents/OpenAI/OpenAIVectorStoreBuilder.cs +++ /dev/null @@ -1,142 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using OpenAI; -using OpenAI.VectorStores; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Fluent builder for creating a new . -/// -/// Configuration for accessing the vector-store service. -public sealed class OpenAIVectorStoreBuilder(OpenAIServiceConfiguration config) -{ - private string? _name; - private FileChunkingStrategy? _chunkingStrategy; - private VectorStoreExpirationPolicy? _expirationPolicy; - private readonly List _fileIds = []; - private readonly Dictionary _metadata = []; - - /// - /// Added a file (by identifier) to the vector store. - /// - /// - public OpenAIVectorStoreBuilder AddFile(string fileId) - { - this._fileIds.Add(fileId); - - return this; - } - - /// - /// Added files (by identifier) to the vector store. - /// - /// - public OpenAIVectorStoreBuilder AddFiles(string[] fileIds) - { - this._fileIds.AddRange(fileIds); - - return this; - } - - /// - /// Define the vector store chunking strategy (if not default). - /// - /// The maximum number of tokens in each chunk. - /// The number of tokens that overlap between chunks. - public OpenAIVectorStoreBuilder WithChunkingStrategy(int maxTokensPerChunk, int overlappingTokenCount) - { - this._chunkingStrategy = FileChunkingStrategy.CreateStaticStrategy(maxTokensPerChunk, overlappingTokenCount); - - return this; - } - - /// - /// The number of days of from the last use until vector store will expire. - /// - /// The duration (in days) from the last usage. - public OpenAIVectorStoreBuilder WithExpiration(TimeSpan duration) - { - this._expirationPolicy = new VectorStoreExpirationPolicy(VectorStoreExpirationAnchor.LastActiveAt, duration.Days); - - return this; - } - - /// - /// Adds a single key/value pair to the metadata. - /// - /// The metadata key - /// The metadata value - /// - /// The metadata is a set of up to 16 key/value pairs that can be attached to an agent, used for - /// storing additional information about that object in a structured format.Keys - /// may be up to 64 characters in length and values may be up to 512 characters in length. - /// > - public OpenAIVectorStoreBuilder WithMetadata(string key, string value) - { - this._metadata[key] = value; - - return this; - } - - /// - /// A set of up to 16 key/value pairs that can be attached to an agent, used for - /// storing additional information about that object in a structured format.Keys - /// may be up to 64 characters in length and values may be up to 512 characters in length. - /// - /// The metadata - public OpenAIVectorStoreBuilder WithMetadata(IDictionary metadata) - { - foreach (KeyValuePair item in this._metadata) - { - this._metadata[item.Key] = item.Value; - } - - return this; - } - - /// - /// Defines the name of the vector store when not anonymous. - /// - /// The store name. - public OpenAIVectorStoreBuilder WithName(string name) - { - this._name = name; - - return this; - } - - /// - /// Creates a as defined. - /// - /// The to monitor for cancellation requests. The default is . - public async Task CreateAsync(CancellationToken cancellationToken = default) - { - OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); - VectorStoreClient client = openAIClient.GetVectorStoreClient(); - - VectorStoreCreationOptions options = - new() - { - FileIds = this._fileIds, - ChunkingStrategy = this._chunkingStrategy, - ExpirationPolicy = this._expirationPolicy, - Name = this._name, - }; - - if (this._metadata != null) - { - foreach (KeyValuePair item in this._metadata) - { - options.Metadata.Add(item.Key, item.Value); - } - } - - VectorStore store = await client.CreateVectorStoreAsync(options, cancellationToken).ConfigureAwait(false); - - return store; - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs new file mode 100644 index 000000000000..69767ee42410 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions; + +/// +/// %%% +/// +public class OpenAIServiceConfigurationExtensionsTests +{ +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs new file mode 100644 index 000000000000..991cbcb2d86f --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; + +/// +/// %%% +/// +public class AssistantRunOptionsFactoryTests +{ +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs index 8a8669e30f07..1d63a6e2e9c0 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs @@ -14,30 +14,30 @@ public class OpenAIAssistantInvocationOptionsTests /// Verify initial state. /// [Fact] - public void OpenAIAssistantInvocationSettingsInitialState() + public void OpenAIAssistantInvocationOptionsInitialState() { - OpenAIAssistantInvocationOptions settings = new(); + OpenAIAssistantInvocationOptions options = new(); - Assert.Null(settings.ModelName); - Assert.Null(settings.Metadata); - Assert.Null(settings.Temperature); - Assert.Null(settings.TopP); - Assert.Null(settings.ParallelToolCallsEnabled); - Assert.Null(settings.MaxCompletionTokens); - Assert.Null(settings.MaxPromptTokens); - Assert.Null(settings.TruncationMessageCount); - Assert.Null(settings.EnableJsonResponse); - Assert.False(settings.EnableCodeInterpreter); - Assert.False(settings.EnableFileSearch); + Assert.Null(options.ModelName); + Assert.Null(options.Metadata); + Assert.Null(options.Temperature); + Assert.Null(options.TopP); + Assert.Null(options.ParallelToolCallsEnabled); + Assert.Null(options.MaxCompletionTokens); + Assert.Null(options.MaxPromptTokens); + Assert.Null(options.TruncationMessageCount); + Assert.Null(options.EnableJsonResponse); + Assert.False(options.EnableCodeInterpreter); + Assert.False(options.EnableFileSearch); } /// /// Verify initialization. /// [Fact] - public void OpenAIAssistantInvocationSettingsAssignment() + public void OpenAIAssistantInvocationOptionsAssignment() { - OpenAIAssistantInvocationOptions settings = + OpenAIAssistantInvocationOptions options = new() { ModelName = "testmodel", @@ -53,16 +53,16 @@ public void OpenAIAssistantInvocationSettingsAssignment() EnableFileSearch = true, }; - Assert.Equal("testmodel", settings.ModelName); - Assert.Equal(2, settings.Temperature); - Assert.Equal(0, settings.TopP); - Assert.Equal(1000, settings.MaxCompletionTokens); - Assert.Equal(1000, settings.MaxPromptTokens); - Assert.Equal(12, settings.TruncationMessageCount); - Assert.False(settings.ParallelToolCallsEnabled); - Assert.Single(settings.Metadata); - Assert.True(settings.EnableCodeInterpreter); - Assert.True(settings.EnableJsonResponse); - Assert.True(settings.EnableFileSearch); + Assert.Equal("testmodel", options.ModelName); + Assert.Equal(2, options.Temperature); + Assert.Equal(0, options.TopP); + Assert.Equal(1000, options.MaxCompletionTokens); + Assert.Equal(1000, options.MaxPromptTokens); + Assert.Equal(12, options.TruncationMessageCount); + Assert.False(options.ParallelToolCallsEnabled); + Assert.Single(options.Metadata); + Assert.True(options.EnableCodeInterpreter); + Assert.True(options.EnableJsonResponse); + Assert.True(options.EnableFileSearch); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index 545a338f5217..a90050623599 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -16,22 +16,22 @@ public class OpenAIThreadCreationOptionsTests /// Verify initial state. /// [Fact] - public void OpenAIThreadCreationSettingsInitialState() + public void OpenAIThreadCreationOptionsInitialState() { - OpenAIThreadCreationOptions settings = new(); + OpenAIThreadCreationOptions options = new(); - Assert.Null(settings.Messages); - Assert.Null(settings.Metadata); - Assert.Null(settings.VectorStoreId); - Assert.Null(settings.CodeInterpterFileIds); - Assert.False(settings.EnableCodeInterpreter); + Assert.Null(options.Messages); + Assert.Null(options.Metadata); + Assert.Null(options.VectorStoreId); + Assert.Null(options.CodeInterpterFileIds); + Assert.False(options.EnableCodeInterpreter); } /// /// Verify initialization. /// [Fact] - public void OpenAIThreadCreationSettingsAssignment() + public void OpenAIThreadCreationOptionsAssignment() { OpenAIThreadCreationOptions definition = new() diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs deleted file mode 100644 index dd6734e8cb71..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreBuilderTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.OpenAI; -using OpenAI.VectorStores; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI; - -/// -/// Unit testing of . -/// -public sealed class OpenAIVectorStoreBuilderTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreBuilderEmptyAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateStore); - - VectorStore store = - await new OpenAIVectorStoreBuilder(this.CreateTestConfiguration()) - .CreateAsync(); - - Assert.NotNull(store); - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreBuilderFluentAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateStore); - - Dictionary metadata = new() { { "key2", "value2" } }; - - VectorStore store = - await new OpenAIVectorStoreBuilder(this.CreateTestConfiguration()) - .WithName("my_vector_store") - .AddFile("#file_1") - .AddFiles(["#file_2", "#file_3"]) - .AddFiles(["#file_4", "#file_5"]) - .WithChunkingStrategy(1000, 400) - .WithExpiration(TimeSpan.FromDays(30)) - .WithMetadata("key1", "value1") - .WithMetadata(metadata) - .CreateAsync(); - - Assert.NotNull(store); - } - - /// - public void Dispose() - { - this._messageHandlerStub.Dispose(); - this._httpClient.Dispose(); - } - - /// - /// Initializes a new instance of the class. - /// - public OpenAIVectorStoreBuilderTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); - } - - private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) - => targetAzure ? - OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : - OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); - - private void SetupResponse(HttpStatusCode statusCode, string content) - { - this._messageHandlerStub.ResponseToReturn = - new(statusCode) - { - Content = new StringContent(content) - }; - } - - private void SetupResponses(HttpStatusCode statusCode, params string[] content) - { - foreach (var item in content) - { -#pragma warning disable CA2000 // Dispose objects before losing scope - this._messageHandlerStub.ResponseQueue.Enqueue( - new(statusCode) - { - Content = new StringContent(item) - }); -#pragma warning restore CA2000 // Dispose objects before losing scope - } - } - - private static class ResponseContent - { - public const string CreateStore = - """ - { - "id": "vs_123", - "object": "vector_store", - "created_at": 1698107661, - "usage_bytes": 123456, - "last_active_at": 1698107661, - "name": "my_vector_store", - "status": "completed", - "file_counts": { - "in_progress": 0, - "completed": 5, - "cancelled": 0, - "failed": 0, - "total": 5 - }, - "metadata": {}, - "last_used_at": 1698107661 - } - """; - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs deleted file mode 100644 index dbcdef23f8b7..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIVectorStoreTests.cs +++ /dev/null @@ -1,241 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Agents.OpenAI; -using OpenAI.VectorStores; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI; - -/// -/// Unit testing of . -/// -public sealed class OpenAIVectorStoreTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - - /// - /// %%% - /// - [Fact] - public void VerifyOpenAIVectorStoreInitialization() - { - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); - Assert.Equal("#vs1", store.VectorStoreId); - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreDeleteAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteStore); - - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); - bool isDeleted = await store.DeleteAsync(); - - Assert.True(isDeleted); - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreListAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.ListStores); - - VectorStore[] stores = await OpenAIVectorStore.GetVectorStoresAsync(this.CreateTestConfiguration()).ToArrayAsync(); - - Assert.Equal(2, stores.Length); - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreAddFileAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.AddFile); - - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); - await store.AddFileAsync("#file_1"); - - // %%% VERIFY - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreRemoveFileAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteFile); - - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); - bool isDeleted = await store.RemoveFileAsync("#file_1"); - - Assert.True(isDeleted); - } - - /// - /// %%% - /// - [Fact] - public async Task VerifyOpenAIVectorStoreGetFilesAsync() - { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.ListFiles); - - OpenAIVectorStore store = new("#vs1", this.CreateTestConfiguration()); - string[] files = await store.GetFilesAsync().ToArrayAsync(); - - Assert.Equal(2, files.Length); - } - - /// - public void Dispose() - { - this._messageHandlerStub.Dispose(); - this._httpClient.Dispose(); - } - - /// - /// Initializes a new instance of the class. - /// - public OpenAIVectorStoreTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, disposeHandler: false); - } - - private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) - => targetAzure ? - OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : - OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); - - private void SetupResponse(HttpStatusCode statusCode, string content) - { - this._messageHandlerStub.ResponseToReturn = - new(statusCode) - { - Content = new StringContent(content) - }; - } - - private void SetupResponses(HttpStatusCode statusCode, params string[] content) - { - foreach (var item in content) - { -#pragma warning disable CA2000 // Dispose objects before losing scope - this._messageHandlerStub.ResponseQueue.Enqueue( - new(statusCode) - { - Content = new StringContent(item) - }); -#pragma warning restore CA2000 // Dispose objects before losing scope - } - } - - private static class ResponseContent - { - public const string AddFile = - """ - { - "id": "#file_1", - "object": "vector_store.file", - "created_at": 1699061776, - "usage_bytes": 1234, - "vector_store_id": "vs_abcd", - "status": "completed", - "last_error": null - } - """; - - public const string DeleteFile = - """ - { - "id": "#file_1", - "object": "vector_store.file.deleted", - "deleted": true - } - """; - - public const string DeleteStore = - """ - { - "id": "vs_abc123", - "object": "vector_store.deleted", - "deleted": true - } - """; - - public const string ListFiles = - """ - { - "object": "list", - "data": [ - { - "id": "file-abc123", - "object": "vector_store.file", - "created_at": 1699061776, - "vector_store_id": "vs_abc123" - }, - { - "id": "file-abc456", - "object": "vector_store.file", - "created_at": 1699061776, - "vector_store_id": "vs_abc123" - } - ], - "first_id": "file-abc123", - "last_id": "file-abc456", - "has_more": false - } - """; - - public const string ListStores = - """ - { - "object": "list", - "data": [ - { - "id": "vs_abc123", - "object": "vector_store", - "created_at": 1699061776, - "name": "Support FAQ", - "bytes": 139920, - "file_counts": { - "in_progress": 0, - "completed": 3, - "failed": 0, - "cancelled": 0, - "total": 3 - } - }, - { - "id": "vs_abc456", - "object": "vector_store", - "created_at": 1699061776, - "name": "Support FAQ v2", - "bytes": 139920, - "file_counts": { - "in_progress": 0, - "completed": 3, - "failed": 0, - "cancelled": 0, - "total": 3 - } - } - ], - "first_id": "vs_abc123", - "last_id": "vs_abc456", - "has_more": false - } - """; - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs new file mode 100644 index 000000000000..e87688791df3 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class RunPollingOptionsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void RunPollingOptionsInitialState() + { + OpenAIThreadCreationOptions options = new(); + + Assert.Null(options.Messages); + Assert.Null(options.Metadata); + Assert.Null(options.VectorStoreId); + Assert.Null(options.CodeInterpterFileIds); + Assert.False(options.EnableCodeInterpreter); + } + + /// s + /// Verify initialization. + /// + [Fact] + public void RunPollingOptionsAssignment() + { + OpenAIThreadCreationOptions definition = + new() + { + Messages = [new ChatMessageContent(AuthorRole.User, "test")], + VectorStoreId = "#vs", + Metadata = new Dictionary() { { "a", "1" } }, + CodeInterpterFileIds = ["file1"], + EnableCodeInterpreter = true, + }; + + Assert.Single(definition.Messages); + Assert.Single(definition.Metadata); + Assert.Equal("#vs", definition.VectorStoreId); + Assert.Single(definition.CodeInterpterFileIds); + Assert.True(definition.EnableCodeInterpreter); + } +} From e09013b715bcff443bff8798ec6cbbc40b52639c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 11:51:51 -0700 Subject: [PATCH 104/226] Namespace --- .../samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs | 2 -- dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs | 2 -- 2 files changed, 4 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index e325a672ac5e..ee6c887c14db 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -1,12 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Text; -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI; using OpenAI.Files; using Resources; diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 091ce3f52a3f..9189214701c6 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI; using OpenAI.Files; using OpenAI.VectorStores; using Resources; From a48aa120c198ecdee74bf9669dbed78aeb95634f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 16:18:54 -0700 Subject: [PATCH 105/226] Checkpoint --- .../OpenAIServiceConfigurationExtensions.cs | 12 +- .../Extensions/RunPollingOptionsExtensions.cs | 24 -- .../Internal/AssistantMessageAdapter.cs | 56 +++++ .../Internal/AssistantRunOptionsFactory.cs | 47 +++- .../OpenAI/Internal/AssistantThreadActions.cs | 76 +------ .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 +- .../OpenAI/OpenAIThreadCreationOptions.cs | 5 - dotnet/src/Agents/OpenAI/RunPollingOptions.cs | 17 ++ .../Agents/UnitTests/Agents.UnitTests.csproj | 2 +- ...enAIServiceConfigurationExtensionsTests.cs | 36 ++- .../Internal/AssistantMessageAdapterTests.cs | 209 ++++++++++++++++++ .../OpenAIClientFactoryTests.cs | 2 +- .../OpenAIThreadCreationOptionsTests.cs | 3 - .../OpenAI/RunPollingOptionsTests.cs | 58 +++-- .../Contents/AnnotationContent.cs | 2 +- .../Contents/FileReferenceContent.cs | 2 +- .../Contents/MessageAttachmentContent.cs | 42 ++++ 17 files changed, 449 insertions(+), 148 deletions(-) delete mode 100644 dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs rename dotnet/src/Agents/UnitTests/OpenAI/{ => Internal}/OpenAIClientFactoryTests.cs (97%) create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs index 79a60452d2fd..76000128ce3a 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -6,15 +6,14 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI.Extensions; /// -/// %%% +/// Extension method for creating OpenAI clients from a . /// public static class OpenAIServiceConfigurationExtensions { /// - /// %%% + /// Provide a newly created based on the specified configuration. /// - /// - /// + /// The configuration public static FileClient CreateFileClient(this OpenAIServiceConfiguration configuration) { OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); @@ -23,10 +22,9 @@ public static FileClient CreateFileClient(this OpenAIServiceConfiguration config } /// - /// %%% + /// Provide a newly created based on the specified configuration. /// - /// - /// + /// The configuration public static VectorStoreClient CreateVectorStoreClient(this OpenAIServiceConfiguration configuration) { OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); diff --git a/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs deleted file mode 100644 index 0bbe3d17ac97..000000000000 --- a/dotnet/src/Agents/OpenAI/Extensions/RunPollingOptionsExtensions.cs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Threading; -using System.Threading.Tasks; - -namespace Microsoft.SemanticKernel.Agents.OpenAI.Extensions; - -internal static class RunPollingOptionsExtensions -{ - /// - /// %%% - /// - /// - /// - /// - /// - public async static Task WaitAsync(this RunPollingOptions pollingOptions, int pollIterationCount, CancellationToken cancellationToken) - { - await Task.Delay( - pollIterationCount >= 2 ? - pollingOptions.RunPollingInterval : - pollingOptions.RunPollingBackoff, - cancellationToken).ConfigureAwait(false); - } -} diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs new file mode 100644 index 000000000000..99aeb53f1ebe --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs @@ -0,0 +1,56 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using OpenAI.Assistants; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +internal static class AssistantMessageAdapter +{ + public static MessageCreationOptions CreateOptions(ChatMessageContent message) + { + MessageCreationOptions options = new(); + + if (message.Metadata != null) + { + foreach (var metadata in message.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value?.ToString() ?? string.Empty); + } + } + + return options; + } + + public static IEnumerable GetMessageContents(ChatMessageContent message, MessageCreationOptions options) + { + foreach (KernelContent content in message.Items) + { + if (content is TextContent textContent) + { + yield return MessageContent.FromText(content.ToString()); + } + else if (content is ImageContent imageContent) + { + if (imageContent.Uri != null) + { + yield return MessageContent.FromImageUrl(imageContent.Uri); + } + //else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) + //{ + // %%% BUG: https://github.com/openai/openai-dotnet/issues/135 + // URI does not accept the format used for `DataUri` + // Approach is inefficient anyway... + // yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); + //} + } + else if (content is MessageAttachmentContent attachmentContent) + { + options.Attachments.Add(new MessageCreationAttachment(attachmentContent.FileId, [ToolDefinition.CreateCodeInterpreter()])); // %%% TODO: Tool Type + } + else if (content is FileReferenceContent fileContent) + { + yield return MessageContent.FromImageFileId(fileContent.FileId); + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs index d28574bf2bcb..40b9ef329691 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -1,12 +1,49 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; -internal class AssistantRunOptionsFactory +internal static class AssistantRunOptionsFactory { + /// + /// %%% + /// + /// + /// + /// + public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions) + { + int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount); + + RunCreationOptions options = + new() + { + MaxCompletionTokens = ResolveExecutionSetting(invocationOptions?.MaxCompletionTokens, definition.ExecutionOptions?.MaxCompletionTokens), + MaxPromptTokens = ResolveExecutionSetting(invocationOptions?.MaxPromptTokens, definition.ExecutionOptions?.MaxPromptTokens), + ModelOverride = invocationOptions?.ModelName, + NucleusSamplingFactor = ResolveExecutionSetting(invocationOptions?.TopP, definition.TopP), + ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), + ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, + Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), + //ToolConstraint // %%% TODO ISSUE + TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, + }; + + if (invocationOptions?.Metadata != null) + { + foreach (var metadata in invocationOptions.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty); + } + } + + return options; + } + + private static TValue? ResolveExecutionSetting(TValue? setting, TValue? agentSetting) where TValue : struct + => + setting.HasValue && (!agentSetting.HasValue || !EqualityComparer.Default.Equals(setting.Value, agentSetting.Value)) ? + setting.Value : + null; } diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index c5ebd60e6236..b3ceaee7ac77 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -10,6 +10,7 @@ using Azure; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; @@ -51,45 +52,13 @@ public static async Task CreateMessageAsync(AssistantClient client, string threa return; } - MessageCreationOptions options = - new() - { - //Role = message.Role.ToMessageRole(), // %%% BUG: ASSIGNABLE (Allow assistant or user) - }; - - if (message.Metadata != null) - { - foreach (var metadata in message.Metadata) - { - options.Metadata.Add(metadata.Key, metadata.Value?.ToString() ?? string.Empty); - } - } + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); await client.CreateMessageAsync( threadId, - GetMessageContents(), + AssistantMessageAdapter.GetMessageContents(message, options), options, cancellationToken).ConfigureAwait(false); - - IEnumerable GetMessageContents() - { - foreach (KernelContent content in message.Items) - { - if (content is TextContent textContent) - { - yield return MessageContent.FromText(content.ToString()); - } - else if (content is ImageContent imageContent && imageContent.Data.HasValue) - { - yield return MessageContent.FromImageUrl( - imageContent.Uri ?? new Uri(Convert.ToBase64String(imageContent.Data.Value.ToArray()))); - } - else if (content is FileReferenceContent fileContent) - { - options.Attachments.Add(new MessageCreationAttachment(fileContent.FileId, [new CodeInterpreterToolDefinition()])); - } - } - } } /// @@ -157,7 +126,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId); - RunCreationOptions options = GenerateRunCreationOptions(agent.Definition, invocationOptions); + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions); options.ToolsOverride.AddRange(agent.Tools); // %%% @@ -290,7 +259,7 @@ async Task PollRunStatusAsync() do { // Reduce polling frequency after a couple attempts - await agent.PollingOptions.WaitAsync(count, cancellationToken).ConfigureAwait(false); + await Task.Delay(agent.PollingOptions.GetPollingInterval(count), cancellationToken).ConfigureAwait(false); ++count; #pragma warning disable CA1031 // Do not catch general exception types @@ -492,39 +461,4 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } - - private static RunCreationOptions GenerateRunCreationOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions) - { - int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount); - - RunCreationOptions options = - new() - { - MaxCompletionTokens = ResolveExecutionSetting(invocationOptions?.MaxCompletionTokens, definition.ExecutionOptions?.MaxCompletionTokens), - MaxPromptTokens = ResolveExecutionSetting(invocationOptions?.MaxPromptTokens, definition.ExecutionOptions?.MaxPromptTokens), - ModelOverride = invocationOptions?.ModelName, - NucleusSamplingFactor = ResolveExecutionSetting(invocationOptions?.TopP, definition.TopP), - ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), - ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, - Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), - //ToolConstraint // %%% TODO - TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, - }; - - if (invocationOptions?.Metadata != null) - { - foreach (var metadata in invocationOptions.Metadata) - { - options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty); - } - } - - return options; - } - - private static TValue? ResolveExecutionSetting(TValue? setting, TValue? agentSetting) where TValue : struct - => - setting.HasValue && (!agentSetting.HasValue || !EqualityComparer.Default.Equals(setting.Value, agentSetting.Value)) ? - setting.Value : - null; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 59df7c9d439d..294330d30da5 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -369,12 +369,12 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss if (definition.EnableCodeInterpreter) { - assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); + assistantCreationOptions.Tools.Add(ToolDefinition.CreateCodeInterpreter()); } if (!string.IsNullOrWhiteSpace(definition.VectorStoreId)) { - assistantCreationOptions.Tools.Add(new FileSearchToolDefinition()); + assistantCreationOptions.Tools.Add(ToolDefinition.CreateFileSearch()); } return assistantCreationOptions; diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs index 56b3aa167284..4f596e000153 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs @@ -13,11 +13,6 @@ public sealed class OpenAIThreadCreationOptions /// public IReadOnlyList? CodeInterpterFileIds { get; init; } - /// - /// Set if code-interpreter is enabled. - /// - public bool EnableCodeInterpreter { get; init; } - /// /// Optional messages to initialize thread with.. /// diff --git a/dotnet/src/Agents/OpenAI/RunPollingOptions.cs b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs index 7dd71cfdd8b2..756ba689131c 100644 --- a/dotnet/src/Agents/OpenAI/RunPollingOptions.cs +++ b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs @@ -18,6 +18,11 @@ public sealed class RunPollingOptions /// public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1); + /// + /// The default number of polling iterations before using . + /// + public static int DefaultPollingBackoffThreshold { get; } = 2; + /// /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. /// @@ -33,8 +38,20 @@ public sealed class RunPollingOptions /// public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff; + /// + /// The number of polling iterations before using . + /// + public int RunPollingBackoffThreshold { get; set; } = DefaultPollingBackoffThreshold; + /// /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. /// public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay; + + /// + /// Gets the polling interval for the specified iteration count. + /// + /// The number of polling iterations already attempted + public TimeSpan GetPollingInterval(int iterationCount) => + iterationCount > this.RunPollingBackoffThreshold ? this.RunPollingBackoff : this.RunPollingInterval; } diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index d46a4ee0cd1e..1bff8161a9b5 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -8,7 +8,7 @@ true false 12 - $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110 + $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110;OPENAI001 diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs index 69767ee42410..288c0b2304f2 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs @@ -1,15 +1,41 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; +using OpenAI.Files; +using OpenAI.VectorStores; +using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions; /// -/// %%% +/// Unit testing of . /// public class OpenAIServiceConfigurationExtensionsTests { + /// + /// Verify can produce a + /// + [Fact] + public void OpenAIServiceConfigurationExtensionsCreateFileClientTest() + { + OpenAIServiceConfiguration configOpenAI = OpenAIServiceConfiguration.ForOpenAI("key", new Uri("https://localhost")); + Assert.IsType(configOpenAI.CreateFileClient()); + + OpenAIServiceConfiguration configAzure = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); + Assert.IsType(configOpenAI.CreateFileClient()); + } + + /// + /// Verify can produce a + /// + [Fact] + public void OpenAIServiceConfigurationExtensionsCreateVectorStoreTest() + { + OpenAIServiceConfiguration configOpenAI = OpenAIServiceConfiguration.ForOpenAI("key", new Uri("https://localhost")); + Assert.IsType(configOpenAI.CreateVectorStoreClient()); + + OpenAIServiceConfiguration configAzure = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); + Assert.IsType(configOpenAI.CreateVectorStoreClient()); + } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs new file mode 100644 index 000000000000..b8f93db1107b --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs @@ -0,0 +1,209 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; + +/// +/// Unit testing of . +/// +public class AssistantMessageAdapterTests +{ + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsDefault() + { + // Setup message with null metadata + ChatMessageContent message = new(AuthorRole.User, "test"); + + // Create options + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + + // Validate + Assert.NotNull(options); + Assert.Empty(options.Metadata); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty() + { + // Setup message with empty metadata + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = new Dictionary() + }; + + // Create options + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + + // Validate + Assert.NotNull(options); + Assert.Empty(options.Metadata); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() + { + // Setup message with metadata + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = + new Dictionary() + { + { "a", 1 }, + { "b", "2" }, + } + }; + + // Create options + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + + // Validate + Assert.NotNull(options); + Assert.NotEmpty(options.Metadata); + Assert.Equal(2, options.Metadata.Count); + Assert.Equal("1", options.Metadata["a"]); + Assert.Equal("2", options.Metadata["b"]); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() + { + // Setup message with null metadata value + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = + new Dictionary() + { + { "a", null }, + { "b", "2" }, + } + }; + + // Create options + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + + // Validate + Assert.NotNull(options); + Assert.NotEmpty(options.Metadata); + Assert.Equal(2, options.Metadata.Count); + Assert.Equal(string.Empty, options.Metadata["a"]); + Assert.Equal("2", options.Metadata["b"]); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageContentsWithText() + { + ChatMessageContent message = new(AuthorRole.User, items: [new TextContent("test")]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().Text); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() + { + ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new Uri("https://localhost/myimage.png"))]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageUrl); + } + + /// + /// Verify options creation. + /// + [Fact(Skip = "API bug with data Uri construction")] + public void VerifyAssistantMessageAdapterGetMessageWithImageData() + { + ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageUrl); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithImageFile() + { + ChatMessageContent message = new(AuthorRole.User, items: [new FileReferenceContent("file-id")]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageFileId); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithAttachment() + { + ChatMessageContent message = new(AuthorRole.User, items: [new MessageAttachmentContent("file-id")]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(options.Attachments); + Assert.Single(options.Attachments); + Assert.NotNull(options.Attachments.Single().FileId); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithAll() + { + ChatMessageContent message = + new( + AuthorRole.User, + items: + [ + new TextContent("test"), + new ImageContent(new Uri("https://localhost/myimage.png")), // %%% + new FileReferenceContent("file-id"), + new MessageAttachmentContent("file-id"), + ]); + MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + Assert.NotNull(contents); + Assert.Equal(3, contents.Length); + Assert.NotNull(options.Attachments); + Assert.Single(options.Attachments); + Assert.NotNull(options.Attachments.Single().FileId); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs similarity index 97% rename from dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs rename to dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs index 46b45e419213..a2ce548d5e0e 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs @@ -7,7 +7,7 @@ using OpenAI; using Xunit; -namespace SemanticKernel.Agents.UnitTests.OpenAI; +namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; /// /// Unit testing of . diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index a90050623599..ba4992304227 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -24,7 +24,6 @@ public void OpenAIThreadCreationOptionsInitialState() Assert.Null(options.Metadata); Assert.Null(options.VectorStoreId); Assert.Null(options.CodeInterpterFileIds); - Assert.False(options.EnableCodeInterpreter); } /// @@ -40,13 +39,11 @@ public void OpenAIThreadCreationOptionsAssignment() VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, CodeInterpterFileIds = ["file1"], - EnableCodeInterpreter = true, }; Assert.Single(definition.Messages); Assert.Single(definition.Metadata); Assert.Equal("#vs", definition.VectorStoreId); Assert.Single(definition.CodeInterpterFileIds); - Assert.True(definition.EnableCodeInterpreter); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs index e87688791df3..eece5208ed1b 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using Microsoft.SemanticKernel; +using System; using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI; @@ -16,37 +14,53 @@ public class RunPollingOptionsTests /// Verify initial state. /// [Fact] - public void RunPollingOptionsInitialState() + public void RunPollingOptionsInitialStateTest() { - OpenAIThreadCreationOptions options = new(); + RunPollingOptions options = new(); - Assert.Null(options.Messages); - Assert.Null(options.Metadata); - Assert.Null(options.VectorStoreId); - Assert.Null(options.CodeInterpterFileIds); - Assert.False(options.EnableCodeInterpreter); + Assert.Equal(RunPollingOptions.DefaultPollingInterval, options.RunPollingInterval); + Assert.Equal(RunPollingOptions.DefaultPollingBackoff, options.RunPollingBackoff); + Assert.Equal(RunPollingOptions.DefaultMessageSynchronizationDelay, options.MessageSynchronizationDelay); + Assert.Equal(RunPollingOptions.DefaultPollingBackoffThreshold, options.RunPollingBackoffThreshold); } /// s /// Verify initialization. /// [Fact] - public void RunPollingOptionsAssignment() + public void RunPollingOptionsAssignmentTest() { - OpenAIThreadCreationOptions definition = + RunPollingOptions options = new() { - Messages = [new ChatMessageContent(AuthorRole.User, "test")], - VectorStoreId = "#vs", - Metadata = new Dictionary() { { "a", "1" } }, - CodeInterpterFileIds = ["file1"], - EnableCodeInterpreter = true, + RunPollingInterval = TimeSpan.FromSeconds(3), + RunPollingBackoff = TimeSpan.FromSeconds(4), + RunPollingBackoffThreshold = 8, + MessageSynchronizationDelay = TimeSpan.FromSeconds(5), }; - Assert.Single(definition.Messages); - Assert.Single(definition.Metadata); - Assert.Equal("#vs", definition.VectorStoreId); - Assert.Single(definition.CodeInterpterFileIds); - Assert.True(definition.EnableCodeInterpreter); + Assert.Equal(3, options.RunPollingInterval.TotalSeconds); + Assert.Equal(4, options.RunPollingBackoff.TotalSeconds); + Assert.Equal(5, options.MessageSynchronizationDelay.TotalSeconds); + Assert.Equal(8, options.RunPollingBackoffThreshold); + } + + + /// s + /// Verify initialization. + /// + [Fact] + public void RunPollingOptionsGetIntervalTest() + { + RunPollingOptions options = + new() + { + RunPollingInterval = TimeSpan.FromSeconds(3), + RunPollingBackoff = TimeSpan.FromSeconds(4), + RunPollingBackoffThreshold = 8, + }; + + Assert.Equal(options.RunPollingInterval, options.GetPollingInterval(8)); + Assert.Equal(options.RunPollingBackoff, options.GetPollingInterval(9)); } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs index f9e6f9f3d71f..fd27b35a4b0f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs @@ -44,7 +44,7 @@ public AnnotationContent() /// Initializes a new instance of the class. /// /// The model ID used to generate the content. - /// Inner content, + /// Inner content /// Additional metadata public AnnotationContent( string? modelId = null, diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs index 16ac0cd7828e..925d74d0c731 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs @@ -28,7 +28,7 @@ public FileReferenceContent() /// /// The identifier of the referenced file. /// The model ID used to generate the content. - /// Inner content, + /// Inner content /// Additional metadata public FileReferenceContent( string fileId, diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs new file mode 100644 index 000000000000..6f4f55f9af4a --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel; + +/// +/// Content type to support message attachment. +/// +[Experimental("SKEXP0110")] +public class MessageAttachmentContent : FileReferenceContent +{ + /// + /// The associated tool. + /// + public string Tool { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public MessageAttachmentContent() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The identifier of the referenced file. + /// The model ID used to generate the content. + /// Inner content + /// Additional metadata + public MessageAttachmentContent( + string fileId, + string? modelId = null, + object? innerContent = null, + IReadOnlyDictionary? metadata = null) + : base(fileId, modelId, innerContent, metadata) + { + // %%% TOOL TYPE + } +} From 991dd59b3968236aaa4e64e2079c7d692b2473f2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 16:22:33 -0700 Subject: [PATCH 106/226] Blank line --- dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs index eece5208ed1b..9ec3567c0987 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs @@ -45,7 +45,6 @@ public void RunPollingOptionsAssignmentTest() Assert.Equal(8, options.RunPollingBackoffThreshold); } - /// s /// Verify initialization. /// From 6bb0613642556148334605850d2ab7d3b1e9cf4c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 16:31:39 -0700 Subject: [PATCH 107/226] Checkpoint --- .../Concepts/Agents/OpenAIAssistant_FileManipulation.cs | 1 - dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs | 1 - .../OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs | 3 ++- dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs | 2 +- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 4 +--- dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs | 2 +- .../OpenAI/Logging/AssistantThreadActionsLogMessages.cs | 1 + dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 1 + dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs | 1 + .../UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs | 2 +- .../Extensions/OpenAIServiceConfigurationExtensionsTests.cs | 1 - .../UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs | 1 + 12 files changed, 10 insertions(+), 10 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index ee6c887c14db..ff4f26a92ba6 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -3,7 +3,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Files; using Resources; diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 9189214701c6..9fe775e0b50e 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -2,7 +2,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Files; using OpenAI.VectorStores; diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs index 76000128ce3a..29e5e36c6f4c 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -1,9 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using OpenAI; using OpenAI.Files; using OpenAI.VectorStores; -namespace Microsoft.SemanticKernel.Agents.OpenAI.Extensions; +namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// Extension method for creating OpenAI clients from a . diff --git a/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs index 1fb0698ffa77..d017fb403f23 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs @@ -2,7 +2,7 @@ using Azure.Core; using Azure.Core.Pipeline; -namespace Microsoft.SemanticKernel.Agents.OpenAI; +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; /// /// Helper class to inject headers into Azure SDK HTTP pipeline diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index b3ceaee7ac77..289a1074795e 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -9,13 +8,12 @@ using System.Threading.Tasks; using Azure; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; -namespace Microsoft.SemanticKernel.Agents.OpenAI; +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; /// /// Actions associated with an Open Assistant thread. diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index ddfd8aef8c31..0c89ea1b3c2b 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -6,7 +6,7 @@ using Microsoft.SemanticKernel.Http; using OpenAI; -namespace Microsoft.SemanticKernel.Agents.OpenAI; +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; internal static class OpenAIClientFactory { diff --git a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs index 288abadde2dd..3a39c314c5c3 100644 --- a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs +++ b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 294330d30da5..336e7ed001d1 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using OpenAI; using OpenAI.Assistants; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 8531178543e4..dabc0bae424b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs index 0a4076055fae..3c2945ad0fb9 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs @@ -2,7 +2,7 @@ using System.Linq; using Azure.Core; using Azure.Core.Pipeline; -using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Azure; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs index 288c0b2304f2..650503a94e5e 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.Agents.OpenAI.Extensions; using OpenAI.Files; using OpenAI.VectorStores; using Xunit; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs index a2ce548d5e0e..7fa2ebca23c8 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs @@ -3,6 +3,7 @@ using System.Net.Http; using Azure.Core; using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Moq; using OpenAI; using Xunit; From 1709575e8e62381e586c27aacfe32bd88edae44a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 17:05:15 -0700 Subject: [PATCH 108/226] Checkpoint --- .../Internal/AssistantRunOptionsFactory.cs | 9 +- .../OpenAI/Internal/AssistantThreadActions.cs | 2 +- .../OpenAI/OpenAIAssistantExecutionOptions.cs | 3 - .../OpenAIAssistantInvocationOptions.cs | 3 - .../Internal/AssistantMessageAdapterTests.cs | 2 +- .../AssistantRunOptionsFactoryTests.cs | 119 +++++++++++++++++- 6 files changed, 120 insertions(+), 18 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs index 40b9ef329691..97132677e8c7 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -7,11 +7,10 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; internal static class AssistantRunOptionsFactory { /// - /// %%% + /// Produce by reconciling and . /// - /// - /// - /// + /// The assistant definition + /// The run specific options public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions) { int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount); @@ -26,7 +25,7 @@ public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition defin ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), - //ToolConstraint // %%% TODO ISSUE + //ToolConstraint - Not Supported: https://github.com/microsoft/semantic-kernel/issues/6795 TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, }; diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 289a1074795e..cea8dbaf9664 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -126,7 +126,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions); - options.ToolsOverride.AddRange(agent.Tools); // %%% + options.ToolsOverride.AddRange(agent.Tools); ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index 9208d8184d82..2f87d326eb75 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -25,9 +25,6 @@ public sealed class OpenAIAssistantExecutionOptions /// public bool? ParallelToolCallsEnabled { get; init; } - //public ToolConstraint? RequiredTool { get; init; } // %%% ENUM ??? - //public KernelFunction? RequiredToolFunction { get; init; } // %%% PLUGIN ??? - /// /// When set, the thread will be truncated to the N most recent messages in the thread. /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs index 2fee5dd84503..1aa0c3ffa745 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs @@ -47,9 +47,6 @@ public sealed class OpenAIAssistantInvocationOptions /// public bool? ParallelToolCallsEnabled { get; init; } - //public ToolConstraint? RequiredTool { get; init; } // %%% ENUM ??? - //public KernelFunction? RequiredToolFunction { get; init; } // %%% PLUGIN ??? - /// /// When set, the thread will be truncated to the N most recent messages in the thread. /// diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs index b8f93db1107b..1a60f1014bf0 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs @@ -194,7 +194,7 @@ public void VerifyAssistantMessageAdapterGetMessageWithAll() items: [ new TextContent("test"), - new ImageContent(new Uri("https://localhost/myimage.png")), // %%% + new ImageContent(new Uri("https://localhost/myimage.png")), new FileReferenceContent("file-id"), new MessageAttachmentContent("file-id"), ]); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs index 991cbcb2d86f..325e93969f20 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs @@ -1,15 +1,124 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using OpenAI.Assistants; +using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; /// -/// %%% +/// Unit testing of . /// public class AssistantRunOptionsFactoryTests { + /// + /// Verify run options generation with null . + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsNullTest() + { + OpenAIAssistantDefinition definition = + new() + { + Temperature = 0.5F, + }; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, null); + Assert.NotNull(options); + Assert.Null(options.Temperature); + Assert.Null(options.NucleusSamplingFactor); + Assert.Empty(options.Metadata); + } + + /// + /// Verify run options generation with equivalent . + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() + { + OpenAIAssistantDefinition definition = + new() + { + Temperature = 0.5F, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Temperature = 0.5F, + }; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + Assert.NotNull(options); + Assert.Null(options.Temperature); + Assert.Null(options.NucleusSamplingFactor); + } + + /// + /// Verify run options generation with override. + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() + { + OpenAIAssistantDefinition definition = + new() + { + Temperature = 0.5F, + ExecutionOptions = + new() + { + TruncationMessageCount = 5, + }, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Temperature = 0.9F, + TruncationMessageCount = 8, + EnableJsonResponse = true, + }; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + Assert.NotNull(options); + Assert.Equal(0.9F, options.Temperature); + Assert.Equal(8, options.TruncationStrategy.LastMessages); + Assert.Equal(AssistantResponseFormat.JsonObject, options.ResponseFormat); + Assert.Null(options.NucleusSamplingFactor); + } + + /// + /// Verify run options generation with metadata. + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest() + { + OpenAIAssistantDefinition definition = + new() + { + Temperature = 0.5F, + ExecutionOptions = + new() + { + TruncationMessageCount = 5, + }, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Metadata = new Dictionary + { + { "key1", "value" }, + { "key2", null! }, + }, + }; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + Assert.Equal(2, options.Metadata.Count); + Assert.Equal("value", options.Metadata["key1"]); + Assert.Equal(string.Empty, options.Metadata["key2"]); + } } From f6e8c7befc727f8e2f9ceb1d74facdd77201987f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 17:22:06 -0700 Subject: [PATCH 109/226] More testing --- .../OpenAI/Internal/AssistantThreadActions.cs | 1 - .../OpenAI/Internal/OpenAIClientFactory.cs | 1 + .../Internal/OpenAIClientFactoryTests.cs | 21 +++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index cea8dbaf9664..bfde63641d18 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using Azure; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index 0c89ea1b3c2b..0b32ae8e0411 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -33,6 +33,7 @@ public static OpenAIClient CreateClient(OpenAIServiceConfiguration config) { return new AzureOpenAIClient(config.Endpoint, config.Credential, clientOptions); } + if (!string.IsNullOrEmpty(config.ApiKey)) { return new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs index 7fa2ebca23c8..df2387c917e3 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs @@ -2,6 +2,7 @@ using System; using System.Net.Http; using Azure.Core; +using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Moq; @@ -38,6 +39,26 @@ public void VerifyOpenAIClientFactoryTargetAzureByCredential() Assert.NotNull(client); } + /// + /// Verify that the factory throws exception for null credential. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureNullCredential() + { + OpenAIServiceConfiguration config = new() { Type = OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI }; + Assert.Throws(() => OpenAIClientFactory.CreateClient(config)); + } + + /// + /// Verify that the factory throws exception for null credential. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetUnknownTypes() + { + OpenAIServiceConfiguration config = new() { Type = (OpenAIServiceConfiguration.OpenAIServiceType)99 }; + Assert.Throws(() => OpenAIClientFactory.CreateClient(config)); + } + /// /// Verify that the factory can create a client for various OpenAI service configurations. /// From 117066c2a8e665e148fa3675d494795f89370ff3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 22 Jul 2024 17:31:37 -0700 Subject: [PATCH 110/226] Clean-up --- .../GettingStartedWithAgents/Step6_DependencyInjection.cs | 6 +++--- .../OpenAI/Internal/AssistantMessageAdapterTests.cs | 1 - dotnet/src/Experimental/Agents/Internal/Agent.cs | 2 +- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs index 06c4ca3060e0..21af5db70dce 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs @@ -38,9 +38,9 @@ public async Task UseDependencyInjectionToCreateAgentAsync() if (this.UseOpenAIConfig) { - //serviceContainer.AddOpenAIChatCompletion( // %%% CONNECTOR IMPL - // TestConfiguration.OpenAI.ChatModelId, - // TestConfiguration.OpenAI.ApiKey); + serviceContainer.AddOpenAIChatCompletion( + TestConfiguration.OpenAI.ChatModelId, + TestConfiguration.OpenAI.ApiKey); } else { diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs index 1a60f1014bf0..241bd5d8ada5 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/src/Experimental/Agents/Internal/Agent.cs b/dotnet/src/Experimental/Agents/Internal/Agent.cs index c078703cda02..ae64af04d39a 100644 --- a/dotnet/src/Experimental/Agents/Internal/Agent.cs +++ b/dotnet/src/Experimental/Agents/Internal/Agent.cs @@ -121,7 +121,7 @@ internal Agent( this.Kernel = this._restContext.HasVersion ? builder.AddAzureOpenAIChatCompletion(this._model.Model, this.GetAzureRootEndpoint(), this._restContext.ApiKey).Build() : - new(); // %%% HACK builder.AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey).Build(); + builder.AddOpenAIChatCompletion(this._model.Model, this._restContext.ApiKey).Build(); if (plugins is not null) { From ecd3feea0b0d955419fce72c3ee431182a7ce7e2 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 23 Jul 2024 17:46:24 +0100 Subject: [PATCH 111/226] .Net: OpenAI V2 Optional Settings (#7409) Resolve #7111 ## Optional Settings As part of this update all the previous enforeced/default settings from OpenAI and AzureOpenAI connectors are now optional, not sending any non-defined information to the server side API, leaving the underlying API to resolve its default value. ## Integration Test Fixes As a small add up to this PR I also added fixes to our Integration Tests when executing against Parallel Function Calling capable models. --- .../AutoFunctionCallingController.cs | 1 + .../Controllers/StepwisePlannerController.cs | 1 + .../Extensions/ConfigurationExtensions.cs | 1 + .../Plugins/TimePlugin.cs | 1 + .../Demos/StepwisePlannerMigration/Program.cs | 5 + .../Services/PlanProvider.cs | 1 + .../StepwisePlannerMigration.csproj | 1 - ...AzureOpenAIPromptExecutionSettingsTests.cs | 14 +- .../Core/ClientCore.AudioToText.cs | 3 +- .../Core/ClientCore.ChatCompletion.cs | 2 +- .../Core/ClientCore.TextToAudio.cs | 3 +- .../OpenAIPromptExecutionSettingsTests.cs | 10 +- .../Core/ClientCore.AudioToText.cs | 3 +- .../Core/ClientCore.TextToAudio.cs | 3 +- .../OpenAIAudioToTextExecutionSettings.cs | 12 +- .../Settings/OpenAIPromptExecutionSettings.cs | 33 ++-- .../OpenAITextToAudioExecutionSettings.cs | 10 +- ...enAIChatCompletion_FunctionCallingTests.cs | 150 ++++++++++++------ .../AzureOpenAITextEmbeddingTests.cs | 4 +- ...enAIChatCompletion_FunctionCallingTests.cs | 148 ++++++++++++----- dotnet/src/IntegrationTestsV2/TestHelpers.cs | 10 ++ 21 files changed, 286 insertions(+), 130 deletions(-) diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs index 8878bc0b57e5..e65f12d59eb0 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs index f060268833ca..7a0041062341 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Extensions/ConfigurationExtensions.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Extensions/ConfigurationExtensions.cs index a7eca68c33c8..3407d79479ed 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Extensions/ConfigurationExtensions.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Extensions/ConfigurationExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel.DataAnnotations; +using Microsoft.Extensions.Configuration; namespace StepwisePlannerMigration.Extensions; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs index 7a1ce92d0a71..1bfdcde9a236 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.ComponentModel; using Microsoft.SemanticKernel; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Program.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Program.cs index 99b62fba30b7..cd9186d405b2 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Program.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Program.cs @@ -1,5 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. +using System.IO; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Planning; using StepwisePlannerMigration.Extensions; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs index 13218eeec135..033473c3c42b 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.IO; using System.Text.Json; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj index adeeb1f6471b..a174d3f4a954 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj +++ b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj @@ -3,7 +3,6 @@ net8.0 enable - enable $(NoWarn);VSTHRD111,CA2007,CS8618,CS1591,SKEXP0001, SKEXP0060 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs index 40d0e36fc1b6..918cc9e3eb90 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Settings/AzureOpenAIPromptExecutionSettingsTests.cs @@ -18,20 +18,22 @@ public class AzureOpenAIPromptExecutionSettingsTests public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() { // Arrange + var maxTokensSettings = 128; + // Act - AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); + AzureOpenAIPromptExecutionSettings executionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(null, maxTokensSettings); // Assert - Assert.Equal(1, executionSettings.Temperature); - Assert.Equal(1, executionSettings.TopP); - Assert.Equal(0, executionSettings.FrequencyPenalty); - Assert.Equal(0, executionSettings.PresencePenalty); + Assert.Null(executionSettings.Temperature); + Assert.Null(executionSettings.TopP); + Assert.Null(executionSettings.FrequencyPenalty); + Assert.Null(executionSettings.PresencePenalty); Assert.Null(executionSettings.StopSequences); Assert.Null(executionSettings.TokenSelectionBiases); Assert.Null(executionSettings.TopLogprobs); Assert.Null(executionSettings.Logprobs); Assert.Null(executionSettings.AzureChatDataSource); - Assert.Equal(128, executionSettings.MaxTokens); + Assert.Equal(maxTokensSettings, executionSettings.MaxTokens); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs index 83a283490305..a900c5c9f0c5 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs @@ -67,7 +67,7 @@ private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(OpenA [nameof(audioTranscription.Segments)] = audioTranscription.Segments }; - private static AudioTranscriptionFormat ConvertResponseFormat(string responseFormat) + private static AudioTranscriptionFormat? ConvertResponseFormat(string? responseFormat) { return responseFormat switch { @@ -75,6 +75,7 @@ private static AudioTranscriptionFormat ConvertResponseFormat(string responseFor "verbose_json" => AudioTranscriptionFormat.Verbose, "vtt" => AudioTranscriptionFormat.Vtt, "srt" => AudioTranscriptionFormat.Srt, + null => null, _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), }; } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs index c341ac29bd3e..408e434d0000 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs @@ -683,7 +683,7 @@ private ChatCompletionOptions CreateChatCompletionOptions( TopLogProbabilityCount = executionSettings.TopLogprobs, IncludeLogProbabilities = executionSettings.Logprobs, ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, - ToolChoice = toolCallingConfig.Choice, + ToolChoice = toolCallingConfig.Choice }; if (executionSettings.AzureChatDataSource is not null) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs index 0742727ac46b..5a5e1e9f7d9d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs @@ -60,7 +60,7 @@ private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice) _ => throw new NotSupportedException($"The voice '{voice}' is not supported."), }; - private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) + private static (GeneratedSpeechFormat? Format, string? MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) => format?.ToUpperInvariant() switch { "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"), @@ -69,6 +69,7 @@ private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeec "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"), "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"), "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), + null => (null, null), _ => throw new NotSupportedException($"The format '{format}' is not supported.") }; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs index 4e272320eee3..d297b2691d0f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs @@ -23,10 +23,10 @@ public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() // Assert Assert.NotNull(executionSettings); - Assert.Equal(1, executionSettings.Temperature); - Assert.Equal(1, executionSettings.TopP); - Assert.Equal(0, executionSettings.FrequencyPenalty); - Assert.Equal(0, executionSettings.PresencePenalty); + Assert.Null(executionSettings.Temperature); + Assert.Null(executionSettings.TopP); + Assert.Null(executionSettings.FrequencyPenalty); + Assert.Null(executionSettings.PresencePenalty); Assert.Null(executionSettings.StopSequences); Assert.Null(executionSettings.TokenSelectionBiases); Assert.Null(executionSettings.TopLogprobs); @@ -58,7 +58,7 @@ public void ItUsesExistingOpenAIExecutionSettings() // Assert Assert.NotNull(executionSettings); Assert.Equal(actualSettings, executionSettings); - Assert.Equal(256, OpenAIPromptExecutionSettings.DefaultTextMaxTokens); + Assert.Equal(128, executionSettings.MaxTokens); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs index 8a652abae397..bdbee092d5e9 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -58,7 +58,7 @@ internal async Task> GetTextFromAudioContentsAsync( ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) }; - private static AudioTranscriptionFormat ConvertResponseFormat(string responseFormat) + private static AudioTranscriptionFormat? ConvertResponseFormat(string? responseFormat) { return responseFormat switch { @@ -66,6 +66,7 @@ private static AudioTranscriptionFormat ConvertResponseFormat(string responseFor "verbose_json" => AudioTranscriptionFormat.Verbose, "vtt" => AudioTranscriptionFormat.Vtt, "srt" => AudioTranscriptionFormat.Srt, + null => null, _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), }; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs index 75e484a489aa..4bf071bc3c26 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs @@ -53,7 +53,7 @@ private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice) _ => throw new NotSupportedException($"The voice '{voice}' is not supported."), }; - private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) + private static (GeneratedSpeechFormat? Format, string? MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) => format?.ToUpperInvariant() switch { "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"), @@ -62,6 +62,7 @@ private static (GeneratedSpeechFormat Format, string MimeType) GetGeneratedSpeec "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"), "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"), "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), + null => (null, null), _ => throw new NotSupportedException($"The format '{format}' is not supported.") }; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs index d41bdcc7ae96..441d29c80607 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs @@ -34,6 +34,7 @@ public string Filename /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es'). /// [JsonPropertyName("language")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Language { get => this._language; @@ -49,6 +50,7 @@ public string? Language /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. /// [JsonPropertyName("prompt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Prompt { get => this._prompt; @@ -64,7 +66,8 @@ public string? Prompt /// The format of the transcript output, in one of these options: json, srt, verbose_json, or vtt. Default is 'json'. /// [JsonPropertyName("response_format")] - public string ResponseFormat + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ResponseFormat { get => this._responseFormat; @@ -82,7 +85,8 @@ public string ResponseFormat /// Default is 0. /// [JsonPropertyName("temperature")] - public float Temperature + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Temperature { get => this._temperature; @@ -152,8 +156,8 @@ public override PromptExecutionSettings Clone() private const string DefaultFilename = "file.mp3"; - private float _temperature = 0; - private string _responseFormat = "json"; + private float? _temperature = 0; + private string? _responseFormat; private string _filename; private string? _language; private string? _prompt; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs index d3e78b9a3c11..f0c92e5af98f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs @@ -23,7 +23,8 @@ public class OpenAIPromptExecutionSettings : PromptExecutionSettings /// Default is 1.0. /// [JsonPropertyName("temperature")] - public double Temperature + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Temperature { get => this._temperature; @@ -40,7 +41,8 @@ public double Temperature /// Default is 1.0. /// [JsonPropertyName("top_p")] - public double TopP + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? TopP { get => this._topP; @@ -57,7 +59,8 @@ public double TopP /// model's likelihood to talk about new topics. /// [JsonPropertyName("presence_penalty")] - public double PresencePenalty + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? PresencePenalty { get => this._presencePenalty; @@ -74,7 +77,8 @@ public double PresencePenalty /// the model's likelihood to repeat the same line verbatim. /// [JsonPropertyName("frequency_penalty")] - public double FrequencyPenalty + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? FrequencyPenalty { get => this._frequencyPenalty; @@ -89,6 +93,7 @@ public double FrequencyPenalty /// The maximum number of tokens to generate in the completion. /// [JsonPropertyName("max_tokens")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxTokens { get => this._maxTokens; @@ -104,6 +109,7 @@ public int? MaxTokens /// Sequences where the completion will stop generating further tokens. /// [JsonPropertyName("stop_sequences")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IList? StopSequences { get => this._stopSequences; @@ -120,6 +126,7 @@ public IList? StopSequences /// same seed and parameters should return the same result. Determinism is not guaranteed. /// [JsonPropertyName("seed")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public long? Seed { get => this._seed; @@ -139,6 +146,7 @@ public long? Seed /// [Experimental("SKEXP0010")] [JsonPropertyName("response_format")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object? ResponseFormat { get => this._responseFormat; @@ -155,6 +163,7 @@ public object? ResponseFormat /// Defaults to "Assistant is a large language model." /// [JsonPropertyName("chat_system_prompt")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ChatSystemPrompt { get => this._chatSystemPrompt; @@ -170,6 +179,7 @@ public string? ChatSystemPrompt /// Modify the likelihood of specified tokens appearing in the completion. /// [JsonPropertyName("token_selection_biases")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IDictionary? TokenSelectionBiases { get => this._tokenSelectionBiases; @@ -242,6 +252,7 @@ public string? User /// [Experimental("SKEXP0010")] [JsonPropertyName("logprobs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? Logprobs { get => this._logprobs; @@ -258,6 +269,7 @@ public bool? Logprobs /// [Experimental("SKEXP0010")] [JsonPropertyName("top_logprobs")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? TopLogprobs { get => this._topLogprobs; @@ -296,11 +308,6 @@ public override PromptExecutionSettings Clone() return this.Clone(); } - /// - /// Default max tokens for a text generation - /// - internal static int DefaultTextMaxTokens { get; } = 256; - /// /// Create a new settings object with the values from another settings object. /// @@ -359,10 +366,10 @@ public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutio #region private ================================================================================ - private double _temperature = 1; - private double _topP = 1; - private double _presencePenalty; - private double _frequencyPenalty; + private double? _temperature; + private double? _topP; + private double? _presencePenalty; + private double? _frequencyPenalty; private int? _maxTokens; private IList? _stopSequences; private long? _seed; diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs index 07e3305e69df..e805578f8cc6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs @@ -38,7 +38,8 @@ public string Voice /// The format to audio in. Supported formats are mp3, opus, aac, and flac. /// [JsonPropertyName("response_format")] - public string ResponseFormat + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ResponseFormat { get => this._responseFormat; @@ -53,7 +54,8 @@ public string ResponseFormat /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. /// [JsonPropertyName("speed")] - public float Speed + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Speed { get => this._speed; @@ -121,8 +123,8 @@ public static OpenAITextToAudioExecutionSettings FromExecutionSettings(PromptExe private const string DefaultVoice = "alloy"; - private float _speed = 1.0f; - private string _responseFormat = "mp3"; + private float? _speed; + private string? _responseFormat; private string _voice; #endregion diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 001f502414c6..24ba6f2cad4d 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -41,7 +41,6 @@ public async Task CanAutoInvokeKernelFunctionsAsync() var result = await kernel.InvokePromptAsync("Given the current time of day and weather, what is the likely color of the sky in Boston?", new(settings)); // Assert - Assert.Contains("rain", result.GetValue(), StringComparison.InvariantCulture); Assert.Contains("GetCurrentUtcTime()", invokedFunctions); Assert.Contains("Get_Weather_For_City([cityName, Boston])", invokedFunctions); } @@ -313,8 +312,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Assert Assert.NotNull(messageContent.Content); - - Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + TestHelpers.AssertChatErrorExcuseMessage(messageContent.Content); } [Fact] @@ -407,42 +405,72 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); // Assert - Assert.Equal(5, chatHistory.Count); - var userMessage = chatHistory[0]; Assert.Equal(AuthorRole.User, userMessage.Role); - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + // LLM requested the functions to call. + var getParallelFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getParallelFunctionCallRequestMessage.Role); + + // Parallel Function Calls in the same request + var functionCalls = getParallelFunctionCallRequestMessage.Items.OfType().ToArray(); + + ChatMessageContent getCurrentTimeFunctionCallResultMessage; + ChatMessageContent getWeatherForCityFunctionCallRequestMessage; + FunctionCallContent getWeatherForCityFunctionCallRequest; + FunctionCallContent getCurrentTimeFunctionCallRequest; + ChatMessageContent getWeatherForCityFunctionCallResultMessage; + + // Assert + // Non Parallel Tool Calling + if (functionCalls.Length == 1) + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + } + else // Parallel Tool Calling + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequest = functionCalls[1]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[3]; + } - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + // Connector invoked the GetCurrentUtcTime function and added result to chat history. Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. @@ -531,42 +559,72 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu } // Assert - Assert.Equal(5, chatHistory.Count); - var userMessage = chatHistory[0]; Assert.Equal(AuthorRole.User, userMessage.Role); - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + // LLM requested the functions to call. + var getParallelFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getParallelFunctionCallRequestMessage.Role); + + // Parallel Function Calls in the same request + var functionCalls = getParallelFunctionCallRequestMessage.Items.OfType().ToArray(); + + ChatMessageContent getCurrentTimeFunctionCallResultMessage; + ChatMessageContent getWeatherForCityFunctionCallRequestMessage; + FunctionCallContent getWeatherForCityFunctionCallRequest; + FunctionCallContent getCurrentTimeFunctionCallRequest; + ChatMessageContent getWeatherForCityFunctionCallResultMessage; + + // Assert + // Non Parallel Tool Calling + if (functionCalls.Length == 1) + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + } + else // Parallel Tool Calling + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequest = functionCalls[1]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[3]; + } - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + // Connector invoked the GetCurrentUtcTime function and added result to chat history. Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. @@ -632,7 +690,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc } // Assert - Assert.Contains("error", result, StringComparison.InvariantCultureIgnoreCase); + TestHelpers.AssertChatErrorExcuseMessage(result); } [Fact] @@ -728,7 +786,7 @@ public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistor kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. - chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + chatHistory.AddUserMessage("Send the exact answer to my email: abc@domain.com"); var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; @@ -737,7 +795,7 @@ public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistor // Assert Assert.Equal("abc@domain.com", emailRecipient); - Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + Assert.Contains("61\u00B0F", emailBody); } [Fact] @@ -764,7 +822,7 @@ public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistor kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. - chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + chatHistory.AddUserMessage("Send the exact answer to my email: abc@domain.com"); var settings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; @@ -773,7 +831,7 @@ public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistor // Assert Assert.Equal("abc@domain.com", emailRecipient); - Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + Assert.Contains("61\u00B0F", emailBody); } /// diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs index 1dfc39670416..1fc5678ed564 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs @@ -30,11 +30,11 @@ public async Task AzureOpenAITestAsync(string testInputString) // Act var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString); - var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]); + var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString]); // Assert Assert.Equal(AdaVectorLength, singleResult.Length); - Assert.Equal(3, batchResult.Count); + Assert.Single(batchResult); } [Theory] diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs index 5cb6c8d4a0b9..4a3746dbca99 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs @@ -313,7 +313,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc // Assert Assert.NotNull(messageContent.Content); - Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); + TestHelpers.AssertChatErrorExcuseMessage(messageContent.Content); } [Fact] @@ -406,42 +406,72 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); // Assert - Assert.Equal(5, chatHistory.Count); - var userMessage = chatHistory[0]; Assert.Equal(AuthorRole.User, userMessage.Role); - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + // LLM requested the functions to call. + var getParallelFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getParallelFunctionCallRequestMessage.Role); + + // Parallel Function Calls in the same request + var functionCalls = getParallelFunctionCallRequestMessage.Items.OfType().ToArray(); + + ChatMessageContent getCurrentTimeFunctionCallResultMessage; + ChatMessageContent getWeatherForCityFunctionCallRequestMessage; + FunctionCallContent getWeatherForCityFunctionCallRequest; + FunctionCallContent getCurrentTimeFunctionCallRequest; + ChatMessageContent getWeatherForCityFunctionCallResultMessage; + + // Assert + // Non Parallel Tool Calling + if (functionCalls.Length == 1) + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + } + else // Parallel Tool Calling + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequest = functionCalls[1]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[3]; + } - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + // Connector invoked the GetCurrentUtcTime function and added result to chat history. Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. @@ -530,42 +560,72 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFu } // Assert - Assert.Equal(5, chatHistory.Count); - var userMessage = chatHistory[0]; Assert.Equal(AuthorRole.User, userMessage.Role); - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); + // LLM requested the functions to call. + var getParallelFunctionCallRequestMessage = chatHistory[1]; + Assert.Equal(AuthorRole.Assistant, getParallelFunctionCallRequestMessage.Role); + + // Parallel Function Calls in the same request + var functionCalls = getParallelFunctionCallRequestMessage.Items.OfType().ToArray(); + + ChatMessageContent getCurrentTimeFunctionCallResultMessage; + ChatMessageContent getWeatherForCityFunctionCallRequestMessage; + FunctionCallContent getWeatherForCityFunctionCallRequest; + FunctionCallContent getCurrentTimeFunctionCallRequest; + ChatMessageContent getWeatherForCityFunctionCallResultMessage; + + // Assert + // Non Parallel Tool Calling + if (functionCalls.Length == 1) + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; + getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[4]; + } + else // Parallel Tool Calling + { + // LLM requested the current time. + getCurrentTimeFunctionCallRequest = functionCalls[0]; + + // LLM requested the weather for Boston. + getWeatherForCityFunctionCallRequest = functionCalls[1]; + + // Connector invoked the GetCurrentUtcTime function and added result to chat history. + getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + + // Connector invoked the Get_Weather_For_City function and added result to chat history. + getWeatherForCityFunctionCallResultMessage = chatHistory[3]; + } - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; + Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); + Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); + Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); + Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); + // Connector invoked the GetCurrentUtcTime function and added result to chat history. Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. @@ -631,7 +691,7 @@ public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExc } // Assert - Assert.Contains("error", result, StringComparison.InvariantCultureIgnoreCase); + TestHelpers.AssertChatErrorExcuseMessage(result); } [Fact] @@ -727,7 +787,7 @@ public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistor kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. - chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + chatHistory.AddUserMessage("Send the exact answer to my email: abc@domain.com"); var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; @@ -736,7 +796,7 @@ public async Task ItShouldSupportOldFunctionCallingModelSerializedIntoChatHistor // Assert Assert.Equal("abc@domain.com", emailRecipient); - Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + Assert.Contains("61\u00B0F", emailBody); } [Fact] @@ -763,7 +823,7 @@ public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistor kernel.ImportPluginFromFunctions("EmailPlugin", [KernelFunctionFactory.CreateFromMethod((string body, string recipient) => { emailBody = body; emailRecipient = recipient; }, "SendEmail")]); // The deserialized chat history contains a list of function calls and the final answer to the question regarding the color of the sky in Boston. - chatHistory.AddUserMessage("Send it to my email: abc@domain.com"); + chatHistory.AddUserMessage("Send the exact answer to my email: abc@domain.com"); var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; @@ -772,7 +832,7 @@ public async Task ItShouldSupportNewFunctionCallingModelSerializedIntoChatHistor // Assert Assert.Equal("abc@domain.com", emailRecipient); - Assert.Equal("Given the current weather in Boston is 61\u00B0F and rainy, the likely color of the sky would be gray or overcast due to the presence of rain clouds.", emailBody); + Assert.Contains("61\u00B0F", emailBody); } /// diff --git a/dotnet/src/IntegrationTestsV2/TestHelpers.cs b/dotnet/src/IntegrationTestsV2/TestHelpers.cs index 350370d6c056..2cd6318b49ee 100644 --- a/dotnet/src/IntegrationTestsV2/TestHelpers.cs +++ b/dotnet/src/IntegrationTestsV2/TestHelpers.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Microsoft.SemanticKernel; +using Xunit; namespace SemanticKernel.IntegrationTestsV2; @@ -52,4 +53,13 @@ internal static IReadOnlyKernelPluginCollection ImportSamplePromptFunctions(Kern from pluginName in pluginNames select kernel.ImportPluginFromPromptDirectory(Path.Combine(parentDirectory, pluginName))); } + + internal static void AssertChatErrorExcuseMessage(string content) + { + string[] errors = ["error", "difficult", "unable"]; + + var matchesAny = errors.Any(e => content.Contains(e, StringComparison.InvariantCultureIgnoreCase)); + + Assert.True(matchesAny); + } } From 10c4afb668a7cf80a3ffcfd4b52a6c2d7f060707 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 23 Jul 2024 13:27:48 -0700 Subject: [PATCH 112/226] Sync with bug-fix --- .../Internal/AssistantMessageAdapter.cs | 4 - .../OpenAI/Internal/AssistantThreadActions.cs | 86 +++++++++---------- .../Internal/AssistantMessageAdapterTests.cs | 20 +---- .../Contents/MessageAttachmentContent.cs | 42 --------- 4 files changed, 43 insertions(+), 109 deletions(-) delete mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs index 99aeb53f1ebe..50b9b9e9547f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs @@ -43,10 +43,6 @@ public static IEnumerable GetMessageContents(ChatMessageContent // yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); //} } - else if (content is MessageAttachmentContent attachmentContent) - { - options.Attachments.Add(new MessageCreationAttachment(attachmentContent.FileId, [ToolDefinition.CreateCodeInterpreter()])); // %%% TODO: Tool Type - } else if (content is FileReferenceContent fileContent) { yield return MessageContent.FromImageFileId(fileContent.FileId); diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index bfde63641d18..68e1f4e0ce0f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -43,8 +43,7 @@ internal static class AssistantThreadActions /// if a system message is present, without taking any other action public static async Task CreateMessageAsync(AssistantClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(message.Content) || - message.Items.Any(i => i is FunctionCallContent)) + if (message.Items.Any(i => i is FunctionCallContent)) { return; } @@ -86,14 +85,11 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist assistantName ??= message.AssistantId; - foreach (MessageContent itemContent in message.Content) - { - ChatMessageContent content = GenerateMessageContent(role, assistantName, itemContent); + ChatMessageContent content = GenerateMessageContent(assistantName, message); - if (content.Items.Count > 0) - { - yield return content; - } + if (content.Items.Count > 0) + { + yield return content; } } } @@ -221,18 +217,13 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist if (message is not null) { - AuthorRole role = new(message.Role.ToString()); + ChatMessageContent content = GenerateMessageContent(agent.GetName(), message); - foreach (MessageContent itemContent in message.Content) + if (content.Items.Count > 0) { - ChatMessageContent content = GenerateMessageContent(role, agent.Name, itemContent); - - if (content.Items.Count > 0) - { - ++messageCount; + ++messageCount; - yield return (IsVisible: true, Message: content); - } + yield return (IsVisible: true, Message: content); } } } @@ -336,6 +327,38 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R } } + private static ChatMessageContent GenerateMessageContent(string? assistantName, ThreadMessage message) + { + AuthorRole role = new(message.Role.ToString()); + + ChatMessageContent content = + new(role, content: null) + { + AuthorName = assistantName, + }; + + foreach (MessageContent itemContent in message.Content) + { + // Process text content + if (!string.IsNullOrEmpty(itemContent.Text)) + { + content.Items.Add(new TextContent(itemContent.Text.Trim())); + + foreach (TextAnnotation annotation in itemContent.TextAnnotations) + { + content.Items.Add(GenerateAnnotationContent(annotation)); + } + } + // Process image content + else if (itemContent.ImageFileId != null) + { + content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); + } + } + + return content; + } + private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation) { string? fileId = null; @@ -363,7 +386,7 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam { return new ChatMessageContent( - AuthorRole.Tool, + AuthorRole.Assistant, [ new TextContent(code) ]) @@ -401,31 +424,6 @@ private static ChatMessageContent GenerateFunctionResultContent(string agentName return functionCallContent; } - private static ChatMessageContent GenerateMessageContent(AuthorRole role, string? assistantName, MessageContent itemContent) - { - ChatMessageContent content = - new(role, content: null) - { - AuthorName = assistantName, - }; - - if (!string.IsNullOrEmpty(itemContent.Text)) - { - content.Items.Add(new TextContent(itemContent.Text.Trim())); - foreach (TextAnnotation annotation in itemContent.TextAnnotations) - { - content.Items.Add(GenerateAnnotationContent(annotation)); - } - } - // Process image content - else if (itemContent.ImageFileId != null) - { - content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); - } - - return content; - } - private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionSteps, CancellationToken cancellationToken) { Task[] functionTasks = new Task[functionSteps.Length]; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs index 241bd5d8ada5..eaa00eb529eb 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs @@ -167,20 +167,6 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageFile() Assert.NotNull(contents.Single().ImageFileId); } - /// - /// Verify options creation. - /// - [Fact] - public void VerifyAssistantMessageAdapterGetMessageWithAttachment() - { - ChatMessageContent message = new(AuthorRole.User, items: [new MessageAttachmentContent("file-id")]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); - Assert.NotNull(options.Attachments); - Assert.Single(options.Attachments); - Assert.NotNull(options.Attachments.Single().FileId); - } - /// /// Verify options creation. /// @@ -194,15 +180,11 @@ public void VerifyAssistantMessageAdapterGetMessageWithAll() [ new TextContent("test"), new ImageContent(new Uri("https://localhost/myimage.png")), - new FileReferenceContent("file-id"), - new MessageAttachmentContent("file-id"), + new FileReferenceContent("file-id") ]); MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); Assert.NotNull(contents); Assert.Equal(3, contents.Length); - Assert.NotNull(options.Attachments); - Assert.Single(options.Attachments); - Assert.NotNull(options.Attachments.Single().FileId); } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs deleted file mode 100644 index 6f4f55f9af4a..000000000000 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/MessageAttachmentContent.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel; - -/// -/// Content type to support message attachment. -/// -[Experimental("SKEXP0110")] -public class MessageAttachmentContent : FileReferenceContent -{ - /// - /// The associated tool. - /// - public string Tool { get; init; } = string.Empty; - - /// - /// Initializes a new instance of the class. - /// - [JsonConstructor] - public MessageAttachmentContent() - { } - - /// - /// Initializes a new instance of the class. - /// - /// The identifier of the referenced file. - /// The model ID used to generate the content. - /// Inner content - /// Additional metadata - public MessageAttachmentContent( - string fileId, - string? modelId = null, - object? innerContent = null, - IReadOnlyDictionary? metadata = null) - : base(fileId, modelId, innerContent, metadata) - { - // %%% TOOL TYPE - } -} From 497f22594827ad00bd6988240b1e21417871f7c9 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 24 Jul 2024 17:31:45 +0100 Subject: [PATCH 113/226] .Net: Net: OpenAI v2 Reusability (#7427) ### Motivation and Context Resolves #7430 - This PR focus mainly and removing duplicate code between OpenAI and AzureOpenAI. --- .../AzureOpenAIAudioToTextServiceTests.cs | 6 +- .../AzureOpenAITextToImageServiceTests.cs | 6 +- .../Core/AzureClientCore.ChatCompletion.cs | 111 ++ .../{ClientCore.cs => AzureClientCore.cs} | 85 +- .../Core/ClientCore.AudioToText.cs | 82 -- .../Core/ClientCore.ChatCompletion.cs | 1210 ----------------- .../Core/ClientCore.Embeddings.cs | 55 - .../Core/ClientCore.TextToAudio.cs | 83 -- .../Core/ClientCore.TextToImage.cs | 43 - .../AzureOpenAIKernelBuilderExtensions.cs | 4 +- .../AzureOpenAIServiceCollectionExtensions.cs | 4 +- .../Services/AzureOpenAIAudioToTextService.cs | 18 +- .../AzureOpenAIChatCompletionService.cs | 24 +- ...ureOpenAITextEmbeddingGenerationService.cs | 18 +- .../Services/AzureOpenAITextToAudioService.cs | 16 +- .../Services/AzureOpenAITextToImageService.cs | 10 +- .../Core/ClientCoreTests.cs | 6 +- .../Core/ClientCore.AudioToText.cs | 12 +- .../Core/ClientCore.ChatCompletion.cs | 256 ++-- .../Core/ClientCore.Embeddings.cs | 4 +- .../Core/ClientCore.TextToAudio.cs | 12 +- .../Core/ClientCore.TextToImage.cs | 6 +- .../Connectors.OpenAIV2/Core/ClientCore.cs | 27 +- .../Services/OpenAIAudioToTextService.cs | 2 +- .../Services/OpenAIChatCompletionService.cs | 8 +- .../OpenAITextEmbbedingGenerationService.cs | 2 +- .../Services/OpenAITextToAudioService.cs | 2 +- .../Services/OpenAITextToImageService.cs | 2 +- .../src/Diagnostics/ModelDiagnostics.cs | 20 +- 29 files changed, 380 insertions(+), 1754 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs rename dotnet/src/Connectors/Connectors.AzureOpenAI/Core/{ClientCore.cs => AzureClientCore.cs} (73%) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs index 89642f1345c0..a7f2f6b5a83d 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIAudioToTextServiceTests.cs @@ -43,7 +43,7 @@ public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) // Assert Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); - Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", service.Attributes[AzureClientCore.DeploymentNameKey]); } [Theory] @@ -59,7 +59,7 @@ public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFacto // Assert Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); - Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", service.Attributes[AzureClientCore.DeploymentNameKey]); } [Theory] @@ -75,7 +75,7 @@ public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) // Assert Assert.Equal("model-id", service.Attributes[AIServiceExtensions.ModelIdKey]); - Assert.Equal("deployment", service.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", service.Attributes[AzureClientCore.DeploymentNameKey]); } [Fact] diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs index 89b25e9b2ec0..60aed7875b56 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAITextToImageServiceTests.cs @@ -42,17 +42,17 @@ public void ConstructorsAddRequiredMetadata() { // Case #1 var sut = new AzureOpenAITextToImageService("deployment", "https://api-host/", "api-key", "model", loggerFactory: this._mockLoggerFactory.Object); - Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", sut.Attributes[AzureClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); // Case #2 sut = new AzureOpenAITextToImageService("deployment", "https://api-hostapi/", new Mock().Object, "model", loggerFactory: this._mockLoggerFactory.Object); - Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", sut.Attributes[AzureClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); // Case #3 sut = new AzureOpenAITextToImageService("deployment", new AzureOpenAIClient(new Uri("https://api-host/"), "api-key"), "model", loggerFactory: this._mockLoggerFactory.Object); - Assert.Equal("deployment", sut.Attributes[ClientCore.DeploymentNameKey]); + Assert.Equal("deployment", sut.Attributes[AzureClientCore.DeploymentNameKey]); Assert.Equal("model", sut.Attributes[AIServiceExtensions.ModelIdKey]); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs new file mode 100644 index 000000000000..8a8fc8dfedca --- /dev/null +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.ChatCompletion.cs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Collections.Generic; +using System.Diagnostics; +using Azure.AI.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; +using Microsoft.SemanticKernel.Diagnostics; +using OpenAI.Chat; +using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; + +#pragma warning disable CA2208 // Instantiate argument exceptions correctly + +namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; + +/// +/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. +/// +internal partial class AzureClientCore +{ + private const string ContentFilterResultForPromptKey = "ContentFilterResultForPrompt"; + private const string ContentFilterResultForResponseKey = "ContentFilterResultForResponse"; + + /// + protected override OpenAIPromptExecutionSettings GetSpecializedExecutionSettings(PromptExecutionSettings? executionSettings) + { + return AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + } + + /// + protected override Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) + { +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + return new Dictionary + { + { nameof(completions.Id), completions.Id }, + { nameof(completions.CreatedAt), completions.CreatedAt }, + { ContentFilterResultForPromptKey, completions.GetContentFilterResultForPrompt() }, + { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, + { nameof(completions.Usage), completions.Usage }, + { ContentFilterResultForResponseKey, completions.GetContentFilterResultForResponse() }, + + // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. + { nameof(completions.FinishReason), completions.FinishReason.ToString() }, + { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, + }; +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + /// + protected override Activity? StartCompletionActivity(ChatHistory chatHistory, PromptExecutionSettings settings) + => ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chatHistory, settings); + + /// + protected override ChatCompletionOptions CreateChatCompletionOptions( + OpenAIPromptExecutionSettings executionSettings, + ChatHistory chatHistory, + ToolCallingConfig toolCallingConfig, + Kernel? kernel) + { + if (executionSettings is not AzureOpenAIPromptExecutionSettings azureSettings) + { + return base.CreateChatCompletionOptions(executionSettings, chatHistory, toolCallingConfig, kernel); + } + + var options = new ChatCompletionOptions + { + MaxTokens = executionSettings.MaxTokens, + Temperature = (float?)executionSettings.Temperature, + TopP = (float?)executionSettings.TopP, + FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, + PresencePenalty = (float?)executionSettings.PresencePenalty, + Seed = executionSettings.Seed, + User = executionSettings.User, + TopLogProbabilityCount = executionSettings.TopLogprobs, + IncludeLogProbabilities = executionSettings.Logprobs, + ResponseFormat = GetResponseFormat(azureSettings) ?? ChatResponseFormat.Text, + ToolChoice = toolCallingConfig.Choice + }; + + if (azureSettings.AzureChatDataSource is not null) + { +#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + options.AddDataSource(azureSettings.AzureChatDataSource); +#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + } + + if (toolCallingConfig.Tools is { Count: > 0 } tools) + { + options.Tools.AddRange(tools); + } + + if (executionSettings.TokenSelectionBiases is not null) + { + foreach (var keyValue in executionSettings.TokenSelectionBiases) + { + options.LogitBiases.Add(keyValue.Key, keyValue.Value); + } + } + + if (executionSettings.StopSequences is { Count: > 0 }) + { + foreach (var s in executionSettings.StopSequences) + { + options.StopSequences.Add(s); + } + } + + return options; + } +} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs similarity index 73% rename from dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs rename to dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs index 6f669d5eede4..e246f90667b6 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/AzureClientCore.cs @@ -1,16 +1,14 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.ClientModel; using System.ClientModel.Primitives; -using System.Collections.Generic; using System.Net.Http; using System.Threading; -using System.Threading.Tasks; using Azure.AI.OpenAI; using Azure.Core; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Http; using OpenAI; @@ -19,10 +17,10 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// /// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. /// -internal partial class ClientCore +internal partial class AzureClientCore : ClientCore { /// - /// Gets the key used to store the deployment name in the dictionary. + /// Gets the key used to store the deployment name in the dictionary. /// internal static string DeploymentNameKey => "DeploymentName"; @@ -32,34 +30,14 @@ internal partial class ClientCore internal string DeploymentName { get; set; } = string.Empty; /// - /// Azure OpenAI Client - /// - internal AzureOpenAIClient Client { get; } - - /// - /// Azure OpenAI API endpoint. - /// - internal Uri? Endpoint { get; set; } = null; - - /// - /// Logger instance - /// - internal ILogger Logger { get; set; } - - /// - /// Storage for AI service attributes. - /// - internal Dictionary Attributes { get; } = []; - - /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. - internal ClientCore( + internal AzureClientCore( string deploymentName, string endpoint, string apiKey, @@ -82,14 +60,14 @@ internal ClientCore( } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. /// Custom for HTTP requests. /// The to use for logging. If null, no logging will be performed. - internal ClientCore( + internal AzureClientCore( string deploymentName, string endpoint, TokenCredential credential, @@ -111,14 +89,14 @@ internal ClientCore( } /// - /// Initializes a new instance of the class.. + /// Initializes a new instance of the class.. /// Note: instances created this way might not have the default diagnostics settings, /// it's up to the caller to configure the client. /// /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource /// Custom . /// The to use for logging. If null, no logging will be performed. - internal ClientCore( + internal AzureClientCore( string deploymentName, AzureOpenAIClient openAIClient, ILogger? logger = null) @@ -143,7 +121,7 @@ internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? ? new(serviceVersion.Value) { ApplicationId = HttpHeaderConstant.Values.UserAgent } : new() { ApplicationId = HttpHeaderConstant.Values.UserAgent }; - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), PipelinePosition.PerCall); + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureClientCore))), PipelinePosition.PerCall); if (httpClient is not null) { @@ -154,47 +132,4 @@ internal static AzureOpenAIClientOptions GetAzureOpenAIClientOptions(HttpClient? return options; } - - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } - } - - private static async Task RunRequestAsync(Func> request) - { - try - { - return await request.Invoke().ConfigureAwait(false); - } - catch (ClientResultException e) - { - throw e.ToHttpOperationException(); - } - } - - private static T RunRequest(Func request) - { - try - { - return request.Invoke(); - } - catch (ClientResultException e) - { - throw e.ToHttpOperationException(); - } - } - - private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) - { - return new GenericActionPipelinePolicy((message) => - { - if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) - { - message.Request.Headers.Set(headerName, headerValue); - } - }); - } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs deleted file mode 100644 index a900c5c9f0c5..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.AudioToText.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using OpenAI.Audio; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Generates an image with the provided configuration. - /// - /// Input audio to generate the text - /// Audio-to-text execution settings for the prompt - /// The to monitor for cancellation requests. The default is . - /// Url of the generated image - internal async Task> GetTextFromAudioContentsAsync( - AudioContent input, - PromptExecutionSettings? executionSettings, - CancellationToken cancellationToken) - { - if (!input.CanRead) - { - throw new ArgumentException("The input audio content is not readable.", nameof(input)); - } - - OpenAIAudioToTextExecutionSettings audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings)!; - AudioTranscriptionOptions audioOptions = AudioOptionsFromExecutionSettings(audioExecutionSettings); - - Verify.ValidFilename(audioExecutionSettings?.Filename); - - using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); - - AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.DeploymentName).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; - - return [new(responseData.Text, this.DeploymentName, metadata: GetResponseMetadata(responseData))]; - } - - /// - /// Converts to type. - /// - /// Instance of . - /// Instance of . - private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) - => new() - { - Granularities = AudioTimestampGranularities.Default, - Language = executionSettings.Language, - Prompt = executionSettings.Prompt, - Temperature = executionSettings.Temperature, - ResponseFormat = ConvertResponseFormat(executionSettings.ResponseFormat) - }; - - private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) - => new(3) - { - [nameof(audioTranscription.Language)] = audioTranscription.Language, - [nameof(audioTranscription.Duration)] = audioTranscription.Duration, - [nameof(audioTranscription.Segments)] = audioTranscription.Segments - }; - - private static AudioTranscriptionFormat? ConvertResponseFormat(string? responseFormat) - { - return responseFormat switch - { - "json" => AudioTranscriptionFormat.Simple, - "verbose_json" => AudioTranscriptionFormat.Verbose, - "vtt" => AudioTranscriptionFormat.Vtt, - "srt" => AudioTranscriptionFormat.Srt, - null => null, - _ => throw new NotSupportedException($"The audio transcription format '{responseFormat}' is not supported."), - }; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs deleted file mode 100644 index 408e434d0000..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.ChatCompletion.cs +++ /dev/null @@ -1,1210 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Diagnostics; -using OpenAI.Chat; -using OpenAIChatCompletion = OpenAI.Chat.ChatCompletion; - -#pragma warning disable CA2208 // Instantiate argument exceptions correctly - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. -/// -internal partial class ClientCore -{ - private const string ContentFilterResultForPromptKey = "ContentFilterResultForPrompt"; - private const string ContentFilterResultForResponseKey = "ContentFilterResultForResponse"; - private const string ModelProvider = "openai"; - private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); - - /// - /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current - /// asynchronous chain of execution. - /// - /// - /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that - /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, - /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close - /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. - /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in - /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that - /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, - /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent - /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made - /// configurable should need arise. - /// - private const int MaxInflightAutoInvokes = 128; - - /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); - - /// Tracking for . - private static readonly AsyncLocal s_inflightAutoInvokes = new(); - - /// - /// Instance of for metrics. - /// - private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); - - /// - /// Instance of to keep track of the number of prompt tokens used. - /// - private static readonly Counter s_promptTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.prompt", - unit: "{token}", - description: "Number of prompt tokens used"); - - /// - /// Instance of to keep track of the number of completion tokens used. - /// - private static readonly Counter s_completionTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.completion", - unit: "{token}", - description: "Number of completion tokens used"); - - /// - /// Instance of to keep track of the total number of tokens used. - /// - private static readonly Counter s_totalTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.total", - unit: "{token}", - description: "Number of tokens used"); - - private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) - { -#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - return new Dictionary - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.CreatedAt), completions.CreatedAt }, - { ContentFilterResultForPromptKey, completions.GetContentFilterResultForPrompt() }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, - { nameof(completions.Usage), completions.Usage }, - { ContentFilterResultForResponseKey, completions.GetContentFilterResultForResponse() }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completions.FinishReason), completions.FinishReason.ToString() }, - { nameof(completions.ContentTokenLogProbabilities), completions.ContentTokenLogProbabilities }, - }; -#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) - { - return new Dictionary - { - { nameof(completionUpdate.Id), completionUpdate.Id }, - { nameof(completionUpdate.CreatedAt), completionUpdate.CreatedAt }, - { nameof(completionUpdate.SystemFingerprint), completionUpdate.SystemFingerprint }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completionUpdate.FinishReason), completionUpdate.FinishReason?.ToString() }, - }; - } - - /// - /// Generate a new chat message - /// - /// Chat history - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// Async cancellation token - /// Generated chat message in string format - internal async Task> GetChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chat), - JsonSerializer.Serialize(executionSettings)); - } - - // Convert the incoming execution settings to OpenAI settings. - AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - - for (int requestIndex = 0; ; requestIndex++) - { - var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - - var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); - - // Make the request. - OpenAIChatCompletion? chatCompletion = null; - OpenAIChatMessageContent chatMessageContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chat, chatExecutionSettings)) - { - try - { - chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.DeploymentName).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; - - this.LogUsage(chatCompletion.Usage); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (chatCompletion != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(chatCompletion.Id) - .SetPromptTokenUsage(chatCompletion.Usage.InputTokens) - .SetCompletionTokenUsage(chatCompletion.Usage.OutputTokens); - } - throw; - } - - chatMessageContent = this.CreateChatMessageContent(chatCompletion); - activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); - } - - // If we don't want to attempt to invoke any functions, just return the result. - if (!toolCallingConfig.AutoInvoke) - { - return [chatMessageContent]; - } - - Debug.Assert(kernel is not null); - - // Get our single result and extract the function call information. If this isn't a function call, or if it is - // but we're unable to find the function or extract the relevant information, just return the single result. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - if (chatCompletion.ToolCalls.Count == 0) - { - return [chatMessageContent]; - } - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Tool requests: {Requests}", chatCompletion.ToolCalls.Count); - } - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); - } - - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. Also add the result message to the caller's chat - // history: if they don't want it, they can remove it, but this makes the data available, - // including metadata like usage. - chatForRequest.Add(CreateRequestMessage(chatCompletion)); - chat.Add(chatMessageContent); - - // We must send back a response for every tool call, regardless of whether we successfully executed it or not. - // If we successfully execute it, we'll add the result. If we don't, we'll add an error. - for (int toolCallIndex = 0; toolCallIndex < chatMessageContent.ToolCalls.Count; toolCallIndex++) - { - ChatToolCall functionToolCall = chatMessageContent.ToolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (functionToolCall.Kind != ChatToolCallKind.Function) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - OpenAIFunctionToolCall? openAIFunctionToolCall; - try - { - openAIFunctionToolCall = new(functionToolCall); - } - catch (JsonException) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) - { - Arguments = functionArgs, - RequestSequenceIndex = requestIndex, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = chatMessageContent.ToolCalls.Count - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); - - // If filter requested termination, returning latest function result. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - return [chat.Last()]; - } - } - } - } - - internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chat), - JsonSerializer.Serialize(executionSettings)); - } - - AzureOpenAIPromptExecutionSettings chatExecutionSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - StringBuilder? contentBuilder = null; - Dictionary? toolCallIdsByIndex = null; - Dictionary? functionNamesByIndex = null; - Dictionary? functionArgumentBuildersByIndex = null; - - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - - for (int requestIndex = 0; ; requestIndex++) - { - var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); - - var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); - - // Reset state - contentBuilder?.Clear(); - toolCallIdsByIndex?.Clear(); - functionNamesByIndex?.Clear(); - functionArgumentBuildersByIndex?.Clear(); - - // Stream the response. - IReadOnlyDictionary? metadata = null; - string? streamedName = null; - ChatMessageRole? streamedRole = default; - ChatFinishReason finishReason = default; - ChatToolCall[]? toolCalls = null; - FunctionCallContent[]? functionCallContents = null; - - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentName, ModelProvider, chat, chatExecutionSettings)) - { - // Make the request. - AsyncResultCollection response; - try - { - response = RunRequest(() => this.Client.GetChatClient(this.DeploymentName).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - StreamingChatCompletionUpdate chatCompletionUpdate = responseEnumerator.Current; - metadata = GetChatCompletionMetadata(chatCompletionUpdate); - streamedRole ??= chatCompletionUpdate.Role; - //streamedName ??= update.AuthorName; - finishReason = chatCompletionUpdate.FinishReason ?? default; - - // If we're intending to invoke function calls, we need to consume that function call information. - if (toolCallingConfig.AutoInvoke) - { - foreach (var contentPart in chatCompletionUpdate.ContentUpdate) - { - if (contentPart.Kind == ChatMessageContentPartKind.Text) - { - (contentBuilder ??= new()).Append(contentPart.Text); - } - } - - OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - } - - var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.DeploymentName, metadata); - - foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) - { - // Using the code below to distinguish and skip non - function call related updates. - // The Kind property of updates can't be reliably used because it's only initialized for the first update. - if (string.IsNullOrEmpty(functionCallUpdate.Id) && - string.IsNullOrEmpty(functionCallUpdate.FunctionName) && - string.IsNullOrEmpty(functionCallUpdate.FunctionArgumentsUpdate)) - { - continue; - } - - openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( - callId: functionCallUpdate.Id, - name: functionCallUpdate.FunctionName, - arguments: functionCallUpdate.FunctionArgumentsUpdate, - functionCallIndex: functionCallUpdate.Index)); - } - - streamedContents?.Add(openAIStreamingChatMessageContent); - yield return openAIStreamingChatMessageContent; - } - - // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToFunctionToolCalls( - ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - - // Translate all entries into FunctionCallContent instances for diagnostics purposes. - functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); - } - finally - { - activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); - await responseEnumerator.DisposeAsync(); - } - } - - // If we don't have a function to invoke, we're done. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - if (!toolCallingConfig.AutoInvoke || - toolCallIdsByIndex is not { Count: > 0 }) - { - yield break; - } - - // Get any response content that was streamed. - string content = contentBuilder?.ToString() ?? string.Empty; - - // Log the requests - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.FunctionName}({fcr.FunctionName})"))); - } - else if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); - } - - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. - chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); - - // Respond to each tooling request. - for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) - { - ChatToolCall toolCall = toolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (string.IsNullOrEmpty(toolCall.FunctionName)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - OpenAIFunctionToolCall? openAIFunctionToolCall; - try - { - openAIFunctionToolCall = new(toolCall); - } - catch (JsonException) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) - { - Arguments = functionArgs, - RequestSequenceIndex = requestIndex, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = toolCalls.Length - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); - - // If filter requested termination, returning latest function result and breaking request iteration loop. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - var lastChatMessage = chat.Last(); - - yield return new OpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); - yield break; - } - } - } - } - - internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - ChatHistory chat = CreateNewChat(prompt, chatSettings); - - await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) - { - yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); - } - } - - internal async Task> GetChatAsTextContentsAsync( - string text, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - AzureOpenAIPromptExecutionSettings chatSettings = AzureOpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ChatHistory chat = CreateNewChat(text, chatSettings); - return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) - .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) - .ToList(); - } - - /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionOptions options, OpenAIFunctionToolCall ftc) - { - IList tools = options.Tools; - for (int i = 0; i < tools.Count; i++) - { - if (tools[i].Kind == ChatToolKind.Function && - string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - /// - /// Create a new empty chat instance - /// - /// Optional chat instructions for the AI service - /// Execution settings - /// Chat object - private static ChatHistory CreateNewChat(string? text = null, AzureOpenAIPromptExecutionSettings? executionSettings = null) - { - var chat = new ChatHistory(); - - // If settings is not provided, create a new chat with the text as the system prompt - AuthorRole textRole = AuthorRole.System; - - if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) - { - chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); - textRole = AuthorRole.User; - } - - if (!string.IsNullOrWhiteSpace(text)) - { - chat.AddMessage(textRole, text!); - } - - return chat; - } - - private ChatCompletionOptions CreateChatCompletionOptions( - AzureOpenAIPromptExecutionSettings executionSettings, - ChatHistory chatHistory, - ToolCallingConfig toolCallingConfig, - Kernel? kernel) - { - var options = new ChatCompletionOptions - { - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - TopP = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - Seed = executionSettings.Seed, - User = executionSettings.User, - TopLogProbabilityCount = executionSettings.TopLogprobs, - IncludeLogProbabilities = executionSettings.Logprobs, - ResponseFormat = GetResponseFormat(executionSettings) ?? ChatResponseFormat.Text, - ToolChoice = toolCallingConfig.Choice - }; - - if (executionSettings.AzureChatDataSource is not null) - { -#pragma warning disable AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - options.AddDataSource(executionSettings.AzureChatDataSource); -#pragma warning restore AOAI001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. - } - - if (toolCallingConfig.Tools is { Count: > 0 } tools) - { - options.Tools.AddRange(tools); - } - - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.LogitBiases.Add(keyValue.Key, keyValue.Value); - } - } - - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } - - return options; - } - - private static List CreateChatCompletionMessages(AzureOpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) - { - List messages = []; - - if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) - { - messages.Add(new SystemChatMessage(executionSettings.ChatSystemPrompt)); - } - - foreach (var message in chatHistory) - { - messages.AddRange(CreateRequestMessages(message, executionSettings.ToolCallBehavior)); - } - - return messages; - } - - private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) - { - if (chatRole == ChatMessageRole.User) - { - return new UserChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.System) - { - return new SystemChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.Assistant) - { - return new AssistantChatMessage(tools, content) { ParticipantName = name }; - } - - throw new NotImplementedException($"Role {chatRole} is not implemented"); - } - - private static List CreateRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) - { - if (message.Role == AuthorRole.System) - { - return [new SystemChatMessage(message.Content) { ParticipantName = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Tool) - { - // Handling function results represented by the TextContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) - if (message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && - toolId?.ToString() is string toolIdString) - { - return [new ToolChatMessage(toolIdString, message.Content)]; - } - - // Handling function results represented by the FunctionResultContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - List? toolMessages = null; - foreach (var item in message.Items) - { - if (item is not FunctionResultContent resultContent) - { - continue; - } - - toolMessages ??= []; - - if (resultContent.Result is Exception ex) - { - toolMessages.Add(new ToolChatMessage(resultContent.CallId, $"Error: Exception while invoking function. {ex.Message}")); - continue; - } - - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); - - toolMessages.Add(new ToolChatMessage(resultContent.CallId, stringResult ?? string.Empty)); - } - - if (toolMessages is not null) - { - return toolMessages; - } - - throw new NotSupportedException("No function result provided in the tool message."); - } - - if (message.Role == AuthorRole.User) - { - if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) - { - return [new UserChatMessage(textContent.Text) { ParticipantName = message.AuthorName }]; - } - - return [new UserChatMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentPart)(item switch - { - TextContent textContent => ChatMessageContentPart.CreateTextMessageContentPart(textContent.Text), - ImageContent imageContent => GetImageContentItem(imageContent), - _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") - }))) - { ParticipantName = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Assistant) - { - var toolCalls = new List(); - - // Handling function calls supplied via either: - // ChatCompletionsToolCall.ToolCalls collection items or - // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as OpenAIChatMessageContent)?.ToolCalls; - if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) - { - tools = toolCallsObject as IEnumerable; - if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) - { - int length = array.GetArrayLength(); - var ftcs = new List(length); - for (int i = 0; i < length; i++) - { - JsonElement e = array[i]; - if (e.TryGetProperty("Id", out JsonElement id) && - e.TryGetProperty("Name", out JsonElement name) && - e.TryGetProperty("Arguments", out JsonElement arguments) && - id.ValueKind == JsonValueKind.String && - name.ValueKind == JsonValueKind.String && - arguments.ValueKind == JsonValueKind.String) - { - ftcs.Add(ChatToolCall.CreateFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); - } - } - tools = ftcs; - } - } - - if (tools is not null) - { - toolCalls.AddRange(tools); - } - - // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. - HashSet? functionCallIds = null; - foreach (var item in message.Items) - { - if (item is not FunctionCallContent callRequest) - { - continue; - } - - functionCallIds ??= new HashSet(toolCalls.Select(t => t.Id)); - - if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) - { - continue; - } - - var argument = JsonSerializer.Serialize(callRequest.Arguments); - - toolCalls.Add(ChatToolCall.CreateFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); - } - - // This check is necessary to prevent an exception that will be thrown if the toolCalls collection is empty. - // HTTP 400 (invalid_request_error:) [] should be non-empty - 'messages.3.tool_calls' - if (toolCalls.Count == 0) - { - return [new AssistantChatMessage(message.Content) { ParticipantName = message.AuthorName }]; - } - - return [new AssistantChatMessage(toolCalls, message.Content) { ParticipantName = message.AuthorName }]; - } - - throw new NotSupportedException($"Role {message.Role} is not supported."); - } - - private static ChatMessageContentPart GetImageContentItem(ImageContent imageContent) - { - if (imageContent.Data is { IsEmpty: false } data) - { - return ChatMessageContentPart.CreateImageMessageContentPart(BinaryData.FromBytes(data), imageContent.MimeType); - } - - if (imageContent.Uri is not null) - { - return ChatMessageContentPart.CreateImageMessageContentPart(imageContent.Uri); - } - - throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); - } - - private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) - { - if (completion.Role == ChatMessageRole.System) - { - return ChatMessage.CreateSystemMessage(completion.Content[0].Text); - } - - if (completion.Role == ChatMessageRole.Assistant) - { - return ChatMessage.CreateAssistantMessage(completion); - } - - if (completion.Role == ChatMessageRole.User) - { - return ChatMessage.CreateUserMessage(completion.Content); - } - - throw new NotSupportedException($"Role {completion.Role} is not supported."); - } - - private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) - { - var message = new OpenAIChatMessageContent(completion, this.DeploymentName, GetChatCompletionMetadata(completion)); - - message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); - - return message; - } - - private OpenAIChatMessageContent CreateChatMessageContent(ChatMessageRole chatRole, string content, ChatToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) - { - var message = new OpenAIChatMessageContent(chatRole, content, this.DeploymentName, toolCalls, metadata) - { - AuthorName = authorName, - }; - - if (functionCalls is not null) - { - message.Items.AddRange(functionCalls); - } - - return message; - } - - private List GetFunctionCallContents(IEnumerable toolCalls) - { - List result = []; - - foreach (var toolCall in toolCalls) - { - // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. - // This allows consumers to work with functions in an LLM-agnostic way. - if (toolCall.Kind == ChatToolCallKind.Function) - { - Exception? exception = null; - KernelArguments? arguments = null; - try - { - arguments = JsonSerializer.Deserialize(toolCall.FunctionArguments); - if (arguments is not null) - { - // Iterate over copy of the names to avoid mutating the dictionary while enumerating it - var names = arguments.Names.ToArray(); - foreach (var name in names) - { - arguments[name] = arguments[name]?.ToString(); - } - } - } - catch (JsonException ex) - { - exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", toolCall.FunctionName, toolCall.Id); - } - } - - var functionName = FunctionName.Parse(toolCall.FunctionName, OpenAIFunction.NameSeparator); - - var functionCallContent = new FunctionCallContent( - functionName: functionName.Name, - pluginName: functionName.PluginName, - id: toolCall.Id, - arguments: arguments) - { - InnerContent = toolCall, - Exception = exception - }; - - result.Add(functionCallContent); - } - } - - return result; - } - - private static void AddResponseMessage(List chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) - { - // Log any error - if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) - { - Debug.Assert(result is null); - logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); - } - - // Add the tool response message to the chat messages - result ??= errorMessage ?? string.Empty; - chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); - - // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); - - if (toolCall.Kind == ChatToolCallKind.Function) - { - // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. - // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(toolCall.FunctionName, OpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, toolCall.Id, result)); - } - - chat.Add(message); - } - - private static void ValidateMaxTokens(int? maxTokens) - { - if (maxTokens.HasValue && maxTokens < 1) - { - throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); - } - } - - /// - /// Captures usage details, including token information. - /// - /// Instance of with token usage details. - private void LogUsage(ChatTokenUsage usage) - { - if (usage is null) - { - this.Logger.LogDebug("Token usage information unavailable."); - return; - } - - if (this.Logger.IsEnabled(LogLevel.Information)) - { - this.Logger.LogInformation( - "Prompt tokens: {InputTokens}. Completion tokens: {OutputTokens}. Total tokens: {TotalTokens}.", - usage.InputTokens, usage.OutputTokens, usage.TotalTokens); - } - - s_promptTokensCounter.Add(usage.InputTokens); - s_completionTokensCounter.Add(usage.OutputTokens); - s_totalTokensCounter.Add(usage.TotalTokens); - } - - /// - /// Processes the function result. - /// - /// The result of the function call. - /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. - /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) - { - if (functionResult is string stringResult) - { - return stringResult; - } - - // This is an optimization to use ChatMessageContent content directly - // without unnecessary serialization of the whole message content class. - if (functionResult is ChatMessageContent chatMessageContent) - { - return chatMessageContent.ToString(); - } - - // For polymorphic serialization of unknown in advance child classes of the KernelContent class, - // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. - // For more details about the polymorphic serialization, see the article at: - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 -#pragma warning disable CS0618 // Type or member is obsolete - return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); -#pragma warning restore CS0618 // Type or member is obsolete - } - - /// - /// Executes auto function invocation filters and/or function itself. - /// This method can be moved to when auto function invocation logic will be extracted to common place. - /// - private static async Task OnAutoFunctionInvocationAsync( - Kernel kernel, - AutoFunctionInvocationContext context, - Func functionCallCallback) - { - await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); - - return context; - } - - /// - /// This method will execute auto function invocation filters and function recursively. - /// If there are no registered filters, just function will be executed. - /// If there are registered filters, filter on position will be executed. - /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. - /// Function will be always executed as last step after all filters. - /// - private static async Task InvokeFilterOrFunctionAsync( - IList? autoFunctionInvocationFilters, - Func functionCallCallback, - AutoFunctionInvocationContext context, - int index = 0) - { - if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) - { - await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, - (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); - } - else - { - await functionCallCallback(context).ConfigureAwait(false); - } - } - - private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, AzureOpenAIPromptExecutionSettings executionSettings, int requestIndex) - { - if (executionSettings.ToolCallBehavior is null) - { - return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); - } - - if (requestIndex >= executionSettings.ToolCallBehavior.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - - return new ToolCallingConfig(Tools: [s_nonInvocableFunctionTool], Choice: ChatToolChoice.None, AutoInvoke: false); - } - - var (tools, choice) = executionSettings.ToolCallBehavior.ConfigureOptions(kernel); - - bool autoInvoke = kernel is not null && - executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts > 0 && - s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } - - return new ToolCallingConfig( - Tools: tools ?? [s_nonInvocableFunctionTool], - Choice: choice ?? ChatToolChoice.None, - AutoInvoke: autoInvoke); - } - - private static ChatResponseFormat? GetResponseFormat(AzureOpenAIPromptExecutionSettings executionSettings) - { - switch (executionSettings.ResponseFormat) - { - case ChatResponseFormat formatObject: - // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. - return formatObject; - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - } - break; - } - - return null; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs deleted file mode 100644 index 20c4736f27c7..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.Embeddings.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using OpenAI.Embeddings; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Generates an embedding from the given . - /// - /// List of strings to generate embeddings for - /// The containing services, plugins, and other state for use throughout the operation. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The to monitor for cancellation requests. The default is . - /// List of embeddings - internal async Task>> GetEmbeddingsAsync( - IList data, - Kernel? kernel, - int? dimensions, - CancellationToken cancellationToken) - { - var result = new List>(data.Count); - - if (data.Count > 0) - { - var embeddingsOptions = new EmbeddingGenerationOptions() - { - Dimensions = dimensions - }; - - var response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.DeploymentName).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); - var embeddings = response.Value; - - if (embeddings.Count != data.Count) - { - throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); - } - - for (var i = 0; i < embeddings.Count; i++) - { - result.Add(embeddings[i].Vector); - } - } - - return result; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs deleted file mode 100644 index 5a5e1e9f7d9d..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToAudio.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ClientModel; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using OpenAI.Audio; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Generates an image with the provided configuration. - /// - /// Prompt to generate the image - /// Text to Audio execution settings for the prompt - /// Azure OpenAI model id - /// The to monitor for cancellation requests. The default is . - /// Url of the generated image - internal async Task> GetAudioContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - string? modelId, - CancellationToken cancellationToken) - { - Verify.NotNullOrWhiteSpace(prompt); - - OpenAITextToAudioExecutionSettings audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - - var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings.ResponseFormat); - - SpeechGenerationOptions options = new() - { - ResponseFormat = responseFormat, - Speed = audioExecutionSettings.Speed, - }; - - var deploymentOrModel = this.GetModelId(audioExecutionSettings, modelId); - - ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(deploymentOrModel).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); - - return [new AudioContent(response.Value.ToArray(), mimeType)]; - } - - private static GeneratedSpeechVoice GetGeneratedSpeechVoice(string? voice) - => voice?.ToUpperInvariant() switch - { - "ALLOY" => GeneratedSpeechVoice.Alloy, - "ECHO" => GeneratedSpeechVoice.Echo, - "FABLE" => GeneratedSpeechVoice.Fable, - "ONYX" => GeneratedSpeechVoice.Onyx, - "NOVA" => GeneratedSpeechVoice.Nova, - "SHIMMER" => GeneratedSpeechVoice.Shimmer, - _ => throw new NotSupportedException($"The voice '{voice}' is not supported."), - }; - - private static (GeneratedSpeechFormat? Format, string? MimeType) GetGeneratedSpeechFormatAndMimeType(string? format) - => format?.ToUpperInvariant() switch - { - "WAV" => (GeneratedSpeechFormat.Wav, "audio/wav"), - "MP3" => (GeneratedSpeechFormat.Mp3, "audio/mpeg"), - "OPUS" => (GeneratedSpeechFormat.Opus, "audio/opus"), - "FLAC" => (GeneratedSpeechFormat.Flac, "audio/flac"), - "AAC" => (GeneratedSpeechFormat.Aac, "audio/aac"), - "PCM" => (GeneratedSpeechFormat.Pcm, "audio/l16"), - null => (null, null), - _ => throw new NotSupportedException($"The format '{format}' is not supported.") - }; - - private string GetModelId(OpenAITextToAudioExecutionSettings executionSettings, string? modelId) - { - return - !string.IsNullOrWhiteSpace(modelId) ? modelId! : - !string.IsNullOrWhiteSpace(executionSettings.ModelId) ? executionSettings.ModelId! : - this.DeploymentName; - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs deleted file mode 100644 index fefa13203ba7..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Core/ClientCore.TextToImage.cs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ClientModel; -using System.Threading; -using System.Threading.Tasks; -using OpenAI.Images; - -namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with Azure OpenAI services. -/// -internal partial class ClientCore -{ - /// - /// Generates an image with the provided configuration. - /// - /// Prompt to generate the image - /// Width of the image - /// Height of the image - /// The to monitor for cancellation requests. The default is . - /// Url of the generated image - internal async Task GenerateImageAsync( - string prompt, - int width, - int height, - CancellationToken cancellationToken) - { - Verify.NotNullOrWhiteSpace(prompt); - - var size = new GeneratedImageSize(width, height); - - var imageOptions = new ImageGenerationOptions() - { - Size = size, - ResponseFormat = GeneratedImageFormat.Uri - }; - - ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(this.DeploymentName).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); - - return response.Value.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs index cb91a512e004..86fbc7ac59df 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIKernelBuilderExtensions.cs @@ -517,8 +517,8 @@ public static IKernelBuilder AddAzureOpenAIAudioToText( #endregion private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, AzureClientCore.GetAzureOpenAIClientOptions(httpClient)); private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, AzureClientCore.GetAzureOpenAIClientOptions(httpClient)); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs index c073624c2bb0..13d44f785212 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Extensions/AzureOpenAIServiceCollectionExtensions.cs @@ -489,8 +489,8 @@ public static IServiceCollection AddAzureOpenAIAudioToText( #endregion private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, AzureClientCore.GetAzureOpenAIClientOptions(httpClient)); private static AzureOpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetAzureOpenAIClientOptions(httpClient)); + new(new Uri(endpoint), credentials, AzureClientCore.GetAzureOpenAIClientOptions(httpClient)); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs index 991342398599..b8dfccdf06bf 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIAudioToTextService.cs @@ -20,10 +20,10 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAIAudioToTextService : IAudioToTextService { /// Core implementation shared by Azure OpenAI services. - private readonly ClientCore _core; + private readonly AzureClientCore _client; /// - public IReadOnlyDictionary Attributes => this._core.Attributes; + public IReadOnlyDictionary Attributes => this._client.Attributes; /// /// Initializes a new instance of the class. @@ -42,8 +42,8 @@ public AzureOpenAIAudioToTextService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// @@ -63,8 +63,8 @@ public AzureOpenAIAudioToTextService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// @@ -80,8 +80,8 @@ public AzureOpenAIAudioToTextService( string? modelId = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// @@ -90,5 +90,5 @@ public Task> GetTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); + => this._client.GetTextFromAudioContentsAsync(this._client.DeploymentName, content, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs index 61aad2714bfd..47cca54662bc 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAIChatCompletionService.cs @@ -19,7 +19,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; public sealed class AzureOpenAIChatCompletionService : IChatCompletionService, ITextGenerationService { /// Core implementation shared by Azure OpenAI clients. - private readonly ClientCore _core; + private readonly AzureClientCore _client; /// /// Initializes a new instance of the class. @@ -38,9 +38,9 @@ public AzureOpenAIChatCompletionService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._client = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// @@ -60,8 +60,8 @@ public AzureOpenAIChatCompletionService( HttpClient? httpClient = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// @@ -77,26 +77,26 @@ public AzureOpenAIChatCompletionService( string? modelId = null, ILoggerFactory? loggerFactory = null) { - this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); } /// - public IReadOnlyDictionary Attributes => this._core.Attributes; + public IReadOnlyDictionary Attributes => this._client.Attributes; /// public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + => this._client.GetChatMessageContentsAsync(this._client.DeploymentName, chatHistory, executionSettings, kernel, cancellationToken); /// public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + => this._client.GetStreamingChatMessageContentsAsync(this._client.DeploymentName, chatHistory, executionSettings, kernel, cancellationToken); /// public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); + => this._client.GetChatAsTextContentsAsync(this._client.DeploymentName, prompt, executionSettings, kernel, cancellationToken); /// public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); + => this._client.GetChatAsTextStreamingContentsAsync(this._client.DeploymentName, prompt, executionSettings, kernel, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs index d332174845cf..bcbcfbb67087 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextEmbeddingGenerationService.cs @@ -20,7 +20,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; [Experimental("SKEXP0010")] public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService { - private readonly ClientCore _core; + private readonly AzureClientCore _client; private readonly int? _dimensions; /// @@ -42,9 +42,9 @@ public AzureOpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + this._client = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); this._dimensions = dimensions; } @@ -68,9 +68,9 @@ public AzureOpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new(deploymentName, endpoint, credential, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + this._client = new(deploymentName, endpoint, credential, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); this._dimensions = dimensions; } @@ -90,15 +90,15 @@ public AzureOpenAITextEmbeddingGenerationService( ILoggerFactory? loggerFactory = null, int? dimensions = null) { - this._core = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); + this._client = new(deploymentName, azureOpenAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); + this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); this._dimensions = dimensions; } /// - public IReadOnlyDictionary Attributes => this._core.Attributes; + public IReadOnlyDictionary Attributes => this._client.Attributes; /// public Task>> GenerateEmbeddingsAsync( @@ -106,6 +106,6 @@ public Task>> GenerateEmbeddingsAsync( Kernel? kernel = null, CancellationToken cancellationToken = default) { - return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); + return this._client.GetEmbeddingsAsync(this._client.DeploymentName, data, kernel, this._dimensions, cancellationToken); } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs index b688f61263b9..0b9f98302a0b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToAudioService.cs @@ -16,13 +16,13 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; /// /// Azure OpenAI text-to-audio service. /// -[Experimental("SKEXP0001")] +[Experimental("SKEXP0010")] public sealed class AzureOpenAITextToAudioService : ITextToAudioService { /// /// Azure OpenAI text-to-audio client. /// - private readonly ClientCore _client; + private readonly AzureClientCore _client; /// /// Azure OpenAI model id. @@ -56,7 +56,7 @@ public AzureOpenAITextToAudioService( { var url = !string.IsNullOrWhiteSpace(httpClient?.BaseAddress?.AbsoluteUri) ? httpClient!.BaseAddress!.AbsoluteUri : endpoint; - var options = ClientCore.GetAzureOpenAIClientOptions( + var options = AzureClientCore.GetAzureOpenAIClientOptions( httpClient, AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#text-to-speech @@ -75,5 +75,13 @@ public Task> GetAudioContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetAudioContentsAsync(text, executionSettings, this._modelId, cancellationToken); + => this._client.GetAudioContentsAsync(this.GetModelId(executionSettings), text, executionSettings, cancellationToken); + + private string GetModelId(PromptExecutionSettings? executionSettings) + { + return + !string.IsNullOrWhiteSpace(this._modelId) ? this._modelId! : + !string.IsNullOrWhiteSpace(executionSettings?.ModelId) ? executionSettings!.ModelId! : + this._client.DeploymentName; + } } diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs index 4b1ebe7aafa5..b066cc4b3e66 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Services/AzureOpenAITextToImageService.cs @@ -20,7 +20,7 @@ namespace Microsoft.SemanticKernel.Connectors.AzureOpenAI; [Experimental("SKEXP0010")] public class AzureOpenAITextToImageService : ITextToImageService { - private readonly ClientCore _client; + private readonly AzureClientCore _client; /// public IReadOnlyDictionary Attributes => this._client.Attributes; @@ -52,7 +52,7 @@ public AzureOpenAITextToImageService( throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); } - var options = ClientCore.GetAzureOpenAIClientOptions( + var options = AzureClientCore.GetAzureOpenAIClientOptions( httpClient, AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // DALL-E 3 is supported in the latest API releases - https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#image-generation @@ -93,7 +93,7 @@ public AzureOpenAITextToImageService( throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); } - var options = ClientCore.GetAzureOpenAIClientOptions( + var options = AzureClientCore.GetAzureOpenAIClientOptions( httpClient, AzureOpenAIClientOptions.ServiceVersion.V2024_05_01_Preview); // DALL-E 3 is supported in the latest API releases - https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#image-generation @@ -132,7 +132,5 @@ public AzureOpenAITextToImageService( /// public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return this._client.GenerateImageAsync(description, width, height, cancellationToken); - } + => this._client.GenerateImageAsync(this._client.DeploymentName, description, width, height, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs index b6783adc4823..bf6caf1ee3f2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs @@ -90,7 +90,7 @@ public async Task ItAddOrganizationHeaderWhenProvidedAsync(bool organizationIdPr organizationId: (organizationIdProvided) ? "organization" : null, httpClient: client); - var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + var pipelineMessage = clientCore.Client!.Pipeline.CreateMessage(); pipelineMessage.Request.Method = "POST"; pipelineMessage.Request.Uri = new Uri("http://localhost"); pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); @@ -119,7 +119,7 @@ public async Task ItAddSemanticKernelHeadersOnEachRequestAsync() // Act var clientCore = new ClientCore(modelId: "model", apiKey: "test", httpClient: client); - var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + var pipelineMessage = clientCore.Client!.Pipeline.CreateMessage(); pipelineMessage.Request.Method = "POST"; pipelineMessage.Request.Uri = new Uri("http://localhost"); pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); @@ -153,7 +153,7 @@ public async Task ItDoNotAddSemanticKernelHeadersWhenOpenAIClientIsProvidedAsync NetworkTimeout = Timeout.InfiniteTimeSpan })); - var pipelineMessage = clientCore.Client.Pipeline.CreateMessage(); + var pipelineMessage = clientCore.Client!.Pipeline.CreateMessage(); pipelineMessage.Request.Method = "POST"; pipelineMessage.Request.Uri = new Uri("http://localhost"); pipelineMessage.Request.Content = BinaryContent.Create(new BinaryData("test")); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs index bdbee092d5e9..48ddee6955c8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs @@ -17,11 +17,13 @@ internal partial class ClientCore /// /// Generates an image with the provided configuration. /// + /// Model identifier /// Input audio to generate the text /// Audio-to-text execution settings for the prompt /// The to monitor for cancellation requests. The default is . /// Url of the generated image internal async Task> GetTextFromAudioContentsAsync( + string targetModel, AudioContent input, PromptExecutionSettings? executionSettings, CancellationToken cancellationToken) @@ -38,17 +40,17 @@ internal async Task> GetTextFromAudioContentsAsync( using var memoryStream = new MemoryStream(input.Data!.Value.ToArray()); - AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; + AudioTranscription responseData = (await RunRequestAsync(() => this.Client!.GetAudioClient(targetModel).TranscribeAudioAsync(memoryStream, audioExecutionSettings?.Filename, audioOptions)).ConfigureAwait(false)).Value; - return [new(responseData.Text, this.ModelId, metadata: GetResponseMetadata(responseData))]; + return [new(responseData.Text, targetModel, metadata: GetResponseMetadata(responseData))]; } /// - /// Converts to type. + /// Converts to type. /// - /// Instance of . + /// Instance of . /// Instance of . - private static AudioTranscriptionOptions? AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) + private static AudioTranscriptionOptions AudioOptionsFromExecutionSettings(OpenAIAudioToTextExecutionSettings executionSettings) => new() { Granularities = AudioTimestampGranularities.Default, diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs index ddcce86e00f7..5ad712255af5 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs @@ -26,8 +26,8 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// internal partial class ClientCore { - private const string ModelProvider = "openai"; - private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); + protected const string ModelProvider = "openai"; + protected record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, bool AutoInvoke); /// /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current @@ -45,23 +45,23 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made /// configurable should need arise. /// - private const int MaxInflightAutoInvokes = 128; + protected const int MaxInflightAutoInvokes = 128; /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); + protected static readonly ChatTool s_nonInvocableFunctionTool = ChatTool.CreateFunctionTool("NonInvocableTool"); /// Tracking for . - private static readonly AsyncLocal s_inflightAutoInvokes = new(); + protected static readonly AsyncLocal s_inflightAutoInvokes = new(); /// /// Instance of for metrics. /// - private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); + protected static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); /// /// Instance of to keep track of the number of prompt tokens used. /// - private static readonly Counter s_promptTokensCounter = + protected static readonly Counter s_promptTokensCounter = s_meter.CreateCounter( name: "semantic_kernel.connectors.openai.tokens.prompt", unit: "{token}", @@ -70,7 +70,7 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, /// /// Instance of to keep track of the number of completion tokens used. /// - private static readonly Counter s_completionTokensCounter = + protected static readonly Counter s_completionTokensCounter = s_meter.CreateCounter( name: "semantic_kernel.connectors.openai.tokens.completion", unit: "{token}", @@ -79,13 +79,13 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, /// /// Instance of to keep track of the total number of tokens used. /// - private static readonly Counter s_totalTokensCounter = + protected static readonly Counter s_totalTokensCounter = s_meter.CreateCounter( name: "semantic_kernel.connectors.openai.tokens.total", unit: "{token}", description: "Number of tokens used"); - private static Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) + protected virtual Dictionary GetChatCompletionMetadata(OpenAIChatCompletion completions) { return new Dictionary { @@ -100,7 +100,7 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, }; } - private static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) + protected static Dictionary GetChatCompletionMetadata(StreamingChatCompletionUpdate completionUpdate) { return new Dictionary { @@ -116,12 +116,14 @@ private record ToolCallingConfig(IList? Tools, ChatToolChoice Choice, /// /// Generate a new chat message /// + /// Model identifier /// Chat history /// Execution settings for the completion API. /// The containing services, plugins, and other state for use throughout the operation. /// Async cancellation token /// Generated chat message in string format internal async Task> GetChatMessageContentsAsync( + string targetModel, ChatHistory chat, PromptExecutionSettings? executionSettings, Kernel? kernel, @@ -129,7 +131,7 @@ internal async Task> GetChatMessageContentsAsy { Verify.NotNull(chat); - if (this.Logger.IsEnabled(LogLevel.Trace)) + if (this.Logger!.IsEnabled(LogLevel.Trace)) { this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", JsonSerializer.Serialize(chat), @@ -137,7 +139,7 @@ internal async Task> GetChatMessageContentsAsy } // Convert the incoming execution settings to OpenAI settings. - OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + OpenAIPromptExecutionSettings chatExecutionSettings = this.GetSpecializedExecutionSettings(executionSettings); ValidateMaxTokens(chatExecutionSettings.MaxTokens); @@ -152,11 +154,11 @@ internal async Task> GetChatMessageContentsAsy // Make the request. OpenAIChatCompletion? chatCompletion = null; OpenAIChatMessageContent chatMessageContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.ModelId, ModelProvider, chat, chatExecutionSettings)) + using (var activity = this.StartCompletionActivity(chat, chatExecutionSettings)) { try { - chatCompletion = (await RunRequestAsync(() => this.Client.GetChatClient(this.ModelId).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; + chatCompletion = (await RunRequestAsync(() => this.Client!.GetChatClient(targetModel).CompleteChatAsync(chatForRequest, chatOptions, cancellationToken)).ConfigureAwait(false)).Value; this.LogUsage(chatCompletion.Usage); } @@ -174,7 +176,7 @@ internal async Task> GetChatMessageContentsAsy throw; } - chatMessageContent = this.CreateChatMessageContent(chatCompletion); + chatMessageContent = this.CreateChatMessageContent(chatCompletion, targetModel); activity?.SetCompletionResponse([chatMessageContent], chatCompletion.Usage.InputTokens, chatCompletion.Usage.OutputTokens); } @@ -256,7 +258,7 @@ internal async Task> GetChatMessageContentsAsy // Now, invoke the function, and add the resulting tool call message to the chat options. FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat, chatMessageContent) { Arguments = functionArgs, RequestSequenceIndex = requestIndex, @@ -316,6 +318,7 @@ internal async Task> GetChatMessageContentsAsy } internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( + string targetModel, ChatHistory chat, PromptExecutionSettings? executionSettings, Kernel? kernel, @@ -323,14 +326,14 @@ internal async IAsyncEnumerable GetStreamingC { Verify.NotNull(chat); - if (this.Logger.IsEnabled(LogLevel.Trace)) + if (this.Logger!.IsEnabled(LogLevel.Trace)) { this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", JsonSerializer.Serialize(chat), JsonSerializer.Serialize(executionSettings)); } - OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + OpenAIPromptExecutionSettings chatExecutionSettings = this.GetSpecializedExecutionSettings(executionSettings); ValidateMaxTokens(chatExecutionSettings.MaxTokens); @@ -361,13 +364,13 @@ internal async IAsyncEnumerable GetStreamingC ChatToolCall[]? toolCalls = null; FunctionCallContent[]? functionCallContents = null; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.ModelId, ModelProvider, chat, chatExecutionSettings)) + using (var activity = this.StartCompletionActivity(chat, chatExecutionSettings)) { // Make the request. AsyncResultCollection response; try { - response = RunRequest(() => this.Client.GetChatClient(this.ModelId).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); + response = RunRequest(() => this.Client!.GetChatClient(targetModel).CompleteChatStreamingAsync(chatForRequest, chatOptions, cancellationToken)); } catch (Exception ex) when (activity is not null) { @@ -414,7 +417,7 @@ internal async IAsyncEnumerable GetStreamingC OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatCompletionUpdate.ToolCallUpdates, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); } - var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, this.ModelId, metadata); + var openAIStreamingChatMessageContent = new OpenAIStreamingChatMessageContent(chatCompletionUpdate, 0, targetModel, metadata); foreach (var functionCallUpdate in chatCompletionUpdate.ToolCallUpdates) { @@ -478,7 +481,8 @@ internal async IAsyncEnumerable GetStreamingC // Add the original assistant message to the chat messages; this is required for the service // to understand the tool call responses. chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - chat.Add(this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName)); + var chatMessageContent = this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName); + chat.Add(chatMessageContent); // Respond to each tooling request. for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) @@ -523,7 +527,7 @@ internal async IAsyncEnumerable GetStreamingC // Now, invoke the function, and add the resulting tool call message to the chat options. FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat) + AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat, chatMessageContent) { Arguments = functionArgs, RequestSequenceIndex = requestIndex, @@ -586,78 +590,52 @@ internal async IAsyncEnumerable GetStreamingC } internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( + string targetModel, string prompt, PromptExecutionSettings? executionSettings, Kernel? kernel, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + OpenAIPromptExecutionSettings chatSettings = this.GetSpecializedExecutionSettings(executionSettings); ChatHistory chat = CreateNewChat(prompt, chatSettings); - await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) + await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(targetModel, chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) { yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); } } internal async Task> GetChatAsTextContentsAsync( + string model, string text, PromptExecutionSettings? executionSettings, Kernel? kernel, CancellationToken cancellationToken = default) { - OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); + OpenAIPromptExecutionSettings chatSettings = this.GetSpecializedExecutionSettings(executionSettings); ChatHistory chat = CreateNewChat(text, chatSettings); - return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) + return (await this.GetChatMessageContentsAsync(model, chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) .ToList(); } - /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionOptions options, OpenAIFunctionToolCall ftc) - { - IList tools = options.Tools; - for (int i = 0; i < tools.Count; i++) - { - if (tools[i].Kind == ChatToolKind.Function && - string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - /// - /// Create a new empty chat instance + /// Returns a specialized execution settings object for the OpenAI chat completion service. /// - /// Optional chat instructions for the AI service - /// Execution settings - /// Chat object - private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null) - { - var chat = new ChatHistory(); - - // If settings is not provided, create a new chat with the text as the system prompt - AuthorRole textRole = AuthorRole.System; + /// Potential execution settings infer specialized. + /// Specialized settings + protected virtual OpenAIPromptExecutionSettings GetSpecializedExecutionSettings(PromptExecutionSettings? executionSettings) + => OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) - { - chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); - textRole = AuthorRole.User; - } - - if (!string.IsNullOrWhiteSpace(text)) - { - chat.AddMessage(textRole, text!); - } - - return chat; - } + /// + /// Start a chat completion activity for a given model. + /// The activity will be tagged with the a set of attributes specified by the semantic conventions. + /// + protected virtual Activity? StartCompletionActivity(ChatHistory chatHistory, PromptExecutionSettings settings) + => ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.ModelId, ModelProvider, chatHistory, settings); - private ChatCompletionOptions CreateChatCompletionOptions( + protected virtual ChatCompletionOptions CreateChatCompletionOptions( OpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory, ToolCallingConfig toolCallingConfig, @@ -702,6 +680,94 @@ private ChatCompletionOptions CreateChatCompletionOptions( return options; } + /// + /// Retrieves the response format based on the provided settings. + /// + /// Execution settings. + /// Chat response format + protected static ChatResponseFormat? GetResponseFormat(OpenAIPromptExecutionSettings executionSettings) + { + switch (executionSettings.ResponseFormat) + { + case ChatResponseFormat formatObject: + // If the response format is an OpenAI SDK ChatCompletionsResponseFormat, just pass it along. + return formatObject; + case string formatString: + // If the response format is a string, map the ones we know about, and ignore the rest. + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + break; + + case JsonElement formatElement: + // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. + // Handling only string formatElement. + if (formatElement.ValueKind == JsonValueKind.String) + { + string formatString = formatElement.GetString() ?? ""; + switch (formatString) + { + case "json_object": + return ChatResponseFormat.JsonObject; + + case "text": + return ChatResponseFormat.Text; + } + } + break; + } + + return null; + } + + /// Checks if a tool call is for a function that was defined. + private static bool IsRequestableTool(ChatCompletionOptions options, OpenAIFunctionToolCall ftc) + { + IList tools = options.Tools; + for (int i = 0; i < tools.Count; i++) + { + if (tools[i].Kind == ChatToolKind.Function && + string.Equals(tools[i].FunctionName, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + + /// + /// Create a new empty chat instance + /// + /// Optional chat instructions for the AI service + /// Execution settings + /// Chat object + private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null) + { + var chat = new ChatHistory(); + + // If settings is not provided, create a new chat with the text as the system prompt + AuthorRole textRole = AuthorRole.System; + + if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) + { + chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); + textRole = AuthorRole.User; + } + + if (!string.IsNullOrWhiteSpace(text)) + { + chat.AddMessage(textRole, text!); + } + + return chat; + } + private static List CreateChatCompletionMessages(OpenAIPromptExecutionSettings executionSettings, ChatHistory chatHistory) { List messages = []; @@ -909,9 +975,9 @@ private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) throw new NotSupportedException($"Role {completion.Role} is not supported."); } - private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion) + private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion, string targetModel) { - var message = new OpenAIChatMessageContent(completion, this.ModelId, GetChatCompletionMetadata(completion)); + var message = new OpenAIChatMessageContent(completion, targetModel, this.GetChatCompletionMetadata(completion)); message.Items.AddRange(this.GetFunctionCallContents(completion.ToolCalls)); @@ -962,7 +1028,7 @@ private List GetFunctionCallContents(IEnumerable= executionSettings.ToolCallBehavior.MaximumUseAttempts) { // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) + if (this.Logger!.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", executionSettings.ToolCallBehavior!.MaximumUseAttempts); } @@ -1141,7 +1207,7 @@ private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, OpenAIProm if (requestIndex >= executionSettings.ToolCallBehavior.MaximumAutoInvokeAttempts) { autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) + if (this.Logger!.IsEnabled(LogLevel.Debug)) { this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", executionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); } @@ -1152,44 +1218,4 @@ private ToolCallingConfig GetToolCallingConfiguration(Kernel? kernel, OpenAIProm Choice: choice ?? ChatToolChoice.None, AutoInvoke: autoInvoke); } - - private static ChatResponseFormat? GetResponseFormat(OpenAIPromptExecutionSettings executionSettings) - { - switch (executionSettings.ResponseFormat) - { - case ChatResponseFormat formatObject: - // If the response format is an OpenAI SDK ChatCompletionsResponseFormat, just pass it along. - return formatObject; - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - return ChatResponseFormat.JsonObject; - - case "text": - return ChatResponseFormat.Text; - } - } - break; - } - - return null; - } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs index aa15de012084..483c726fa959 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs @@ -23,12 +23,14 @@ internal partial class ClientCore /// /// Generates an embedding from the given . /// + /// Target model to generate embeddings from /// List of strings to generate embeddings for /// The containing services, plugins, and other state for use throughout the operation. /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. /// The to monitor for cancellation requests. The default is . /// List of embeddings internal async Task>> GetEmbeddingsAsync( + string targetModel, IList data, Kernel? kernel, int? dimensions, @@ -43,7 +45,7 @@ internal async Task>> GetEmbeddingsAsync( Dimensions = dimensions }; - ClientResult response = await RunRequestAsync(() => this.Client.GetEmbeddingClient(this.ModelId).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); + ClientResult response = await RunRequestAsync(() => this.Client!.GetEmbeddingClient(targetModel).GenerateEmbeddingsAsync(data, embeddingsOptions, cancellationToken)).ConfigureAwait(false); var embeddings = response.Value; if (embeddings.Count != data.Count) diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs index 4bf071bc3c26..c0fd15380dfb 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs @@ -17,26 +17,30 @@ internal partial class ClientCore /// /// Generates an image with the provided configuration. /// + /// Model identifier /// Prompt to generate the image /// Text to Audio execution settings for the prompt /// The to monitor for cancellation requests. The default is . /// Url of the generated image internal async Task> GetAudioContentsAsync( + string targetModel, string prompt, PromptExecutionSettings? executionSettings, CancellationToken cancellationToken) { Verify.NotNullOrWhiteSpace(prompt); - OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings?.ResponseFormat); + OpenAITextToAudioExecutionSettings audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); + + var (responseFormat, mimeType) = GetGeneratedSpeechFormatAndMimeType(audioExecutionSettings.ResponseFormat); + SpeechGenerationOptions options = new() { ResponseFormat = responseFormat, - Speed = audioExecutionSettings?.Speed, + Speed = audioExecutionSettings.Speed, }; - ClientResult response = await RunRequestAsync(() => this.Client.GetAudioClient(this.ModelId).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); + ClientResult response = await RunRequestAsync(() => this.Client!.GetAudioClient(targetModel).GenerateSpeechFromTextAsync(prompt, GetGeneratedSpeechVoice(audioExecutionSettings?.Voice), options, cancellationToken)).ConfigureAwait(false); return [new AudioContent(response.Value.ToArray(), mimeType)]; } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs index cb6a681ca0e1..ac6111088ebf 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs @@ -24,12 +24,14 @@ internal partial class ClientCore /// /// Generates an image with the provided configuration. /// + /// Model identifier /// Prompt to generate the image /// Width of the image /// Height of the image /// The to monitor for cancellation requests. The default is . /// Url of the generated image internal async Task GenerateImageAsync( + string? targetModel, string prompt, int width, int height, @@ -47,9 +49,9 @@ internal async Task GenerateImageAsync( // The model is not required by the OpenAI API and defaults to the DALL-E 2 server-side - https://platform.openai.com/docs/api-reference/images/create#images-create-model. // However, considering that the model is required by the OpenAI SDK and the ModelId property is optional, it defaults to DALL-E 2 in the line below. - var model = string.IsNullOrEmpty(this.ModelId) ? "dall-e-2" : this.ModelId; + targetModel = string.IsNullOrEmpty(targetModel) ? "dall-e-2" : targetModel!; - ClientResult response = await RunRequestAsync(() => this.Client.GetImageClient(model).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); + ClientResult response = await RunRequestAsync(() => this.Client!.GetImageClient(targetModel).GenerateImageAsync(prompt, imageOptions, cancellationToken)).ConfigureAwait(false); var generatedImage = response.Value; return generatedImage.ImageUri?.ToString() ?? throw new KernelException("The generated image is not in url format"); diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs index 08c617bf2e8b..64083aa99acc 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs @@ -56,22 +56,22 @@ internal partial class ClientCore /// /// Identifier of the default model to use /// - internal string ModelId { get; init; } = string.Empty; + protected internal string ModelId { get; init; } = string.Empty; /// /// Non-default endpoint for OpenAI API. /// - internal Uri? Endpoint { get; init; } + protected internal Uri? Endpoint { get; init; } /// /// Logger instance /// - internal ILogger Logger { get; init; } + protected internal ILogger? Logger { get; init; } /// /// OpenAI Client /// - internal OpenAIClient Client { get; } + protected internal OpenAIClient? Client { get; set; } /// /// Storage for AI service attributes. @@ -95,6 +95,17 @@ internal ClientCore( HttpClient? httpClient = null, ILogger? logger = null) { + // Empty constructor will be used when inherited by a specialized Client. + if (modelId is null + && apiKey is null + && organizationId is null + && endpoint is null + && httpClient is null + && logger is null) + { + return; + } + if (!string.IsNullOrWhiteSpace(modelId)) { this.ModelId = modelId!; @@ -161,7 +172,7 @@ internal ClientCore( /// Caller member name. Populated automatically by runtime. internal void LogActionDetails([CallerMemberName] string? callerMemberName = default) { - if (this.Logger.IsEnabled(LogLevel.Information)) + if (this.Logger!.IsEnabled(LogLevel.Information)) { this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.ModelId); } @@ -210,7 +221,7 @@ private static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient /// Type of the response. /// Request to invoke. /// Returns the response. - private static async Task RunRequestAsync(Func> request) + protected static async Task RunRequestAsync(Func> request) { try { @@ -228,7 +239,7 @@ private static async Task RunRequestAsync(Func> request) /// Type of the response. /// Request to invoke. /// Returns the response. - private static T RunRequest(Func request) + protected static T RunRequest(Func request) { try { @@ -240,7 +251,7 @@ private static T RunRequest(Func request) } } - private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + protected static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) { return new GenericActionPipelinePolicy((message) => { diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs index 585488d24f7f..331da48cc08c 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs @@ -65,5 +65,5 @@ public Task> GetTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetTextFromAudioContentsAsync(content, executionSettings, cancellationToken); + => this._client.GetTextFromAudioContentsAsync(this._client.ModelId, content, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs index f3a5dd7fd790..08de7612b078 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs @@ -128,7 +128,7 @@ public Task> GetChatMessageContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + => this._client.GetChatMessageContentsAsync(this._client.ModelId, chatHistory, executionSettings, kernel, cancellationToken); /// public IAsyncEnumerable GetStreamingChatMessageContentsAsync( @@ -136,7 +136,7 @@ public IAsyncEnumerable GetStreamingChatMessageCont PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); + => this._client.GetStreamingChatMessageContentsAsync(this._client.ModelId, chatHistory, executionSettings, kernel, cancellationToken); /// public Task> GetTextContentsAsync( @@ -144,7 +144,7 @@ public Task> GetTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); + => this._client.GetChatAsTextContentsAsync(this._client.ModelId, prompt, executionSettings, kernel, cancellationToken); /// public IAsyncEnumerable GetStreamingTextContentsAsync( @@ -152,5 +152,5 @@ public IAsyncEnumerable GetStreamingTextContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); + => this._client.GetChatAsTextStreamingContentsAsync(this._client.ModelId, prompt, executionSettings, kernel, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs index fbe17e21f398..ce3cdcab43b8 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs @@ -82,6 +82,6 @@ public Task>> GenerateEmbeddingsAsync( CancellationToken cancellationToken = default) { this._client.LogActionDetails(); - return this._client.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); + return this._client.GetEmbeddingsAsync(this._client.ModelId, data, kernel, this._dimensions, cancellationToken); } } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs index 5c5aba683e6e..93b5ede244fb 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs @@ -55,5 +55,5 @@ public Task> GetAudioContentsAsync( PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); + => this._client.GetAudioContentsAsync(this._client.ModelId, text, executionSettings, cancellationToken); } diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs index 5bbff66c761e..48953d56912b 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs @@ -55,5 +55,5 @@ public OpenAITextToImageService( /// public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._client.GenerateImageAsync(description, width, height, cancellationToken); + => this._client.GenerateImageAsync(this._client.ModelId, description, width, height, cancellationToken); } diff --git a/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs b/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs index ecd4f18a0c90..e091939f0cf3 100644 --- a/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs +++ b/dotnet/src/InternalUtilities/src/Diagnostics/ModelDiagnostics.cs @@ -39,7 +39,7 @@ internal static class ModelDiagnostics /// Start a text completion activity for a given model. /// The activity will be tagged with the a set of attributes specified by the semantic conventions. /// - public static Activity? StartCompletionActivity( + internal static Activity? StartCompletionActivity( Uri? endpoint, string modelName, string modelProvider, @@ -52,7 +52,7 @@ internal static class ModelDiagnostics /// Start a chat completion activity for a given model. /// The activity will be tagged with the a set of attributes specified by the semantic conventions. /// - public static Activity? StartCompletionActivity( + internal static Activity? StartCompletionActivity( Uri? endpoint, string modelName, string modelProvider, @@ -65,20 +65,20 @@ internal static class ModelDiagnostics /// Set the text completion response for a given activity. /// The activity will be enriched with the response attributes specified by the semantic conventions. /// - public static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) + internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) => SetCompletionResponse(activity, completions, promptTokens, completionTokens, completions => $"[{string.Join(", ", completions)}]"); /// /// Set the chat completion response for a given activity. /// The activity will be enriched with the response attributes specified by the semantic conventions. /// - public static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) + internal static void SetCompletionResponse(this Activity activity, IEnumerable completions, int? promptTokens = null, int? completionTokens = null) => SetCompletionResponse(activity, completions, promptTokens, completionTokens, ToOpenAIFormat); /// /// Notify the end of streaming for a given activity. /// - public static void EndStreaming( + internal static void EndStreaming( this Activity activity, IEnumerable? contents, IEnumerable? toolCalls = null, @@ -98,7 +98,7 @@ public static void EndStreaming( /// The activity to set the response id /// The response id /// The activity with the response id set for chaining - public static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); + internal static Activity SetResponseId(this Activity activity, string responseId) => activity.SetTag(ModelDiagnosticsTags.ResponseId, responseId); /// /// Set the prompt token usage for a given activity. @@ -106,7 +106,7 @@ public static void EndStreaming( /// The activity to set the prompt token usage /// The number of prompt tokens used /// The activity with the prompt token usage set for chaining - public static Activity SetPromptTokenUsage(this Activity activity, int promptTokens) => activity.SetTag(ModelDiagnosticsTags.PromptToken, promptTokens); + internal static Activity SetPromptTokenUsage(this Activity activity, int promptTokens) => activity.SetTag(ModelDiagnosticsTags.PromptToken, promptTokens); /// /// Set the completion token usage for a given activity. @@ -114,13 +114,13 @@ public static void EndStreaming( /// The activity to set the completion token usage /// The number of completion tokens used /// The activity with the completion token usage set for chaining - public static Activity SetCompletionTokenUsage(this Activity activity, int completionTokens) => activity.SetTag(ModelDiagnosticsTags.CompletionToken, completionTokens); + internal static Activity SetCompletionTokenUsage(this Activity activity, int completionTokens) => activity.SetTag(ModelDiagnosticsTags.CompletionToken, completionTokens); /// /// Check if model diagnostics is enabled /// Model diagnostics is enabled if either EnableModelDiagnostics or EnableSensitiveEvents is set to true and there are listeners. /// - public static bool IsModelDiagnosticsEnabled() + internal static bool IsModelDiagnosticsEnabled() { return (s_enableDiagnostics || s_enableSensitiveEvents) && s_activitySource.HasListeners(); } @@ -129,7 +129,7 @@ public static bool IsModelDiagnosticsEnabled() /// Check if sensitive events are enabled. /// Sensitive events are enabled if EnableSensitiveEvents is set to true and there are listeners. /// - public static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); + internal static bool IsSensitiveEventsEnabled() => s_enableSensitiveEvents && s_activitySource.HasListeners(); internal static bool HasListeners() => s_activitySource.HasListeners(); From 3396076c06b0cdb3d066b57ed332d3509122406a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 24 Jul 2024 09:40:38 -0700 Subject: [PATCH 114/226] Checkpoint --- dotnet/Directory.Packages.props | 2 +- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 2 +- ...eAdapter.cs => AssistantMessageFactory.cs} | 21 ++++++- .../Internal/AssistantRunOptionsFactory.cs | 8 ++- .../OpenAI/Internal/AssistantThreadActions.cs | 43 ++++++++++++- .../Internal/AssistantToolResourcesFactory.cs | 51 +++++++++++++++ .../OpenAI/Internal/OpenAIClientFactory.cs | 3 + .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 62 ++----------------- ...sts.cs => AssistantMessageFactoryTests.cs} | 27 ++++---- .../Connectors.OpenAI.csproj | 2 +- 10 files changed, 138 insertions(+), 83 deletions(-) rename dotnet/src/Agents/OpenAI/Internal/{AssistantMessageAdapter.cs => AssistantMessageFactory.cs} (67%) create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs rename dotnet/src/Agents/UnitTests/OpenAI/Internal/{AssistantMessageAdapterTests.cs => AssistantMessageFactoryTests.cs} (79%) diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 2da11cbc8882..1c1884adb7e7 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index a4cd9b8b9f57..fa73edc77cde 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -33,7 +33,7 @@ - + diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs similarity index 67% rename from dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs rename to dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs index 50b9b9e9547f..0814d9720ecf 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageAdapter.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -4,8 +4,19 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; -internal static class AssistantMessageAdapter +/// +/// Factory for creating based on . +/// Also able to produce . +/// +/// +/// Improves testability. +/// +internal static class AssistantMessageFactory { + /// + /// Produces based on . + /// + /// The message content. public static MessageCreationOptions CreateOptions(ChatMessageContent message) { MessageCreationOptions options = new(); @@ -21,7 +32,11 @@ public static MessageCreationOptions CreateOptions(ChatMessageContent message) return options; } - public static IEnumerable GetMessageContents(ChatMessageContent message, MessageCreationOptions options) + /// + /// Translates into enumeration of . + /// + /// The message content. + public static IEnumerable GetMessageContents(ChatMessageContent message) { foreach (KernelContent content in message.Items) { @@ -37,7 +52,7 @@ public static IEnumerable GetMessageContents(ChatMessageContent } //else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) //{ - // %%% BUG: https://github.com/openai/openai-dotnet/issues/135 + // SDK BUG - BAD SIGNATURE (https://github.com/openai/openai-dotnet/issues/135) // URI does not accept the format used for `DataUri` // Approach is inefficient anyway... // yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs index 97132677e8c7..03f0b5ca067a 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -4,6 +4,12 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; +/// +/// Factory for creating definition. +/// +/// +/// Improves testability. +/// internal static class AssistantRunOptionsFactory { /// @@ -25,7 +31,7 @@ public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition defin ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), - //ToolConstraint - Not Supported: https://github.com/microsoft/semantic-kernel/issues/6795 + //ToolConstraint - Not Currently Supported (https://github.com/microsoft/semantic-kernel/issues/6795) TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, }; diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 68e1f4e0ce0f..e8830e9106f7 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -33,6 +33,43 @@ internal static class AssistantThreadActions RunStatus.Cancelled, ]; + /// + /// Create a new assistant thread. + /// + /// %%% + /// %%% + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public static async Task CreateThreadAsync(AssistantClient client, OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) + { + ThreadCreationOptions createOptions = + new() + { + ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpterFileIds), + }; + + if (options?.Messages != null) + { + foreach (ChatMessageContent message in options.Messages) + { + ThreadInitializationMessage threadMessage = new(AssistantMessageFactory.GetMessageContents(message)); + createOptions.InitialMessages.Add(threadMessage); + } + } + + if (options?.Metadata != null) + { + foreach (KeyValuePair item in options.Metadata) + { + createOptions.Metadata[item.Key] = item.Value; + } + } + + AssistantThread thread = await client.CreateThreadAsync(createOptions, cancellationToken).ConfigureAwait(false); + + return thread.Id; + } + /// /// Create a message in the specified thread. /// @@ -48,11 +85,11 @@ public static async Task CreateMessageAsync(AssistantClient client, string threa return; } - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); await client.CreateMessageAsync( threadId, - AssistantMessageAdapter.GetMessageContents(message, options), + AssistantMessageFactory.GetMessageContents(message), options, cancellationToken).ConfigureAwait(false); } @@ -76,7 +113,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist if (!string.IsNullOrWhiteSpace(message.AssistantId) && !agentNames.TryGetValue(message.AssistantId, out assistantName)) { - Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // %%% BUG CANCEL TOKEN + Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) if (!string.IsNullOrWhiteSpace(assistant.Name)) { agentNames.Add(assistant.Id, assistant.Name); diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs new file mode 100644 index 000000000000..e7566a5db4f8 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using OpenAI.Assistants; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +/// +/// Factory for creating definition. +/// +/// +/// Improves testability. +/// +internal static class AssistantToolResourcesFactory +{ + /// + /// Produces a definition based on the provided parameters. + /// + /// An optional vector-store-id for the 'file_search' tool + /// An optionallist of file-identifiers for the 'code_interpreter' tool. + public static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds) + { + bool hasFileSearch = !string.IsNullOrWhiteSpace(vectorStoreId); + bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0; + + ToolResources? toolResources = null; + + if (hasFileSearch || hasCodeInterpreterFiles) + { + toolResources = + new ToolResources() + { + FileSearch = + hasFileSearch ? + new FileSearchToolResources() + { + VectorStoreIds = [vectorStoreId!], + } : + null, + CodeInterpreter = + hasCodeInterpreterFiles ? + new CodeInterpreterToolResources() + { + FileIds = (IList)codeInterpreterFileIds!, + } : + null, + }; + } + + return toolResources; + } +} diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index 0b32ae8e0411..4474da62115f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -8,6 +8,9 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; +/// +/// Factory for creating . +/// internal static class OpenAIClientFactory { /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 336e7ed001d1..b94c99bf1b87 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -116,7 +116,7 @@ public static async Task RetrieveAsync( AssistantClient client = CreateClient(config); // Retrieve the assistant - Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // %%% BUG CANCEL TOKEN + Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) // Instantiate the agent return @@ -132,7 +132,7 @@ public static async Task RetrieveAsync( /// The to monitor for cancellation requests. The default is . /// The thread identifier public Task CreateThreadAsync(CancellationToken cancellationToken = default) - => this.CreateThreadAsync(options: null, cancellationToken); + => AssistantThreadActions.CreateThreadAsync(this._client, options: null, cancellationToken); /// /// Create a new assistant thread. @@ -140,28 +140,8 @@ public Task CreateThreadAsync(CancellationToken cancellationToken = defa /// %%% /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) - { - ThreadCreationOptions createOptions = - new() - { - ToolResources = GenerateToolResources(options?.VectorStoreId, options?.CodeInterpterFileIds), - }; - - //options.InitialMessages, // %%% TODO - - if (options?.Metadata != null) - { - foreach (KeyValuePair item in options.Metadata) - { - createOptions.Metadata[item.Key] = item.Value; - } - } - - AssistantThread thread = await this._client.CreateThreadAsync(createOptions, cancellationToken).ConfigureAwait(false); - - return thread.Id; - } + public Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) + => AssistantThreadActions.CreateThreadAsync(this._client, options, cancellationToken); /// /// Create a new assistant thread. @@ -348,7 +328,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - ToolResources = GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpterFileIds : null), + ToolResources = AssistantToolResourcesFactory.GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpterFileIds : null), ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, Temperature = definition.Temperature, NucleusSamplingFactor = definition.TopP, @@ -381,38 +361,6 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss return assistantCreationOptions; } - private static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds) - { - bool hasFileSearch = !string.IsNullOrWhiteSpace(vectorStoreId); - bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0; - - ToolResources? toolResources = null; - - if (hasFileSearch || hasCodeInterpreterFiles) - { - toolResources = - new ToolResources() - { - FileSearch = - hasFileSearch ? - new FileSearchToolResources() - { - VectorStoreIds = [vectorStoreId!], - } : - null, - CodeInterpreter = - hasCodeInterpreterFiles ? - new CodeInterpreterToolResources() - { - FileIds = (IList)codeInterpreterFileIds!, - } : - null, - }; - } - - return toolResources; - } - private static AssistantClient CreateClient(OpenAIServiceConfiguration config) { OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs similarity index 79% rename from dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs rename to dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs index eaa00eb529eb..5b13a584deb0 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageAdapterTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -11,9 +11,9 @@ namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; /// -/// Unit testing of . +/// Unit testing of . /// -public class AssistantMessageAdapterTests +public class AssistantMessageFactoryTests { /// /// Verify options creation. @@ -25,7 +25,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsDefault() ChatMessageContent message = new(AuthorRole.User, "test"); // Create options - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Validate Assert.NotNull(options); @@ -46,7 +46,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty() }; // Create options - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Validate Assert.NotNull(options); @@ -72,7 +72,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() }; // Create options - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Validate Assert.NotNull(options); @@ -101,7 +101,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() }; // Create options - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Validate Assert.NotNull(options); @@ -118,8 +118,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() public void VerifyAssistantMessageAdapterGetMessageContentsWithText() { ChatMessageContent message = new(AuthorRole.User, items: [new TextContent("test")]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().Text); @@ -132,8 +131,7 @@ public void VerifyAssistantMessageAdapterGetMessageContentsWithText() public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() { ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new Uri("https://localhost/myimage.png"))]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageUrl); @@ -146,8 +144,7 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() public void VerifyAssistantMessageAdapterGetMessageWithImageData() { ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageUrl); @@ -160,8 +157,7 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageData() public void VerifyAssistantMessageAdapterGetMessageWithImageFile() { ChatMessageContent message = new(AuthorRole.User, items: [new FileReferenceContent("file-id")]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageFileId); @@ -182,8 +178,7 @@ public void VerifyAssistantMessageAdapterGetMessageWithAll() new ImageContent(new Uri("https://localhost/myimage.png")), new FileReferenceContent("file-id") ]); - MessageCreationOptions options = AssistantMessageAdapter.CreateOptions(message); - MessageContent[] contents = AssistantMessageAdapter.GetMessageContents(message, options).ToArray(); + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); Assert.NotNull(contents); Assert.Equal(3, contents.Length); } diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj index f873d8d9cd29..06237a66d39f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj @@ -29,6 +29,6 @@ - + From 511e6ca712953e756be33d319d3cc043f42e96c2 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 24 Jul 2024 16:56:37 -0700 Subject: [PATCH 115/226] Test Complete --- .../Agents/OpenAIAssistant_ChartMaker.cs | 4 +- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 2 +- dotnet/samples/Concepts/Concepts.csproj | 1 - .../GettingStartedWithAgents.csproj | 1 - .../OpenAI/Internal/AssistantThreadActions.cs | 4 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 +- .../OpenAI/OpenAIAssistantExecutionOptions.cs | 6 + .../OpenAI/OpenAIAssistantAgentTests.cs | 472 +++++++++++++----- 8 files changed, 348 insertions(+), 146 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 63ed511742f8..ce1f05a8b08b 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -37,7 +37,7 @@ await OpenAIAssistantAgent.CreateAsync( }); // Create a chat for agent interaction. - var chat = new AgentGroupChat(); + AgentGroupChat chat = new(); // Respond to user input try @@ -54,7 +54,7 @@ Others 23 373 156 552 Sum 426 1622 856 2904 """); - await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); // %%% WHY NOT ??? + await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); } finally { diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs index 646c1f244967..b3090007e0e4 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -28,7 +28,7 @@ await OpenAIAssistantAgent.CreateAsync( }); // Create a chat for agent interaction. - var chat = new AgentGroupChat(); + AgentGroupChat chat = new(); // Respond to user input try diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 5001100fae7d..4816c0b6257e 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -63,7 +63,6 @@ - diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index 99d086787951..b95bbd546d34 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -39,7 +39,6 @@ - diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index e8830e9106f7..1e393027e43d 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -36,8 +36,8 @@ internal static class AssistantThreadActions /// /// Create a new assistant thread. /// - /// %%% - /// %%% + /// The assistant client + /// The options for creating the thread /// The to monitor for cancellation requests. The default is . /// The thread identifier public static async Task CreateThreadAsync(AssistantClient client, OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index b94c99bf1b87..164eceb5aff5 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -17,7 +17,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// public sealed class OpenAIAssistantAgent : KernelAgent { - private const string OptionsMetadataKey = "__run_options"; + internal const string OptionsMetadataKey = "__run_options"; private readonly Assistant _assistant; private readonly AssistantClient _client; @@ -137,7 +137,7 @@ public Task CreateThreadAsync(CancellationToken cancellationToken = defa /// /// Create a new assistant thread. /// - /// %%% + /// The options for creating the thread /// The to monitor for cancellation requests. The default is . /// The thread identifier public Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index 2f87d326eb75..28b625d3eee8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -1,4 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + namespace Microsoft.SemanticKernel.Agents.OpenAI; /// @@ -12,21 +14,25 @@ public sealed class OpenAIAssistantExecutionOptions /// /// The maximum number of completion tokens that may be used over the course of the run. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxCompletionTokens { get; init; } /// /// The maximum number of prompt tokens that may be used over the course of the run. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxPromptTokens { get; init; } /// /// Enables parallel function calling during tool use. Enabled by default. /// Use this property to disable. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? ParallelToolCallsEnabled { get; init; } /// /// When set, the thread will be truncated to the N most recent messages in the thread. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? TruncationMessageCount { get; init; } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 8eab72d37805..9c1c789fa8e2 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -4,6 +4,8 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; @@ -36,25 +38,12 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() ModelName = "testmodel", }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); - - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(targetAzure: true), - definition); - - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.Null(agent.Instructions); - Assert.Null(agent.Name); - Assert.Null(agent.Description); - Assert.False(agent.IsDeleted); + await this.VerifyAgentCreationAsync(definition); } /// /// Verify the invocation and response of - /// for an agent with optional properties defined. + /// for an agent with name, instructions, and description. /// [Fact] public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() @@ -68,130 +57,207 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() Instructions = "testinstructions", }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentFull); + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with code-interpreter enabled. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + EnableCodeInterpreter = true, + }; - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.NotNull(agent.Instructions); - Assert.NotNull(agent.Name); - Assert.NotNull(agent.Description); - Assert.False(agent.IsDeleted); + await this.VerifyAgentCreationAsync(definition); } /// - /// %%% + /// Verify the invocation and response of + /// for an agent with code-interpreter files. /// [Fact] - public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() // %%% NAME + public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsync() { OpenAIAssistantDefinition definition = new() { ModelName = "testmodel", EnableCodeInterpreter = true, - VectorStoreId = "#vs", - Metadata = new Dictionary() { { "a", "1" } }, + CodeInterpterFileIds = ["file1", "file2"], }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with a vector-store-id (for file-search). + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + VectorStoreId = "#vs1", + }; - Assert.NotNull(agent); - Assert.Equal(2, agent.Tools.Count); - Assert.True(agent.Tools.OfType().Any()); - Assert.True(agent.Tools.OfType().Any()); - Assert.Equal("#vs", agent.Definition.VectorStoreId); - Assert.Null(agent.Definition.CodeInterpterFileIds); - Assert.NotNull(agent.Definition.Metadata); - Assert.NotEmpty(agent.Definition.Metadata); + await this.VerifyAgentCreationAsync(definition); } /// - /// %%% + /// Verify the invocation and response of + /// for an agent with metadata. /// [Fact] - public async Task VerifyOpenAIAssistantAgentCreationEverything2Async() // %%% NAME + public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() { OpenAIAssistantDefinition definition = new() { ModelName = "testmodel", - EnableCodeInterpreter = true, - CodeInterpterFileIds = ["file1", "file2"], - Metadata = new Dictionary() { { "a", "1" } }, + Metadata = new Dictionary() + { + { "a", "1" }, + { "b", "2" }, + }, }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with json-response mode enabled. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + EnableJsonResponse = true, + }; - Assert.NotNull(agent); - Assert.Equal(2, agent.Tools.Count); - Assert.True(agent.Tools.OfType().Any()); - Assert.True(agent.Tools.OfType().Any()); - //Assert.Null(agent.Definition.VectorStoreId); // %%% SETUP - //Assert.Null(agent.Definition.CodeInterpterFileIds); // %%% SETUP - Assert.NotNull(agent.Definition.Metadata); - Assert.NotEmpty(agent.Definition.Metadata); + await this.VerifyAgentCreationAsync(definition); } /// - /// %%% + /// Verify the invocation and response of + /// for an agent with temperature defined. /// [Fact] - public async Task VerifyOpenAIAssistantAgentCreationEverything3Async() // %%% NAME + public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + Temperature = 2.0F, + }; + + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with topP defined. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + TopP = 2.0F, + }; + + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with empty execution settings. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsync() { OpenAIAssistantDefinition definition = new() { ModelName = "testmodel", - EnableCodeInterpreter = false, - EnableJsonResponse = true, - CodeInterpterFileIds = ["file1", "file2"], - Metadata = new Dictionary() { { "a", "1" } }, ExecutionOptions = new(), }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with populated execution settings. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + ExecutionOptions = + new() + { + MaxCompletionTokens = 100, + ParallelToolCallsEnabled = false, + } + }; - Assert.NotNull(agent); - Assert.Equal(2, agent.Tools.Count); - Assert.True(agent.Tools.OfType().Any()); - Assert.True(agent.Tools.OfType().Any()); - //Assert.Null(agent.Definition.VectorStoreId); // %%% SETUP - //Assert.Null(agent.Definition.CodeInterpterFileIds); // %%% SETUP - Assert.NotNull(agent.Definition.Metadata); - Assert.NotEmpty(agent.Definition.Metadata); + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with execution settings and meta-data. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAndMetadataAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + ExecutionOptions = new(), + Metadata = new Dictionary() + { + { "a", "1" }, + { "b", "2" }, + }, + }; + + await this.VerifyAgentCreationAsync(definition); } /// /// Verify the invocation and response of . /// [Fact] - public async Task VerifyOpenAIAssistantAgentRetrieveAsync() + public async Task VerifyOpenAIAssistantAgentRetrievalAsync() { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); + OpenAIAssistantDefinition definition = + new() + { + ModelName = "testmodel", + }; + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); OpenAIAssistantAgent agent = await OpenAIAssistantAgent.RetrieveAsync( @@ -199,12 +265,7 @@ await OpenAIAssistantAgent.RetrieveAsync( this.CreateTestConfiguration(), "#id"); - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.Null(agent.Instructions); - Assert.Null(agent.Name); - Assert.Null(agent.Description); - Assert.False(agent.IsDeleted); + ValidateAgentDefinition(agent, definition); } /// @@ -432,6 +493,91 @@ public OpenAIAssistantAgentTests() this._emptyKernel = new Kernel(); } + private async Task VerifyAgentCreationAsync(OpenAIAssistantDefinition definition) + { + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + this._emptyKernel, + this.CreateTestConfiguration(), + definition); + + ValidateAgentDefinition(agent, definition); + } + + private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAssistantDefinition sourceDefinition) + { + // Verify fundamental state + Assert.NotNull(agent); + Assert.NotNull(agent.Id); + Assert.False(agent.IsDeleted); + Assert.NotNull(agent.Definition); + Assert.Equal(sourceDefinition.ModelName, agent.Definition.ModelName); + + // Verify core properties + Assert.Equal(sourceDefinition.Instructions ?? string.Empty, agent.Instructions); + Assert.Equal(sourceDefinition.Name ?? string.Empty, agent.Name); + Assert.Equal(sourceDefinition.Description ?? string.Empty, agent.Description); + + // Verify options + Assert.Equal(sourceDefinition.Temperature, agent.Definition.Temperature); + Assert.Equal(sourceDefinition.TopP, agent.Definition.TopP); + Assert.Equal(sourceDefinition.ExecutionOptions?.MaxCompletionTokens, agent.Definition.ExecutionOptions?.MaxCompletionTokens); + Assert.Equal(sourceDefinition.ExecutionOptions?.MaxPromptTokens, agent.Definition.ExecutionOptions?.MaxPromptTokens); + Assert.Equal(sourceDefinition.ExecutionOptions?.ParallelToolCallsEnabled, agent.Definition.ExecutionOptions?.ParallelToolCallsEnabled); + Assert.Equal(sourceDefinition.ExecutionOptions?.TruncationMessageCount, agent.Definition.ExecutionOptions?.TruncationMessageCount); + + // Verify tool definitions + int expectedToolCount = 0; + + bool hasCodeIterpreter = false; + if (sourceDefinition.EnableCodeInterpreter) + { + hasCodeIterpreter = true; + ++expectedToolCount; + } + + Assert.Equal(hasCodeIterpreter, agent.Tools.OfType().Any()); + + bool hasFileSearch = false; + if (!string.IsNullOrWhiteSpace(sourceDefinition.VectorStoreId)) + { + hasFileSearch = true; + ++expectedToolCount; + } + + Assert.Equal(hasFileSearch, agent.Tools.OfType().Any()); + + Assert.Equal(expectedToolCount, agent.Tools.Count); + + // Verify metadata + Assert.NotNull(agent.Definition.Metadata); + if (sourceDefinition.ExecutionOptions == null) + { + Assert.Equal(sourceDefinition.Metadata ?? new Dictionary(), agent.Definition.Metadata); + } + else // Additional metadata present when execution options are defined + { + Assert.Equal((sourceDefinition.Metadata?.Count ?? 0) + 1, agent.Definition.Metadata.Count); + + if (sourceDefinition.Metadata != null) + { + foreach (var (key, value) in sourceDefinition.Metadata) + { + string? targetValue = agent.Definition.Metadata[key]; + Assert.NotNull(targetValue); + Assert.Equal(value, targetValue); + } + } + } + + // Verify detail definition + Assert.Equal(sourceDefinition.VectorStoreId, agent.Definition.VectorStoreId); + Assert.Equal(sourceDefinition.CodeInterpterFileIds, agent.Definition.CodeInterpterFileIds); + + } + private Task CreateAgentAsync() { OpenAIAssistantDefinition definition = @@ -440,7 +586,7 @@ private Task CreateAgentAsync() ModelName = "testmodel", }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); return OpenAIAssistantAgent.CreateAsync( @@ -486,62 +632,114 @@ public void MyFunction(int index) private static class ResponseContent { - public const string CreateAgentSimple = - """ + public static string CreateAgentPayload(OpenAIAssistantDefinition definition) + { + StringBuilder builder = new(); + builder.AppendLine("{"); + builder.AppendLine(@" ""id"": ""asst_abc123"","); + builder.AppendLine(@" ""object"": ""assistant"","); + builder.AppendLine(@" ""created_at"": 1698984975,"); + builder.AppendLine(@$" ""name"": ""{definition.Name}"","); + builder.AppendLine(@$" ""description"": ""{definition.Description}"","); + builder.AppendLine(@$" ""instructions"": ""{definition.Instructions}"","); + builder.AppendLine(@$" ""model"": ""{definition.ModelName}"","); + + bool hasCodeInterpeter = definition.EnableCodeInterpreter; + bool hasCodeInterpeterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; + bool hasFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); + if (!hasCodeInterpeter && !hasFileSearch) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {} + builder.AppendLine(@" ""tools"": [],"); } - """; + else + { + builder.AppendLine(@" ""tools"": ["); - public const string CreateAgentFull = - """ + if (hasCodeInterpeter) + { + builder.Append(@$" {{ ""type"": ""code_interpreter"" }}{(hasFileSearch ? "," : string.Empty)}"); + } + + if (hasFileSearch) + { + builder.AppendLine(@" { ""type"": ""file_search"" }"); + } + + builder.AppendLine(" ],"); + } + + if (!hasCodeInterpeterFiles && !hasFileSearch) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": "testname", - "description": "testdescription", - "model": "gpt-4-turbo", - "instructions": "testinstructions", - "tools": [], - "file_ids": [], - "metadata": {} + builder.AppendLine(@" ""tool_resources"": {},"); } - """; + else + { + builder.AppendLine(@" ""tool_resources"": {"); - public const string CreateAgentWithEverything = - """ + if (hasCodeInterpeterFiles) + { + string fileIds = string.Join(",", definition.CodeInterpterFileIds!.Select(fileId => "\"" + fileId + "\"")); + builder.AppendLine(@$" ""code_interpreter"": {{ ""file_ids"": [{fileIds}] }}{(hasFileSearch ? "," : string.Empty)}"); + } + + if (hasFileSearch) + { + builder.AppendLine(@$" ""file_search"": {{ ""vector_store_ids"": [""{definition.VectorStoreId}""] }}"); + } + + builder.AppendLine(" },"); + } + + if (definition.Temperature.HasValue) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [ + builder.AppendLine(@$" ""temperature"": {definition.Temperature},"); + } + + if (definition.TopP.HasValue) + { + builder.AppendLine(@$" ""top_p"": {definition.TopP},"); + } + + bool hasExecutionOptions = definition.ExecutionOptions != null; + int metadataCount = (definition.Metadata?.Count ?? 0); + if (metadataCount == 0 && !hasExecutionOptions) + { + builder.AppendLine(@" ""metadata"": {}"); + } + else + { + int index = 0; + builder.AppendLine(@" ""metadata"": {"); + + if (hasExecutionOptions) { - "type": "code_interpreter" - }, + string serializedExecutionOptions = JsonSerializer.Serialize(definition.ExecutionOptions); + builder.AppendLine(@$" ""{OpenAIAssistantAgent.OptionsMetadataKey}"": ""{JsonEncodedText.Encode(serializedExecutionOptions)}""{(metadataCount > 0 ? "," : string.Empty)}"); + } + + if (metadataCount > 0) { - "type": "file_search" + foreach (var (key, value) in definition.Metadata!) + { + builder.AppendLine(@$" ""{key}"": ""{value}""{(index < metadataCount - 1 ? "," : string.Empty)}"); + ++index; + } } - ], + + builder.AppendLine(" }"); + } + + builder.AppendLine("}"); + + return builder.ToString(); + } + + public const string CreateAgentWithEverything = + """ + { "tool_resources": { - "file_search": { - "vector_store_ids": ["#vs"] - } + "file_search": { "vector_store_ids": ["#vs"] } }, - "metadata": {"a": "1"} } """; From 8ca0a9dbc391131a14c1109f14a6fdb3338ccd61 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 24 Jul 2024 17:11:44 -0700 Subject: [PATCH 116/226] More tests + typo fix --- .../OpenAI/OpenAIAssistantAgentTests.cs | 34 +++++++++++++++---- 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 9c1c789fa8e2..c83d69b560d0 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -284,6 +284,28 @@ public async Task VerifyOpenAIAssistantAgentDeleteAsync() await agent.DeleteAsync(); // Doesn't throw Assert.True(agent.IsDeleted); + + await Assert.ThrowsAsync(() => agent.AddChatMessageAsync("threadid", new(AuthorRole.User, "test"))); + await Assert.ThrowsAsync(() => agent.InvokeAsync("threadid").ToArrayAsync().AsTask()); + } + + /// + /// Verify the deletion of agent via . + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreateThreadAsync() + { + OpenAIAssistantAgent agent = await this.CreateAgentAsync(); + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + + string threadId = await agent.CreateThreadAsync(); + Assert.NotNull(threadId); + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + + threadId = await agent.CreateThreadAsync(new()); + Assert.NotNull(threadId); } /// @@ -644,10 +666,10 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) builder.AppendLine(@$" ""instructions"": ""{definition.Instructions}"","); builder.AppendLine(@$" ""model"": ""{definition.ModelName}"","); - bool hasCodeInterpeter = definition.EnableCodeInterpreter; - bool hasCodeInterpeterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; + bool hasCodeInterpreter = definition.EnableCodeInterpreter; + bool hasCodeInterpreterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; bool hasFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); - if (!hasCodeInterpeter && !hasFileSearch) + if (!hasCodeInterpreter && !hasFileSearch) { builder.AppendLine(@" ""tools"": [],"); } @@ -655,7 +677,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) { builder.AppendLine(@" ""tools"": ["); - if (hasCodeInterpeter) + if (hasCodeInterpreter) { builder.Append(@$" {{ ""type"": ""code_interpreter"" }}{(hasFileSearch ? "," : string.Empty)}"); } @@ -668,7 +690,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) builder.AppendLine(" ],"); } - if (!hasCodeInterpeterFiles && !hasFileSearch) + if (!hasCodeInterpreterFiles && !hasFileSearch) { builder.AppendLine(@" ""tool_resources"": {},"); } @@ -676,7 +698,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) { builder.AppendLine(@" ""tool_resources"": {"); - if (hasCodeInterpeterFiles) + if (hasCodeInterpreterFiles) { string fileIds = string.Join(",", definition.CodeInterpterFileIds!.Select(fileId => "\"" + fileId + "\"")); builder.AppendLine(@$" ""code_interpreter"": {{ ""file_ids"": [{fileIds}] }}{(hasFileSearch ? "," : string.Empty)}"); From bd56cd9c62e0d6448bf512e24a1b780a59e43209 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 24 Jul 2024 17:12:22 -0700 Subject: [PATCH 117/226] Blank line --- dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index c83d69b560d0..ddbace38d4d1 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -597,7 +597,6 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs // Verify detail definition Assert.Equal(sourceDefinition.VectorStoreId, agent.Definition.VectorStoreId); Assert.Equal(sourceDefinition.CodeInterpterFileIds, agent.Definition.CodeInterpterFileIds); - } private Task CreateAgentAsync() From 51ffe3aef0c2af53a55852a2ac41254344a5c21a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 24 Jul 2024 17:13:36 -0700 Subject: [PATCH 118/226] Typo --- .../Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index ddbace38d4d1..c61cc99093e4 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -553,14 +553,14 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs // Verify tool definitions int expectedToolCount = 0; - bool hasCodeIterpreter = false; + bool hasCodeInterpreter = false; if (sourceDefinition.EnableCodeInterpreter) { - hasCodeIterpreter = true; + hasCodeInterpreter = true; ++expectedToolCount; } - Assert.Equal(hasCodeIterpreter, agent.Tools.OfType().Any()); + Assert.Equal(hasCodeInterpreter, agent.Tools.OfType().Any()); bool hasFileSearch = false; if (!string.IsNullOrWhiteSpace(sourceDefinition.VectorStoreId)) From 3117d3cb67197db2485a1a21441f15fd7bf13778 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 25 Jul 2024 12:08:14 +0100 Subject: [PATCH 119/226] .Net: OpenAI V2 Migration - Decomission V1 Phase 01 (#7446) ### Motivation and Context Remove all references and files related to previous V1 project Resolves Partially - #6870 --- dotnet/SK-dotnet.sln | 35 +- .../sk-chatgpt-azure-function.csproj | 2 +- .../GettingStarted/GettingStarted.csproj | 4 +- .../Step4_Dependency_Injection.cs | 2 +- .../GettingStartedWithAgents.csproj | 1 - dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 1 + .../Agents/UnitTests/Agents.UnitTests.csproj | 1 - .../AzureOpenAIAudioToTextService.cs | 94 - .../OpenAIAudioToTextExecutionSettings.cs | 168 -- .../AudioToText/OpenAIAudioToTextService.cs | 76 - .../AzureSdk/AddHeaderRequestPolicy.cs | 20 - .../AzureSdk/AzureOpenAIClientCore.cs | 102 - .../AzureSdk/AzureOpenAITextToAudioClient.cs | 141 -- .../AzureOpenAIWithDataChatMessageContent.cs | 69 - ...enAIWithDataStreamingChatMessageContent.cs | 49 - .../AzureSdk/ChatHistoryExtensions.cs | 70 - .../Connectors.OpenAI/AzureSdk/ClientCore.cs | 1591 ------------- .../AzureSdk/CustomHostPipelinePolicy.cs | 23 - .../AzureSdk/OpenAIChatMessageContent.cs | 117 - .../AzureSdk/OpenAIClientCore.cs | 106 - .../AzureSdk/OpenAIFunction.cs | 182 -- .../AzureSdk/OpenAIFunctionToolCall.cs | 170 -- .../OpenAIKernelFunctionMetadataExtensions.cs | 54 - .../OpenAIPluginCollectionExtensions.cs | 62 - .../OpenAIStreamingChatMessageContent.cs | 87 - .../AzureSdk/OpenAIStreamingTextContent.cs | 51 - .../AzureSdk/OpenAITextToAudioClient.cs | 128 -- .../RequestFailedExceptionExtensions.cs | 38 - .../AzureOpenAIChatCompletionService.cs | 102 - .../OpenAIChatCompletionService.cs | 133 -- ...AzureOpenAIChatCompletionWithDataConfig.cs | 53 - ...zureOpenAIChatCompletionWithDataService.cs | 305 --- .../ChatWithDataMessage.cs | 18 - .../ChatWithDataRequest.cs | 71 - .../ChatWithDataResponse.cs | 57 - .../ChatWithDataStreamingResponse.cs | 64 - .../CompatibilitySuppressions.xml | 116 - .../Connectors.OpenAI.csproj | 34 - .../OpenAITextToImageClientCore.cs | 114 - .../Files/OpenAIFilePurpose.cs | 99 - .../Files/OpenAIFileReference.cs | 38 - .../Files/OpenAIFileService.cs | 333 --- .../OpenAIFileUploadExecutionSettings.cs | 35 - .../OpenAIMemoryBuilderExtensions.cs | 111 - .../OpenAIPromptExecutionSettings.cs | 432 ---- .../OpenAIServiceCollectionExtensions.cs | 2042 ----------------- ...ureOpenAITextEmbeddingGenerationService.cs | 111 - .../OpenAITextEmbeddingGenerationService.cs | 85 - .../AzureOpenAITextGenerationService.cs | 97 - .../OpenAITextGenerationService.cs | 77 - .../AzureOpenAITextToAudioService.cs | 63 - .../OpenAITextToAudioExecutionSettings.cs | 130 -- .../TextToAudio/OpenAITextToAudioService.cs | 61 - .../TextToAudio/TextToAudioRequest.cs | 26 - .../AzureOpenAITextToImageService.cs | 212 -- .../TextToImage/OpenAITextToImageService.cs | 117 - .../TextToImage/TextToImageRequest.cs | 42 - .../TextToImage/TextToImageResponse.cs | 44 - .../Connectors.OpenAI/ToolCallBehavior.cs | 269 --- .../Connectors.UnitTests.csproj | 42 +- .../MultipleHttpMessageHandlerStub.cs | 53 - .../OpenAI/AIServicesOpenAIExtensionsTests.cs | 88 - .../AzureOpenAIAudioToTextServiceTests.cs | 127 - ...OpenAIAudioToTextExecutionSettingsTests.cs | 122 - .../OpenAIAudioToTextServiceTests.cs | 85 - ...reOpenAIWithDataChatMessageContentTests.cs | 120 - ...ithDataStreamingChatMessageContentTests.cs | 61 - .../AzureSdk/OpenAIChatMessageContentTests.cs | 125 - .../AzureSdk/OpenAIFunctionToolCallTests.cs | 82 - .../OpenAIPluginCollectionExtensionsTests.cs | 76 - .../OpenAIStreamingTextContentTests.cs | 42 - .../RequestFailedExceptionExtensionsTests.cs | 78 - .../AzureOpenAIChatCompletionServiceTests.cs | 959 -------- .../OpenAIChatCompletionServiceTests.cs | 687 ------ .../AzureOpenAIChatCompletionWithDataTests.cs | 201 -- .../OpenAI/ChatHistoryExtensionsTests.cs | 46 - .../OpenAI/Files/OpenAIFileServiceTests.cs | 298 --- .../AutoFunctionInvocationFilterTests.cs | 752 ------ .../KernelFunctionMetadataExtensionsTests.cs | 257 --- .../FunctionCalling/OpenAIFunctionTests.cs | 189 -- .../OpenAIMemoryBuilderExtensionsTests.cs | 66 - .../OpenAIPromptExecutionSettingsTests.cs | 275 --- .../OpenAIServiceCollectionExtensionsTests.cs | 746 ------ .../OpenAI/OpenAITestHelper.cs | 20 - ...multiple_function_calls_test_response.json | 64 - ...on_single_function_call_test_response.json | 32 - ..._multiple_function_calls_test_response.txt | 9 - ...ing_single_function_call_test_response.txt | 3 - ...hat_completion_streaming_test_response.txt | 5 - .../chat_completion_test_response.json | 22 - ...tion_with_data_streaming_test_response.txt | 1 - ...at_completion_with_data_test_response.json | 28 - ...multiple_function_calls_test_response.json | 40 - ..._multiple_function_calls_test_response.txt | 5 - ...ext_completion_streaming_test_response.txt | 3 - .../text_completion_test_response.json | 19 - ...enAITextEmbeddingGenerationServiceTests.cs | 188 -- ...enAITextEmbeddingGenerationServiceTests.cs | 164 -- .../AzureOpenAITextGenerationServiceTests.cs | 210 -- .../OpenAITextGenerationServiceTests.cs | 113 - .../AzureOpenAITextToAudioServiceTests.cs | 130 -- ...OpenAITextToAudioExecutionSettingsTests.cs | 108 - .../OpenAITextToAudioServiceTests.cs | 129 -- .../AzureOpenAITextToImageTests.cs | 174 -- .../OpenAITextToImageServiceTests.cs | 89 - .../OpenAI/ToolCallBehaviorTests.cs | 249 -- ...Orchestration.Flow.IntegrationTests.csproj | 2 +- .../Functions.Prompty.UnitTests.csproj | 2 +- .../PromptyTest.cs | 4 +- .../Functions.UnitTests.csproj | 2 +- .../OpenApi/RestApiOperationTests.cs | 7 +- .../Connectors/OpenAI/AIServiceType.cs | 19 - .../Connectors/OpenAI/ChatHistoryTests.cs | 149 -- .../OpenAI/OpenAIAudioToTextTests.cs | 76 - .../OpenAI/OpenAICompletionTests.cs | 668 ------ .../OpenAI/OpenAIFileServiceTests.cs | 156 -- .../OpenAI/OpenAITextEmbeddingTests.cs | 108 - .../OpenAI/OpenAITextToAudioTests.cs | 65 - .../OpenAI/OpenAITextToImageTests.cs | 85 - .../Connectors/OpenAI/OpenAIToolsTests.cs | 852 ------- .../IntegrationTests/IntegrationTests.csproj | 2 +- .../Handlebars/HandlebarsPlannerTests.cs | 35 +- dotnet/src/IntegrationTests/PromptTests.cs | 10 +- .../SemanticKernel.MetaPackage.csproj | 2 +- .../Functions/KernelBuilderTests.cs | 7 +- .../SemanticKernel.UnitTests.csproj | 2 +- 126 files changed, 79 insertions(+), 18491 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AudioToText/AzureOpenAIAudioToTextService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextExecutionSettings.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AddHeaderRequestPolicy.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAITextToAudioClient.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ChatHistoryExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/CustomHostPipelinePolicy.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIClientCore.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunctionToolCall.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIPluginCollectionExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingChatMessageContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingTextContent.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAITextToAudioClient.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/RequestFailedExceptionExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/OpenAIChatCompletionService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataConfig.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataMessage.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataRequest.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataResponse.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataStreamingResponse.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/CustomClient/OpenAITextToImageClientCore.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/AzureOpenAITextGenerationService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/OpenAITextGenerationService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/AzureOpenAITextToAudioService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioExecutionSettings.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/TextToAudioRequest.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToImage/AzureOpenAITextToImageService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageRequest.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageResponse.cs delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/AzureOpenAIAudioToTextServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatHistoryExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/AutoFunctionInvocationFilterTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAITestHelper.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_multiple_function_calls_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_streaming_multiple_function_calls_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/AzureOpenAITextToAudioServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioExecutionSettingsTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/AzureOpenAITextToImageTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs delete mode 100644 dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/AIServiceType.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs delete mode 100644 dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 93936ced5bc9..3805151b3a33 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -62,8 +62,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Redis", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.Chroma", "src\Connectors\Connectors.Memory.Chroma\Connectors.Memory.Chroma.csproj", "{185E0CE8-C2DA-4E4C-A491-E8EB40316315}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAI", "src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj", "{AFA81EB7-F869-467D-8A90-744305D80AAC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.Abstractions", "src\SemanticKernel.Abstractions\SemanticKernel.Abstractions.csproj", "{627742DB-1E52-468A-99BD-6FF1A542D25B}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SemanticKernel.MetaPackage", "src\SemanticKernel.MetaPackage\SemanticKernel.MetaPackage.csproj", "{E3299033-EB81-4C4C-BCD9-E8DC40937969}" @@ -346,6 +344,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CreateChatGptPlugin", "CreateChatGptPlugin", "{F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "sk-chatgpt-azure-function", "samples\Demos\CreateChatGptPlugin\MathPlugin\azure-function\sk-chatgpt-azure-function.csproj", "{6B268108-2AB5-4607-B246-06AD8410E60E}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MathPlugin", "MathPlugin", "{4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -437,12 +443,6 @@ Global {185E0CE8-C2DA-4E4C-A491-E8EB40316315}.Publish|Any CPU.Build.0 = Publish|Any CPU {185E0CE8-C2DA-4E4C-A491-E8EB40316315}.Release|Any CPU.ActiveCfg = Release|Any CPU {185E0CE8-C2DA-4E4C-A491-E8EB40316315}.Release|Any CPU.Build.0 = Release|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Publish|Any CPU.ActiveCfg = Publish|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Publish|Any CPU.Build.0 = Publish|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AFA81EB7-F869-467D-8A90-744305D80AAC}.Release|Any CPU.Build.0 = Release|Any CPU {627742DB-1E52-468A-99BD-6FF1A542D25B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {627742DB-1E52-468A-99BD-6FF1A542D25B}.Debug|Any CPU.Build.0 = Debug|Any CPU {627742DB-1E52-468A-99BD-6FF1A542D25B}.Publish|Any CPU.ActiveCfg = Publish|Any CPU @@ -848,6 +848,18 @@ Global {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.ActiveCfg = Debug|Any CPU {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Publish|Any CPU.Build.0 = Debug|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6B268108-2AB5-4607-B246-06AD8410E60E}.Release|Any CPU.Build.0 = Release|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Publish|Any CPU.Build.0 = Debug|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -870,7 +882,6 @@ Global {C9F957FA-A70F-4A6D-8F95-23FCD7F4FB87} = {24503383-A8C4-4255-9998-28D70FE8E99A} {3720F5ED-FB4D-485E-8A93-CDE60DEF0805} = {24503383-A8C4-4255-9998-28D70FE8E99A} {185E0CE8-C2DA-4E4C-A491-E8EB40316315} = {24503383-A8C4-4255-9998-28D70FE8E99A} - {AFA81EB7-F869-467D-8A90-744305D80AAC} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {627742DB-1E52-468A-99BD-6FF1A542D25B} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {E3299033-EB81-4C4C-BCD9-E8DC40937969} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {078F96B4-09E1-4E0E-B214-F71A4F4BF633} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} @@ -934,7 +945,7 @@ Global {644A2F10-324D-429E-A1A3-887EAE64207F} = {6823CD5E-2ABE-41EB-B865-F86EC13F0CF9} {5D4C0700-BBB5-418F-A7B2-F392B9A18263} = {FA3720F1-C99A-49B2-9577-A940257098BF} {B04C26BC-A933-4A53-BE17-7875EB12E012} = {FA3720F1-C99A-49B2-9577-A940257098BF} - {E6204E79-EFBF-499E-9743-85199310A455} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {E6204E79-EFBF-499E-9743-85199310A455} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} {CBEEF941-AEC6-42A4-A567-B5641CEFBB87} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {E12E15F2-6819-46EA-8892-73E3D60BE76F} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {5C813F83-9FD8-462A-9B38-865CA01C384C} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} @@ -965,6 +976,10 @@ Global {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} + {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} + {4326A974-F027-4ABD-A220-382CC6BB0801} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Demos/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj b/dotnet/samples/Demos/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj index 3c6ca9a15470..805e10f7d5ac 100644 --- a/dotnet/samples/Demos/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj +++ b/dotnet/samples/Demos/CreateChatGptPlugin/MathPlugin/azure-function/sk-chatgpt-azure-function.csproj @@ -28,7 +28,7 @@ - + diff --git a/dotnet/samples/GettingStarted/GettingStarted.csproj b/dotnet/samples/GettingStarted/GettingStarted.csproj index bbfb30f31a72..81581e7b4d57 100644 --- a/dotnet/samples/GettingStarted/GettingStarted.csproj +++ b/dotnet/samples/GettingStarted/GettingStarted.csproj @@ -50,7 +50,7 @@ - + @@ -60,6 +60,6 @@ - + \ No newline at end of file diff --git a/dotnet/samples/GettingStarted/Step4_Dependency_Injection.cs b/dotnet/samples/GettingStarted/Step4_Dependency_Injection.cs index 15d90a3c7b53..dd39962d627a 100644 --- a/dotnet/samples/GettingStarted/Step4_Dependency_Injection.cs +++ b/dotnet/samples/GettingStarted/Step4_Dependency_Injection.cs @@ -41,7 +41,7 @@ private ServiceProvider BuildServiceProvider() collection.AddSingleton(new XunitLogger(this.Output)); var kernelBuilder = collection.AddKernel(); - kernelBuilder.Services.AddOpenAITextGeneration(TestConfiguration.OpenAI.ModelId, TestConfiguration.OpenAI.ApiKey); + kernelBuilder.Services.AddOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey); kernelBuilder.Plugins.AddFromType(); return collection.BuildServiceProvider(); diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index ea4decbf86bb..decbe920b28b 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -38,7 +38,6 @@ - diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 222ea5c5be88..22db4073d90a 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -28,6 +28,7 @@ + diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index d46a4ee0cd1e..27e1afcfa92c 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -34,7 +34,6 @@ - diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/AzureOpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/AzureOpenAIAudioToTextService.cs deleted file mode 100644 index 2e065876b779..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/AzureOpenAIAudioToTextService.cs +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI audio-to-text service. -/// -[Experimental("SKEXP0001")] -public sealed class AzureOpenAIAudioToTextService : IAudioToTextService -{ - /// Core implementation shared by Azure OpenAI services. - private readonly AzureOpenAIClientCore _core; - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - /// Creates an instance of the with API key auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIAudioToTextService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Creates an instance of the with AAD auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIAudioToTextService( - string deploymentName, - string endpoint, - TokenCredential credentials, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Creates an instance of the using the specified . - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIAudioToTextService( - string deploymentName, - OpenAIClient openAIClient, - string? modelId = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIAudioToTextService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public Task> GetTextContentsAsync( - AudioContent content, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - => this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextExecutionSettings.cs deleted file mode 100644 index ef7f5e54f7df..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextExecutionSettings.cs +++ /dev/null @@ -1,168 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Execution settings for OpenAI audio-to-text request. -/// -[Experimental("SKEXP0001")] -public sealed class OpenAIAudioToTextExecutionSettings : PromptExecutionSettings -{ - /// - /// Filename or identifier associated with audio data. - /// Should be in format {filename}.{extension} - /// - [JsonPropertyName("filename")] - public string Filename - { - get => this._filename; - - set - { - this.ThrowIfFrozen(); - this._filename = value; - } - } - - /// - /// An optional language of the audio data as two-letter ISO-639-1 language code (e.g. 'en' or 'es'). - /// - [JsonPropertyName("language")] - public string? Language - { - get => this._language; - - set - { - this.ThrowIfFrozen(); - this._language = value; - } - } - - /// - /// An optional text to guide the model's style or continue a previous audio segment. The prompt should match the audio language. - /// - [JsonPropertyName("prompt")] - public string? Prompt - { - get => this._prompt; - - set - { - this.ThrowIfFrozen(); - this._prompt = value; - } - } - - /// - /// The format of the transcript output, in one of these options: json, text, srt, verbose_json, or vtt. Default is 'json'. - /// - [JsonPropertyName("response_format")] - public string ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The sampling temperature, between 0 and 1. - /// Higher values like 0.8 will make the output more random, while lower values like 0.2 will make it more focused and deterministic. - /// If set to 0, the model will use log probability to automatically increase the temperature until certain thresholds are hit. - /// Default is 0. - /// - [JsonPropertyName("temperature")] - public float Temperature - { - get => this._temperature; - - set - { - this.ThrowIfFrozen(); - this._temperature = value; - } - } - - /// - /// Creates an instance of class with default filename - "file.mp3". - /// - public OpenAIAudioToTextExecutionSettings() - : this(DefaultFilename) - { - } - - /// - /// Creates an instance of class. - /// - /// Filename or identifier associated with audio data. Should be in format {filename}.{extension} - public OpenAIAudioToTextExecutionSettings(string filename) - { - this._filename = filename; - } - - /// - public override PromptExecutionSettings Clone() - { - return new OpenAIAudioToTextExecutionSettings(this.Filename) - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Temperature = this.Temperature, - ResponseFormat = this.ResponseFormat, - Language = this.Language, - Prompt = this.Prompt - }; - } - - /// - /// Converts to derived type. - /// - /// Instance of . - /// Instance of . - public static OpenAIAudioToTextExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings) - { - if (executionSettings is null) - { - return new OpenAIAudioToTextExecutionSettings(); - } - - if (executionSettings is OpenAIAudioToTextExecutionSettings settings) - { - return settings; - } - - var json = JsonSerializer.Serialize(executionSettings); - - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - - if (openAIExecutionSettings is not null) - { - return openAIExecutionSettings; - } - - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(OpenAIAudioToTextExecutionSettings)}", nameof(executionSettings)); - } - - #region private ================================================================================ - - private const string DefaultFilename = "file.mp3"; - - private float _temperature = 0; - private string _responseFormat = "json"; - private string _filename; - private string? _language; - private string? _prompt; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextService.cs deleted file mode 100644 index 3bebb4867af8..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AudioToText/OpenAIAudioToTextService.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI audio-to-text service. -/// -[Experimental("SKEXP0001")] -public sealed class OpenAIAudioToTextService : IAudioToTextService -{ - /// Core implementation shared by OpenAI services. - private readonly OpenAIClientCore _core; - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - /// Creates an instance of the with API key auth. - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIAudioToTextService( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new( - modelId: modelId, - apiKey: apiKey, - organization: organization, - httpClient: httpClient, - logger: loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization); - } - - /// - /// Creates an instance of the using the specified . - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIAudioToTextService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) - { - this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAIAudioToTextService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public Task> GetTextContentsAsync( - AudioContent content, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - => this._core.GetTextContentFromAudioAsync(content, executionSettings, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AddHeaderRequestPolicy.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AddHeaderRequestPolicy.cs deleted file mode 100644 index 89ecb3bef22b..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AddHeaderRequestPolicy.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.Core; -using Azure.Core.Pipeline; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Helper class to inject headers into Azure SDK HTTP pipeline -/// -internal sealed class AddHeaderRequestPolicy(string headerName, string headerValue) : HttpPipelineSynchronousPolicy -{ - private readonly string _headerName = headerName; - private readonly string _headerValue = headerValue; - - public override void OnSendingRequest(HttpMessage message) - { - message.Request.Headers.Add(this._headerName, this._headerValue); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs deleted file mode 100644 index be0428faa799..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIClientCore.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using Azure; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Core implementation for Azure OpenAI clients, providing common functionality and properties. -/// -internal sealed class AzureOpenAIClientCore : ClientCore -{ - /// - /// Gets the key used to store the deployment name in the dictionary. - /// - public static string DeploymentNameKey => "DeploymentName"; - - /// - /// OpenAI / Azure OpenAI Client - /// - internal override OpenAIClient Client { get; } - - /// - /// Initializes a new instance of the class using API Key authentication. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - string endpoint, - string apiKey, - HttpClient? httpClient = null, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - Verify.NotNullOrWhiteSpace(apiKey); - - var options = GetOpenAIClientOptions(httpClient); - - this.DeploymentOrModelName = deploymentName; - this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, new AzureKeyCredential(apiKey), options); - } - - /// - /// Initializes a new instance of the class supporting AAD authentication. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credential, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - string endpoint, - TokenCredential credential, - HttpClient? httpClient = null, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - - var options = GetOpenAIClientOptions(httpClient); - - this.DeploymentOrModelName = deploymentName; - this.Endpoint = new Uri(endpoint); - this.Client = new OpenAIClient(this.Endpoint, credential, options); - } - - /// - /// Initializes a new instance of the class using the specified OpenAIClient. - /// Note: instances created this way might not have the default diagnostics settings, - /// it's up to the caller to configure the client. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAIClientCore( - string deploymentName, - OpenAIClient openAIClient, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNull(openAIClient); - - this.DeploymentOrModelName = deploymentName; - this.Client = openAIClient; - - this.AddAttribute(DeploymentNameKey, deploymentName); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAITextToAudioClient.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAITextToAudioClient.cs deleted file mode 100644 index dd02ddd0ebee..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAITextToAudioClient.cs +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Http; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI text-to-audio client for HTTP operations. -/// -[Experimental("SKEXP0001")] -internal sealed class AzureOpenAITextToAudioClient -{ - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - - private readonly string _deploymentName; - private readonly string _endpoint; - private readonly string _apiKey; - private readonly string? _modelId; - - /// - /// Storage for AI service attributes. - /// - internal Dictionary Attributes { get; } = []; - - /// - /// Creates an instance of the with API key auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal AzureOpenAITextToAudioClient( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILogger? logger = null) - { - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - Verify.NotNullOrWhiteSpace(apiKey); - - this._deploymentName = deploymentName; - this._endpoint = endpoint; - this._apiKey = apiKey; - this._modelId = modelId; - - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._logger = logger ?? NullLogger.Instance; - } - - internal async Task> GetAudioContentsAsync( - string text, - PromptExecutionSettings? executionSettings, - CancellationToken cancellationToken) - { - OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - - Verify.NotNullOrWhiteSpace(audioExecutionSettings?.Voice); - - var modelId = this.GetModelId(audioExecutionSettings); - - using var request = this.GetRequest(text, modelId, audioExecutionSettings); - using var response = await this.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - var data = await response.Content.ReadAsByteArrayAndTranslateExceptionAsync().ConfigureAwait(false); - - return [new(data, modelId)]; - } - - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } - } - - #region private - - private async Task SendRequestAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); - request.Headers.Add("Api-Key", this._apiKey); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureOpenAITextToAudioClient))); - - try - { - return await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - } - catch (HttpOperationException ex) - { - this._logger.LogError( - "Error occurred on text-to-audio request execution: {ExceptionMessage}", ex.Message); - - throw; - } - } - - private HttpRequestMessage GetRequest(string text, string modelId, OpenAITextToAudioExecutionSettings executionSettings) - { - const string DefaultApiVersion = "2024-02-15-preview"; - - var baseUrl = !string.IsNullOrWhiteSpace(this._httpClient.BaseAddress?.AbsoluteUri) ? - this._httpClient.BaseAddress!.AbsoluteUri : - this._endpoint; - - var requestUrl = $"openai/deployments/{this._deploymentName}/audio/speech?api-version={DefaultApiVersion}"; - - var payload = new TextToAudioRequest(modelId, text, executionSettings.Voice) - { - ResponseFormat = executionSettings.ResponseFormat, - Speed = executionSettings.Speed - }; - - return HttpRequest.CreatePostRequest($"{baseUrl.TrimEnd('/')}/{requestUrl}", payload); - } - - private string GetModelId(OpenAITextToAudioExecutionSettings executionSettings) - { - return - !string.IsNullOrWhiteSpace(this._modelId) ? this._modelId! : - !string.IsNullOrWhiteSpace(executionSettings.ModelId) ? executionSettings.ModelId! : - this._deploymentName; - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs deleted file mode 100644 index 594b420bc5f2..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContent.cs +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI specialized with data chat message content -/// -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -public sealed class AzureOpenAIWithDataChatMessageContent : ChatMessageContent -{ - /// - /// Content from data source, including citations. - /// For more information see . - /// - public string? ToolContent { get; set; } - - /// - /// Initializes a new instance of the class. - /// - /// Azure Chat With Data Choice - /// The model ID used to generate the content - /// Additional metadata - internal AzureOpenAIWithDataChatMessageContent(ChatWithDataChoice chatChoice, string? modelId, IReadOnlyDictionary? metadata = null) - : base(default, string.Empty, modelId, chatChoice, System.Text.Encoding.UTF8, CreateMetadataDictionary(metadata)) - { - // An assistant message content must be present, otherwise the chat is not valid. - var chatMessage = chatChoice.Messages.FirstOrDefault(m => string.Equals(m.Role, AuthorRole.Assistant.Label, StringComparison.OrdinalIgnoreCase)) ?? - throw new ArgumentException("Chat is not valid. Chat message does not contain any messages with 'assistant' role."); - - this.Content = chatMessage.Content; - this.Role = new AuthorRole(chatMessage.Role); - - this.ToolContent = chatChoice.Messages.FirstOrDefault(message => message.Role.Equals(AuthorRole.Tool.Label, StringComparison.OrdinalIgnoreCase))?.Content; - ((Dictionary)this.Metadata!).Add(nameof(this.ToolContent), this.ToolContent); - } - - private static Dictionary CreateMetadataDictionary(IReadOnlyDictionary? metadata) - { - Dictionary newDictionary; - if (metadata is null) - { - // There's no existing metadata to clone; just allocate a new dictionary. - newDictionary = new Dictionary(1); - } - else if (metadata is IDictionary origMutable) - { - // Efficiently clone the old dictionary to a new one. - newDictionary = new Dictionary(origMutable); - } - else - { - // There's metadata to clone but we have to do so one item at a time. - newDictionary = new Dictionary(metadata.Count + 1); - foreach (var kvp in metadata) - { - newDictionary[kvp.Key] = kvp.Value; - } - } - - return newDictionary; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContent.cs deleted file mode 100644 index ebe57f446293..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContent.cs +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Text; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure Open AI WithData Specialized streaming chat message content. -/// -/// -/// Represents a chat message content chunk that was streamed from the remote model. -/// -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -public sealed class AzureOpenAIWithDataStreamingChatMessageContent : StreamingChatMessageContent -{ - /// - public string? FunctionName { get; set; } - - /// - public string? FunctionArgument { get; set; } - - /// - /// Create a new instance of the class. - /// - /// Azure message update representation from WithData apis - /// Index of the choice - /// The model ID used to generate the content - /// Additional metadata - internal AzureOpenAIWithDataStreamingChatMessageContent(ChatWithDataStreamingChoice choice, int choiceIndex, string modelId, IReadOnlyDictionary? metadata = null) : - base(AuthorRole.Assistant, null, choice, choiceIndex, modelId, Encoding.UTF8, metadata) - { - var message = choice.Messages.FirstOrDefault(this.IsValidMessage); - var messageContent = message?.Delta?.Content; - - this.Content = messageContent; - } - - private bool IsValidMessage(ChatWithDataStreamingMessage message) - { - return !message.EndTurn && - (message.Delta.Role is null || !message.Delta.Role.Equals(AuthorRole.Tool.Label, StringComparison.Ordinal)); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ChatHistoryExtensions.cs deleted file mode 100644 index b4466a30af90..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ChatHistoryExtensions.cs +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; - -namespace Microsoft.SemanticKernel; - -/// -/// Chat history extensions. -/// -public static class ChatHistoryExtensions -{ - /// - /// Add a message to the chat history at the end of the streamed message - /// - /// Target chat history - /// list of streaming message contents - /// Returns the original streaming results with some message processing - [Experimental("SKEXP0010")] - public static async IAsyncEnumerable AddStreamingMessageAsync(this ChatHistory chatHistory, IAsyncEnumerable streamingMessageContents) - { - List messageContents = []; - - // Stream the response. - StringBuilder? contentBuilder = null; - Dictionary? toolCallIdsByIndex = null; - Dictionary? functionNamesByIndex = null; - Dictionary? functionArgumentBuildersByIndex = null; - Dictionary? metadata = null; - AuthorRole? streamedRole = null; - string? streamedName = null; - - await foreach (var chatMessage in streamingMessageContents.ConfigureAwait(false)) - { - metadata ??= (Dictionary?)chatMessage.Metadata; - - if (chatMessage.Content is { Length: > 0 } contentUpdate) - { - (contentBuilder ??= new()).Append(contentUpdate); - } - - OpenAIFunctionToolCall.TrackStreamingToolingUpdate(chatMessage.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - - // Is always expected to have at least one chunk with the role provided from a streaming message - streamedRole ??= chatMessage.Role; - streamedName ??= chatMessage.AuthorName; - - messageContents.Add(chatMessage); - yield return chatMessage; - } - - if (messageContents.Count != 0) - { - var role = streamedRole ?? AuthorRole.Assistant; - - chatHistory.Add( - new OpenAIChatMessageContent( - role, - contentBuilder?.ToString() ?? string.Empty, - messageContents[0].ModelId!, - OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls(ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex), - metadata) - { AuthorName = streamedName }); - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs deleted file mode 100644 index 6cfcf4e3e459..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/ClientCore.cs +++ /dev/null @@ -1,1591 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Diagnostics.Metrics; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.AI.OpenAI; -using Azure.Core; -using Azure.Core.Pipeline; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Diagnostics; -using Microsoft.SemanticKernel.Http; - -#pragma warning disable CA2208 // Instantiate argument exceptions correctly - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Base class for AI clients that provides common functionality for interacting with OpenAI services. -/// -internal abstract class ClientCore -{ - private const string ModelProvider = "openai"; - private const int MaxResultsPerPrompt = 128; - - /// - /// The maximum number of auto-invokes that can be in-flight at any given time as part of the current - /// asynchronous chain of execution. - /// - /// - /// This is a fail-safe mechanism. If someone accidentally manages to set up execution settings in such a way that - /// auto-invocation is invoked recursively, and in particular where a prompt function is able to auto-invoke itself, - /// we could end up in an infinite loop. This const is a backstop against that happening. We should never come close - /// to this limit, but if we do, auto-invoke will be disabled for the current flow in order to prevent runaway execution. - /// With the current setup, the way this could possibly happen is if a prompt function is configured with built-in - /// execution settings that opt-in to auto-invocation of everything in the kernel, in which case the invocation of that - /// prompt function could advertize itself as a candidate for auto-invocation. We don't want to outright block that, - /// if that's something a developer has asked to do (e.g. it might be invoked with different arguments than its parent - /// was invoked with), but we do want to limit it. This limit is arbitrary and can be tweaked in the future and/or made - /// configurable should need arise. - /// - private const int MaxInflightAutoInvokes = 128; - - /// Singleton tool used when tool call count drops to 0 but we need to supply tools to keep the service happy. - private static readonly ChatCompletionsFunctionToolDefinition s_nonInvocableFunctionTool = new() { Name = "NonInvocableTool" }; - - /// Tracking for . - private static readonly AsyncLocal s_inflightAutoInvokes = new(); - - internal ClientCore(ILogger? logger = null) - { - this.Logger = logger ?? NullLogger.Instance; - } - - /// - /// Model Id or Deployment Name - /// - internal string DeploymentOrModelName { get; set; } = string.Empty; - - /// - /// OpenAI / Azure OpenAI Client - /// - internal abstract OpenAIClient Client { get; } - - internal Uri? Endpoint { get; set; } = null; - - /// - /// Logger instance - /// - internal ILogger Logger { get; set; } - - /// - /// Storage for AI service attributes. - /// - internal Dictionary Attributes { get; } = []; - - /// - /// Instance of for metrics. - /// - private static readonly Meter s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI"); - - /// - /// Instance of to keep track of the number of prompt tokens used. - /// - private static readonly Counter s_promptTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.prompt", - unit: "{token}", - description: "Number of prompt tokens used"); - - /// - /// Instance of to keep track of the number of completion tokens used. - /// - private static readonly Counter s_completionTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.completion", - unit: "{token}", - description: "Number of completion tokens used"); - - /// - /// Instance of to keep track of the total number of tokens used. - /// - private static readonly Counter s_totalTokensCounter = - s_meter.CreateCounter( - name: "semantic_kernel.connectors.openai.tokens.total", - unit: "{token}", - description: "Number of tokens used"); - - /// - /// Creates completions for the prompt and settings. - /// - /// The prompt to complete. - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// The to monitor for cancellation requests. The default is . - /// Completions generated by the remote model - internal async Task> GetTextResultsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - OpenAIPromptExecutionSettings textExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, OpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - ValidateMaxTokens(textExecutionSettings.MaxTokens); - - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - Completions? responseData = null; - List responseContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings)) - { - try - { - responseData = (await RunRequestAsync(() => this.Client.GetCompletionsAsync(options, cancellationToken)).ConfigureAwait(false)).Value; - if (responseData.Choices.Count == 0) - { - throw new KernelException("Text completions not found"); - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (responseData != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); - } - throw; - } - - responseContent = responseData.Choices.Select(choice => new TextContent(choice.Text, this.DeploymentOrModelName, choice, Encoding.UTF8, GetTextChoiceMetadata(responseData, choice))).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); - } - - this.LogUsage(responseData.Usage); - - return responseContent; - } - - internal async IAsyncEnumerable GetStreamingTextContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - OpenAIPromptExecutionSettings textExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings, OpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - ValidateMaxTokens(textExecutionSettings.MaxTokens); - - var options = CreateCompletionsOptions(prompt, textExecutionSettings, this.DeploymentOrModelName); - - using var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, prompt, textExecutionSettings); - - StreamingResponse response; - try - { - response = await RunRequestAsync(() => this.Client.GetCompletionsStreamingAsync(options, cancellationToken)).ConfigureAwait(false); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - Completions completions = responseEnumerator.Current; - foreach (Choice choice in completions.Choices) - { - var openAIStreamingTextContent = new OpenAIStreamingTextContent( - choice.Text, choice.Index, this.DeploymentOrModelName, choice, GetTextChoiceMetadata(completions, choice)); - streamedContents?.Add(openAIStreamingTextContent); - yield return openAIStreamingTextContent; - } - } - } - finally - { - activity?.EndStreaming(streamedContents); - await responseEnumerator.DisposeAsync(); - } - } - - private static Dictionary GetTextChoiceMetadata(Completions completions, Choice choice) - { - return new Dictionary(8) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, - { nameof(completions.Usage), completions.Usage }, - { nameof(choice.ContentFilterResults), choice.ContentFilterResults }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(choice.FinishReason), choice.FinishReason?.ToString() }, - - { nameof(choice.LogProbabilityModel), choice.LogProbabilityModel }, - { nameof(choice.Index), choice.Index }, - }; - } - - private static Dictionary GetChatChoiceMetadata(ChatCompletions completions, ChatChoice chatChoice) - { - return new Dictionary(12) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.PromptFilterResults), completions.PromptFilterResults }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, - { nameof(completions.Usage), completions.Usage }, - { nameof(chatChoice.ContentFilterResults), chatChoice.ContentFilterResults }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(chatChoice.FinishReason), chatChoice.FinishReason?.ToString() }, - - { nameof(chatChoice.FinishDetails), chatChoice.FinishDetails }, - { nameof(chatChoice.LogProbabilityInfo), chatChoice.LogProbabilityInfo }, - { nameof(chatChoice.Index), chatChoice.Index }, - { nameof(chatChoice.Enhancements), chatChoice.Enhancements }, - }; - } - - private static Dictionary GetResponseMetadata(StreamingChatCompletionsUpdate completions) - { - return new Dictionary(4) - { - { nameof(completions.Id), completions.Id }, - { nameof(completions.Created), completions.Created }, - { nameof(completions.SystemFingerprint), completions.SystemFingerprint }, - - // Serialization of this struct behaves as an empty object {}, need to cast to string to avoid it. - { nameof(completions.FinishReason), completions.FinishReason?.ToString() }, - }; - } - - private static Dictionary GetResponseMetadata(AudioTranscription audioTranscription) - { - return new Dictionary(3) - { - { nameof(audioTranscription.Language), audioTranscription.Language }, - { nameof(audioTranscription.Duration), audioTranscription.Duration }, - { nameof(audioTranscription.Segments), audioTranscription.Segments } - }; - } - - /// - /// Generates an embedding from the given . - /// - /// List of strings to generate embeddings for - /// The containing services, plugins, and other state for use throughout the operation. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The to monitor for cancellation requests. The default is . - /// List of embeddings - internal async Task>> GetEmbeddingsAsync( - IList data, - Kernel? kernel, - int? dimensions, - CancellationToken cancellationToken) - { - var result = new List>(data.Count); - - if (data.Count > 0) - { - var embeddingsOptions = new EmbeddingsOptions(this.DeploymentOrModelName, data) - { - Dimensions = dimensions - }; - - var response = await RunRequestAsync(() => this.Client.GetEmbeddingsAsync(embeddingsOptions, cancellationToken)).ConfigureAwait(false); - var embeddings = response.Value.Data; - - if (embeddings.Count != data.Count) - { - throw new KernelException($"Expected {data.Count} text embedding(s), but received {embeddings.Count}"); - } - - for (var i = 0; i < embeddings.Count; i++) - { - result.Add(embeddings[i].Embedding); - } - } - - return result; - } - - internal async Task> GetTextContentFromAudioAsync( - AudioContent content, - PromptExecutionSettings? executionSettings, - CancellationToken cancellationToken) - { - Verify.NotNull(content.Data); - var audioData = content.Data.Value; - if (audioData.IsEmpty) - { - throw new ArgumentException("Audio data cannot be empty", nameof(content)); - } - - OpenAIAudioToTextExecutionSettings? audioExecutionSettings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); - - Verify.ValidFilename(audioExecutionSettings?.Filename); - - var audioOptions = new AudioTranscriptionOptions - { - AudioData = BinaryData.FromBytes(audioData), - DeploymentName = this.DeploymentOrModelName, - Filename = audioExecutionSettings.Filename, - Language = audioExecutionSettings.Language, - Prompt = audioExecutionSettings.Prompt, - ResponseFormat = audioExecutionSettings.ResponseFormat, - Temperature = audioExecutionSettings.Temperature - }; - - AudioTranscription responseData = (await RunRequestAsync(() => this.Client.GetAudioTranscriptionAsync(audioOptions, cancellationToken)).ConfigureAwait(false)).Value; - - return [new(responseData.Text, this.DeploymentOrModelName, metadata: GetResponseMetadata(responseData))]; - } - - /// - /// Generate a new chat message - /// - /// Chat history - /// Execution settings for the completion API. - /// The containing services, plugins, and other state for use throughout the operation. - /// Async cancellation token - /// Generated chat message in string format - internal async Task> GetChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - // Convert the incoming execution settings to OpenAI settings. - OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - - // Create the Azure SDK ChatCompletionOptions instance from all available information. - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - - for (int requestIndex = 1; ; requestIndex++) - { - // Make the request. - ChatCompletions? responseData = null; - List responseContent; - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) - { - try - { - responseData = (await RunRequestAsync(() => this.Client.GetChatCompletionsAsync(chatOptions, cancellationToken)).ConfigureAwait(false)).Value; - this.LogUsage(responseData.Usage); - if (responseData.Choices.Count == 0) - { - throw new KernelException("Chat completions not found"); - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - if (responseData != null) - { - // Capture available metadata even if the operation failed. - activity - .SetResponseId(responseData.Id) - .SetPromptTokenUsage(responseData.Usage.PromptTokens) - .SetCompletionTokenUsage(responseData.Usage.CompletionTokens); - } - throw; - } - - responseContent = responseData.Choices.Select(chatChoice => this.GetChatMessage(chatChoice, responseData)).ToList(); - activity?.SetCompletionResponse(responseContent, responseData.Usage.PromptTokens, responseData.Usage.CompletionTokens); - } - - // If we don't want to attempt to invoke any functions, just return the result. - // Or if we are auto-invoking but we somehow end up with other than 1 choice even though only 1 was requested, similarly bail. - if (!autoInvoke || responseData.Choices.Count != 1) - { - return responseContent; - } - - Debug.Assert(kernel is not null); - - // Get our single result and extract the function call information. If this isn't a function call, or if it is - // but we're unable to find the function or extract the relevant information, just return the single result. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - ChatChoice resultChoice = responseData.Choices[0]; - OpenAIChatMessageContent result = this.GetChatMessage(resultChoice, responseData); - if (result.ToolCalls.Count == 0) - { - return [result]; - } - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Tool requests: {Requests}", result.ToolCalls.Count); - } - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", result.ToolCalls.OfType().Select(ftc => $"{ftc.Name}({ftc.Arguments})"))); - } - - // Add the original assistant message to the chatOptions; this is required for the service - // to understand the tool call responses. Also add the result message to the caller's chat - // history: if they don't want it, they can remove it, but this makes the data available, - // including metadata like usage. - chatOptions.Messages.Add(GetRequestMessage(resultChoice.Message)); - chat.Add(result); - - // We must send back a response for every tool call, regardless of whether we successfully executed it or not. - // If we successfully execute it, we'll add the result. If we don't, we'll add an error. - for (int toolCallIndex = 0; toolCallIndex < result.ToolCalls.Count; toolCallIndex++) - { - ChatCompletionsToolCall toolCall = result.ToolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (toolCall is not ChatCompletionsFunctionToolCall functionToolCall) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - OpenAIFunctionToolCall? openAIFunctionToolCall; - try - { - openAIFunctionToolCall = new(functionToolCall); - } - catch (JsonException) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat, result) - { - ToolCallId = toolCall.Id, - Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = result.ToolCalls.Count, - CancellationToken = cancellationToken - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatOptions, chat, null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); - - // If filter requested termination, returning latest function result. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - return [chat.Last()]; - } - } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } - } - } - - internal async IAsyncEnumerable GetStreamingChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - Verify.NotNull(chat); - - OpenAIPromptExecutionSettings chatExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ValidateMaxTokens(chatExecutionSettings.MaxTokens); - - bool autoInvoke = kernel is not null && chatExecutionSettings.ToolCallBehavior?.MaximumAutoInvokeAttempts > 0 && s_inflightAutoInvokes.Value < MaxInflightAutoInvokes; - ValidateAutoInvoke(autoInvoke, chatExecutionSettings.ResultsPerPrompt); - - var chatOptions = this.CreateChatCompletionsOptions(chatExecutionSettings, chat, kernel, this.DeploymentOrModelName); - - StringBuilder? contentBuilder = null; - Dictionary? toolCallIdsByIndex = null; - Dictionary? functionNamesByIndex = null; - Dictionary? functionArgumentBuildersByIndex = null; - - for (int requestIndex = 1; ; requestIndex++) - { - // Reset state - contentBuilder?.Clear(); - toolCallIdsByIndex?.Clear(); - functionNamesByIndex?.Clear(); - functionArgumentBuildersByIndex?.Clear(); - - // Stream the response. - IReadOnlyDictionary? metadata = null; - string? streamedName = null; - ChatRole? streamedRole = default; - CompletionsFinishReason finishReason = default; - ChatCompletionsFunctionToolCall[]? toolCalls = null; - FunctionCallContent[]? functionCallContents = null; - - using (var activity = ModelDiagnostics.StartCompletionActivity(this.Endpoint, this.DeploymentOrModelName, ModelProvider, chat, chatExecutionSettings)) - { - // Make the request. - StreamingResponse response; - try - { - response = await RunRequestAsync(() => this.Client.GetChatCompletionsStreamingAsync(chatOptions, cancellationToken)).ConfigureAwait(false); - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - var responseEnumerator = response.ConfigureAwait(false).GetAsyncEnumerator(); - List? streamedContents = activity is not null ? [] : null; - try - { - while (true) - { - try - { - if (!await responseEnumerator.MoveNextAsync()) - { - break; - } - } - catch (Exception ex) when (activity is not null) - { - activity.SetError(ex); - throw; - } - - StreamingChatCompletionsUpdate update = responseEnumerator.Current; - metadata = GetResponseMetadata(update); - streamedRole ??= update.Role; - streamedName ??= update.AuthorName; - finishReason = update.FinishReason ?? default; - - // If we're intending to invoke function calls, we need to consume that function call information. - if (autoInvoke) - { - if (update.ContentUpdate is { Length: > 0 } contentUpdate) - { - (contentBuilder ??= new()).Append(contentUpdate); - } - - OpenAIFunctionToolCall.TrackStreamingToolingUpdate(update.ToolCallUpdate, ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - } - - AuthorRole? role = null; - if (streamedRole.HasValue) - { - role = new AuthorRole(streamedRole.Value.ToString()); - } - - OpenAIStreamingChatMessageContent openAIStreamingChatMessageContent = - new(update, update.ChoiceIndex ?? 0, this.DeploymentOrModelName, metadata) - { - AuthorName = streamedName, - Role = role, - }; - - if (update.ToolCallUpdate is StreamingFunctionToolCallUpdate functionCallUpdate) - { - openAIStreamingChatMessageContent.Items.Add(new StreamingFunctionCallUpdateContent( - callId: functionCallUpdate.Id, - name: functionCallUpdate.Name, - arguments: functionCallUpdate.ArgumentsUpdate, - functionCallIndex: functionCallUpdate.ToolCallIndex)); - } - - streamedContents?.Add(openAIStreamingChatMessageContent); - yield return openAIStreamingChatMessageContent; - } - - // Translate all entries into ChatCompletionsFunctionToolCall instances. - toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( - ref toolCallIdsByIndex, ref functionNamesByIndex, ref functionArgumentBuildersByIndex); - - // Translate all entries into FunctionCallContent instances for diagnostics purposes. - functionCallContents = this.GetFunctionCallContents(toolCalls).ToArray(); - } - finally - { - activity?.EndStreaming(streamedContents, ModelDiagnostics.IsSensitiveEventsEnabled() ? functionCallContents : null); - await responseEnumerator.DisposeAsync(); - } - } - - // If we don't have a function to invoke, we're done. - // Note that we don't check the FinishReason and instead check whether there are any tool calls, as the service - // may return a FinishReason of "stop" even if there are tool calls to be made, in particular if a required tool - // is specified. - if (!autoInvoke || - toolCallIdsByIndex is not { Count: > 0 }) - { - yield break; - } - - // Get any response content that was streamed. - string content = contentBuilder?.ToString() ?? string.Empty; - - // Log the requests - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", toolCalls.Select(fcr => $"{fcr.Name}({fcr.Arguments})"))); - } - else if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); - } - - // Add the original assistant message to the chatOptions; this is required for the service - // to understand the tool call responses. - chatOptions.Messages.Add(GetRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); - - var chatMessageContent = this.GetChatMessage(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName); - chat.Add(chatMessageContent); - - // Respond to each tooling request. - for (int toolCallIndex = 0; toolCallIndex < toolCalls.Length; toolCallIndex++) - { - ChatCompletionsFunctionToolCall toolCall = toolCalls[toolCallIndex]; - - // We currently only know about function tool calls. If it's anything else, we'll respond with an error. - if (string.IsNullOrEmpty(toolCall.Name)) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); - continue; - } - - // Parse the function call arguments. - OpenAIFunctionToolCall? openAIFunctionToolCall; - try - { - openAIFunctionToolCall = new(toolCall); - } - catch (JsonException) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); - continue; - } - - // Make sure the requested function is one we requested. If we're permitting any kernel function to be invoked, - // then we don't need to check this, as it'll be handled when we look up the function in the kernel to be able - // to invoke it. If we're permitting only a specific list of functions, though, then we need to explicitly check. - if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && - !IsRequestableTool(chatOptions, openAIFunctionToolCall)) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); - continue; - } - - // Find the function in the kernel and populate the arguments. - if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) - { - AddResponseMessage(chatOptions, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); - continue; - } - - // Now, invoke the function, and add the resulting tool call message to the chat options. - FunctionResult functionResult = new(function) { Culture = kernel.Culture }; - AutoFunctionInvocationContext invocationContext = new(kernel, function, functionResult, chat, chatMessageContent) - { - ToolCallId = toolCall.Id, - Arguments = functionArgs, - RequestSequenceIndex = requestIndex - 1, - FunctionSequenceIndex = toolCallIndex, - FunctionCount = toolCalls.Length, - CancellationToken = cancellationToken - }; - - s_inflightAutoInvokes.Value++; - try - { - invocationContext = await OnAutoFunctionInvocationAsync(kernel, invocationContext, async (context) => - { - // Check if filter requested termination. - if (context.Terminate) - { - return; - } - - // Note that we explicitly do not use executionSettings here; those pertain to the all-up operation and not necessarily to any - // further calls made as part of this function invocation. In particular, we must not use function calling settings naively here, - // as the called function could in turn telling the model about itself as a possible candidate for invocation. - context.Result = await function.InvokeAsync(kernel, invocationContext.Arguments, cancellationToken: cancellationToken).ConfigureAwait(false); - }).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception e) -#pragma warning restore CA1031 // Do not catch general exception types - { - AddResponseMessage(chatOptions, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); - continue; - } - finally - { - s_inflightAutoInvokes.Value--; - } - - // Apply any changes from the auto function invocation filters context to final result. - functionResult = invocationContext.Result; - - object functionResultValue = functionResult.GetValue() ?? string.Empty; - var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - - AddResponseMessage(chatOptions, chat, stringResult, errorMessage: null, toolCall, this.Logger); - - // If filter requested termination, returning latest function result and breaking request iteration loop. - if (invocationContext.Terminate) - { - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Filter requested termination of automatic function invocation."); - } - - var lastChatMessage = chat.Last(); - - yield return new OpenAIStreamingChatMessageContent(lastChatMessage.Role, lastChatMessage.Content); - yield break; - } - } - - // Update tool use information for the next go-around based on having completed another iteration. - Debug.Assert(chatExecutionSettings.ToolCallBehavior is not null); - - // Set the tool choice to none. If we end up wanting to use tools, we'll reset it to the desired value. - chatOptions.ToolChoice = ChatCompletionsToolChoice.None; - chatOptions.Tools.Clear(); - - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts) - { - // Don't add any tools as we've reached the maximum attempts limit. - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum use ({MaximumUse}) reached; removing the tool.", chatExecutionSettings.ToolCallBehavior!.MaximumUseAttempts); - } - } - else - { - // Regenerate the tool list as necessary. The invocation of the function(s) could have augmented - // what functions are available in the kernel. - chatExecutionSettings.ToolCallBehavior.ConfigureOptions(kernel, chatOptions); - } - - // Having already sent tools and with tool call information in history, the service can become unhappy ("[] is too short - 'tools'") - // if we don't send any tools in subsequent requests, even if we say not to use any. - if (chatOptions.ToolChoice == ChatCompletionsToolChoice.None) - { - Debug.Assert(chatOptions.Tools.Count == 0); - chatOptions.Tools.Add(s_nonInvocableFunctionTool); - } - - // Disable auto invocation if we've exceeded the allowed limit. - if (requestIndex >= chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts) - { - autoInvoke = false; - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug("Maximum auto-invoke ({MaximumAutoInvoke}) reached.", chatExecutionSettings.ToolCallBehavior!.MaximumAutoInvokeAttempts); - } - } - } - } - - /// Checks if a tool call is for a function that was defined. - private static bool IsRequestableTool(ChatCompletionsOptions options, OpenAIFunctionToolCall ftc) - { - IList tools = options.Tools; - for (int i = 0; i < tools.Count; i++) - { - if (tools[i] is ChatCompletionsFunctionToolDefinition def && - string.Equals(def.Name, ftc.FullyQualifiedName, StringComparison.OrdinalIgnoreCase)) - { - return true; - } - } - - return false; - } - - internal async IAsyncEnumerable GetChatAsTextStreamingContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - ChatHistory chat = CreateNewChat(prompt, chatSettings); - - await foreach (var chatUpdate in this.GetStreamingChatMessageContentsAsync(chat, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) - { - yield return new StreamingTextContent(chatUpdate.Content, chatUpdate.ChoiceIndex, chatUpdate.ModelId, chatUpdate, Encoding.UTF8, chatUpdate.Metadata); - } - } - - internal async Task> GetChatAsTextContentsAsync( - string text, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - OpenAIPromptExecutionSettings chatSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(executionSettings); - - ChatHistory chat = CreateNewChat(text, chatSettings); - return (await this.GetChatMessageContentsAsync(chat, chatSettings, kernel, cancellationToken).ConfigureAwait(false)) - .Select(chat => new TextContent(chat.Content, chat.ModelId, chat.Content, Encoding.UTF8, chat.Metadata)) - .ToList(); - } - - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } - } - - /// Gets options to use for an OpenAIClient - /// Custom for HTTP requests. - /// Optional API version. - /// An instance of . - internal static OpenAIClientOptions GetOpenAIClientOptions(HttpClient? httpClient, OpenAIClientOptions.ServiceVersion? serviceVersion = null) - { - OpenAIClientOptions options = serviceVersion is not null ? - new(serviceVersion.Value) : - new(); - - options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(ClientCore))), HttpPipelinePosition.PerCall); - - if (httpClient is not null) - { - options.Transport = new HttpClientTransport(httpClient); - options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout - } - - return options; - } - - /// - /// Create a new empty chat instance - /// - /// Optional chat instructions for the AI service - /// Execution settings - /// Chat object - private static ChatHistory CreateNewChat(string? text = null, OpenAIPromptExecutionSettings? executionSettings = null) - { - var chat = new ChatHistory(); - - // If settings is not provided, create a new chat with the text as the system prompt - AuthorRole textRole = AuthorRole.System; - - if (!string.IsNullOrWhiteSpace(executionSettings?.ChatSystemPrompt)) - { - chat.AddSystemMessage(executionSettings!.ChatSystemPrompt!); - textRole = AuthorRole.User; - } - - if (!string.IsNullOrWhiteSpace(text)) - { - chat.AddMessage(textRole, text!); - } - - return chat; - } - - private static CompletionsOptions CreateCompletionsOptions(string text, OpenAIPromptExecutionSettings executionSettings, string deploymentOrModelName) - { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } - - var options = new CompletionsOptions - { - Prompts = { text.Replace("\r\n", "\n") }, // normalize line endings - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - Echo = false, - ChoicesPerPrompt = executionSettings.ResultsPerPrompt, - GenerationSampleCount = executionSettings.ResultsPerPrompt, - LogProbabilityCount = executionSettings.TopLogprobs, - User = executionSettings.User, - DeploymentName = deploymentOrModelName - }; - - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); - } - } - - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } - - return options; - } - - private ChatCompletionsOptions CreateChatCompletionsOptions( - OpenAIPromptExecutionSettings executionSettings, - ChatHistory chatHistory, - Kernel? kernel, - string deploymentOrModelName) - { - if (executionSettings.ResultsPerPrompt is < 1 or > MaxResultsPerPrompt) - { - throw new ArgumentOutOfRangeException($"{nameof(executionSettings)}.{nameof(executionSettings.ResultsPerPrompt)}", executionSettings.ResultsPerPrompt, $"The value must be in range between 1 and {MaxResultsPerPrompt}, inclusive."); - } - - if (this.Logger.IsEnabled(LogLevel.Trace)) - { - this.Logger.LogTrace("ChatHistory: {ChatHistory}, Settings: {Settings}", - JsonSerializer.Serialize(chatHistory), - JsonSerializer.Serialize(executionSettings)); - } - - var options = new ChatCompletionsOptions - { - MaxTokens = executionSettings.MaxTokens, - Temperature = (float?)executionSettings.Temperature, - NucleusSamplingFactor = (float?)executionSettings.TopP, - FrequencyPenalty = (float?)executionSettings.FrequencyPenalty, - PresencePenalty = (float?)executionSettings.PresencePenalty, - ChoiceCount = executionSettings.ResultsPerPrompt, - DeploymentName = deploymentOrModelName, - Seed = executionSettings.Seed, - User = executionSettings.User, - LogProbabilitiesPerToken = executionSettings.TopLogprobs, - EnableLogProbabilities = executionSettings.Logprobs, - AzureExtensionsOptions = executionSettings.AzureChatExtensionsOptions - }; - - switch (executionSettings.ResponseFormat) - { - case ChatCompletionsResponseFormat formatObject: - // If the response format is an Azure SDK ChatCompletionsResponseFormat, just pass it along. - options.ResponseFormat = formatObject; - break; - - case string formatString: - // If the response format is a string, map the ones we know about, and ignore the rest. - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; - - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - break; - - case JsonElement formatElement: - // This is a workaround for a type mismatch when deserializing a JSON into an object? type property. - // Handling only string formatElement. - if (formatElement.ValueKind == JsonValueKind.String) - { - string formatString = formatElement.GetString() ?? ""; - switch (formatString) - { - case "json_object": - options.ResponseFormat = ChatCompletionsResponseFormat.JsonObject; - break; - - case "text": - options.ResponseFormat = ChatCompletionsResponseFormat.Text; - break; - } - } - break; - } - - executionSettings.ToolCallBehavior?.ConfigureOptions(kernel, options); - if (executionSettings.TokenSelectionBiases is not null) - { - foreach (var keyValue in executionSettings.TokenSelectionBiases) - { - options.TokenSelectionBiases.Add(keyValue.Key, keyValue.Value); - } - } - - if (executionSettings.StopSequences is { Count: > 0 }) - { - foreach (var s in executionSettings.StopSequences) - { - options.StopSequences.Add(s); - } - } - - if (!string.IsNullOrWhiteSpace(executionSettings.ChatSystemPrompt) && !chatHistory.Any(m => m.Role == AuthorRole.System)) - { - options.Messages.AddRange(GetRequestMessages(new ChatMessageContent(AuthorRole.System, executionSettings!.ChatSystemPrompt), executionSettings.ToolCallBehavior)); - } - - foreach (var message in chatHistory) - { - options.Messages.AddRange(GetRequestMessages(message, executionSettings.ToolCallBehavior)); - } - - return options; - } - - private static ChatRequestMessage GetRequestMessage(ChatRole chatRole, string contents, string? name, ChatCompletionsFunctionToolCall[]? tools) - { - if (chatRole == ChatRole.User) - { - return new ChatRequestUserMessage(contents) { Name = name }; - } - - if (chatRole == ChatRole.System) - { - return new ChatRequestSystemMessage(contents) { Name = name }; - } - - if (chatRole == ChatRole.Assistant) - { - var msg = new ChatRequestAssistantMessage(contents) { Name = name }; - if (tools is not null) - { - foreach (ChatCompletionsFunctionToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - return msg; - } - - throw new NotImplementedException($"Role {chatRole} is not implemented"); - } - - private static List GetRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) - { - if (message.Role == AuthorRole.System) - { - return [new ChatRequestSystemMessage(message.Content) { Name = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Tool) - { - // Handling function results represented by the TextContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, content, metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }) - if (message.Metadata?.TryGetValue(OpenAIChatMessageContent.ToolIdProperty, out object? toolId) is true && - toolId?.ToString() is string toolIdString) - { - return [new ChatRequestToolMessage(message.Content, toolIdString)]; - } - - // Handling function results represented by the FunctionResultContent type. - // Example: new ChatMessageContent(AuthorRole.Tool, items: new ChatMessageContentItemCollection { new FunctionResultContent(functionCall, result) }) - List? toolMessages = null; - foreach (var item in message.Items) - { - if (item is not FunctionResultContent resultContent) - { - continue; - } - - toolMessages ??= []; - - if (resultContent.Result is Exception ex) - { - toolMessages.Add(new ChatRequestToolMessage($"Error: Exception while invoking function. {ex.Message}", resultContent.CallId)); - continue; - } - - var stringResult = ProcessFunctionResult(resultContent.Result ?? string.Empty, toolCallBehavior); - - toolMessages.Add(new ChatRequestToolMessage(stringResult ?? string.Empty, resultContent.CallId)); - } - - if (toolMessages is not null) - { - return toolMessages; - } - - throw new NotSupportedException("No function result provided in the tool message."); - } - - if (message.Role == AuthorRole.User) - { - if (message.Items is { Count: 1 } && message.Items.FirstOrDefault() is TextContent textContent) - { - return [new ChatRequestUserMessage(textContent.Text) { Name = message.AuthorName }]; - } - - return [new ChatRequestUserMessage(message.Items.Select(static (KernelContent item) => (ChatMessageContentItem)(item switch - { - TextContent textContent => new ChatMessageTextContentItem(textContent.Text), - ImageContent imageContent => GetImageContentItem(imageContent), - _ => throw new NotSupportedException($"Unsupported chat message content type '{item.GetType()}'.") - }))) - { Name = message.AuthorName }]; - } - - if (message.Role == AuthorRole.Assistant) - { - var asstMessage = new ChatRequestAssistantMessage(message.Content) { Name = message.AuthorName }; - - // Handling function calls supplied via either: - // ChatCompletionsToolCall.ToolCalls collection items or - // ChatMessageContent.Metadata collection item with 'ChatResponseMessage.FunctionToolCalls' key. - IEnumerable? tools = (message as OpenAIChatMessageContent)?.ToolCalls; - if (tools is null && message.Metadata?.TryGetValue(OpenAIChatMessageContent.FunctionToolCallsProperty, out object? toolCallsObject) is true) - { - tools = toolCallsObject as IEnumerable; - if (tools is null && toolCallsObject is JsonElement { ValueKind: JsonValueKind.Array } array) - { - int length = array.GetArrayLength(); - var ftcs = new List(length); - for (int i = 0; i < length; i++) - { - JsonElement e = array[i]; - if (e.TryGetProperty("Id", out JsonElement id) && - e.TryGetProperty("Name", out JsonElement name) && - e.TryGetProperty("Arguments", out JsonElement arguments) && - id.ValueKind == JsonValueKind.String && - name.ValueKind == JsonValueKind.String && - arguments.ValueKind == JsonValueKind.String) - { - ftcs.Add(new ChatCompletionsFunctionToolCall(id.GetString()!, name.GetString()!, arguments.GetString()!)); - } - } - tools = ftcs; - } - } - - if (tools is not null) - { - asstMessage.ToolCalls.AddRange(tools); - } - - // Handling function calls supplied via ChatMessageContent.Items collection elements of the FunctionCallContent type. - HashSet? functionCallIds = null; - foreach (var item in message.Items) - { - if (item is not FunctionCallContent callRequest) - { - continue; - } - - functionCallIds ??= new HashSet(asstMessage.ToolCalls.Select(t => t.Id)); - - if (callRequest.Id is null || functionCallIds.Contains(callRequest.Id)) - { - continue; - } - - var argument = JsonSerializer.Serialize(callRequest.Arguments); - - asstMessage.ToolCalls.Add(new ChatCompletionsFunctionToolCall(callRequest.Id, FunctionName.ToFullyQualifiedName(callRequest.FunctionName, callRequest.PluginName, OpenAIFunction.NameSeparator), argument ?? string.Empty)); - } - - return [asstMessage]; - } - - throw new NotSupportedException($"Role {message.Role} is not supported."); - } - - private static ChatMessageImageContentItem GetImageContentItem(ImageContent imageContent) - { - if (imageContent.Data is { IsEmpty: false } data) - { - return new ChatMessageImageContentItem(BinaryData.FromBytes(data), imageContent.MimeType); - } - - if (imageContent.Uri is not null) - { - return new ChatMessageImageContentItem(imageContent.Uri); - } - - throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); - } - - private static ChatRequestMessage GetRequestMessage(ChatResponseMessage message) - { - if (message.Role == ChatRole.System) - { - return new ChatRequestSystemMessage(message.Content); - } - - if (message.Role == ChatRole.Assistant) - { - var msg = new ChatRequestAssistantMessage(message.Content); - if (message.ToolCalls is { Count: > 0 } tools) - { - foreach (ChatCompletionsToolCall tool in tools) - { - msg.ToolCalls.Add(tool); - } - } - - return msg; - } - - if (message.Role == ChatRole.User) - { - return new ChatRequestUserMessage(message.Content); - } - - throw new NotSupportedException($"Role {message.Role} is not supported."); - } - - private OpenAIChatMessageContent GetChatMessage(ChatChoice chatChoice, ChatCompletions responseData) - { - var message = new OpenAIChatMessageContent(chatChoice.Message, this.DeploymentOrModelName, GetChatChoiceMetadata(responseData, chatChoice)); - - message.Items.AddRange(this.GetFunctionCallContents(chatChoice.Message.ToolCalls)); - - return message; - } - - private OpenAIChatMessageContent GetChatMessage(ChatRole chatRole, string content, ChatCompletionsFunctionToolCall[] toolCalls, FunctionCallContent[]? functionCalls, IReadOnlyDictionary? metadata, string? authorName) - { - var message = new OpenAIChatMessageContent(chatRole, content, this.DeploymentOrModelName, toolCalls, metadata) - { - AuthorName = authorName, - }; - - if (functionCalls is not null) - { - message.Items.AddRange(functionCalls); - } - - return message; - } - - private IEnumerable GetFunctionCallContents(IEnumerable toolCalls) - { - List? result = null; - - foreach (var toolCall in toolCalls) - { - // Adding items of 'FunctionCallContent' type to the 'Items' collection even though the function calls are available via the 'ToolCalls' property. - // This allows consumers to work with functions in an LLM-agnostic way. - if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) - { - Exception? exception = null; - KernelArguments? arguments = null; - try - { - arguments = JsonSerializer.Deserialize(functionToolCall.Arguments); - if (arguments is not null) - { - // Iterate over copy of the names to avoid mutating the dictionary while enumerating it - var names = arguments.Names.ToArray(); - foreach (var name in names) - { - arguments[name] = arguments[name]?.ToString(); - } - } - } - catch (JsonException ex) - { - exception = new KernelException("Error: Function call arguments were invalid JSON.", ex); - - if (this.Logger.IsEnabled(LogLevel.Debug)) - { - this.Logger.LogDebug(ex, "Failed to deserialize function arguments ({FunctionName}/{FunctionId}).", functionToolCall.Name, functionToolCall.Id); - } - } - - var functionName = FunctionName.Parse(functionToolCall.Name, OpenAIFunction.NameSeparator); - - var functionCallContent = new FunctionCallContent( - functionName: functionName.Name, - pluginName: functionName.PluginName, - id: functionToolCall.Id, - arguments: arguments) - { - InnerContent = functionToolCall, - Exception = exception - }; - - result ??= []; - result.Add(functionCallContent); - } - } - - return result ?? Enumerable.Empty(); - } - - private static void AddResponseMessage(ChatCompletionsOptions chatOptions, ChatHistory chat, string? result, string? errorMessage, ChatCompletionsToolCall toolCall, ILogger logger) - { - // Log any error - if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) - { - Debug.Assert(result is null); - logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); - } - - // Add the tool response message to the chat options - result ??= errorMessage ?? string.Empty; - chatOptions.Messages.Add(new ChatRequestToolMessage(result, toolCall.Id)); - - // Add the tool response message to the chat history. - var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); - - if (toolCall is ChatCompletionsFunctionToolCall functionCall) - { - // Add an item of type FunctionResultContent to the ChatMessageContent.Items collection in addition to the function result stored as a string in the ChatMessageContent.Content property. - // This will enable migration to the new function calling model and facilitate the deprecation of the current one in the future. - var functionName = FunctionName.Parse(functionCall.Name, OpenAIFunction.NameSeparator); - message.Items.Add(new FunctionResultContent(functionName.Name, functionName.PluginName, functionCall.Id, result)); - } - - chat.Add(message); - } - - private static void ValidateMaxTokens(int? maxTokens) - { - if (maxTokens.HasValue && maxTokens < 1) - { - throw new ArgumentException($"MaxTokens {maxTokens} is not valid, the value must be greater than zero"); - } - } - - private static void ValidateAutoInvoke(bool autoInvoke, int resultsPerPrompt) - { - if (autoInvoke && resultsPerPrompt != 1) - { - // We can remove this restriction in the future if valuable. However, multiple results per prompt is rare, - // and limiting this significantly curtails the complexity of the implementation. - throw new ArgumentException($"Auto-invocation of tool calls may only be used with a {nameof(OpenAIPromptExecutionSettings.ResultsPerPrompt)} of 1."); - } - } - - private static async Task RunRequestAsync(Func> request) - { - try - { - return await request.Invoke().ConfigureAwait(false); - } - catch (RequestFailedException e) - { - throw e.ToHttpOperationException(); - } - } - - /// - /// Captures usage details, including token information. - /// - /// Instance of with usage details. - private void LogUsage(CompletionsUsage usage) - { - if (usage is null) - { - this.Logger.LogDebug("Token usage information unavailable."); - return; - } - - if (this.Logger.IsEnabled(LogLevel.Information)) - { - this.Logger.LogInformation( - "Prompt tokens: {PromptTokens}. Completion tokens: {CompletionTokens}. Total tokens: {TotalTokens}.", - usage.PromptTokens, usage.CompletionTokens, usage.TotalTokens); - } - - s_promptTokensCounter.Add(usage.PromptTokens); - s_completionTokensCounter.Add(usage.CompletionTokens); - s_totalTokensCounter.Add(usage.TotalTokens); - } - - /// - /// Processes the function result. - /// - /// The result of the function call. - /// The ToolCallBehavior object containing optional settings like JsonSerializerOptions.TypeInfoResolver. - /// A string representation of the function result. - private static string? ProcessFunctionResult(object functionResult, ToolCallBehavior? toolCallBehavior) - { - if (functionResult is string stringResult) - { - return stringResult; - } - - // This is an optimization to use ChatMessageContent content directly - // without unnecessary serialization of the whole message content class. - if (functionResult is ChatMessageContent chatMessageContent) - { - return chatMessageContent.ToString(); - } - - // For polymorphic serialization of unknown in advance child classes of the KernelContent class, - // a corresponding JsonTypeInfoResolver should be provided via the JsonSerializerOptions.TypeInfoResolver property. - // For more details about the polymorphic serialization, see the article at: - // https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-8-0 -#pragma warning disable CS0618 // Type or member is obsolete - return JsonSerializer.Serialize(functionResult, toolCallBehavior?.ToolCallResultSerializerOptions); -#pragma warning restore CS0618 // Type or member is obsolete - } - - /// - /// Executes auto function invocation filters and/or function itself. - /// This method can be moved to when auto function invocation logic will be extracted to common place. - /// - private static async Task OnAutoFunctionInvocationAsync( - Kernel kernel, - AutoFunctionInvocationContext context, - Func functionCallCallback) - { - await InvokeFilterOrFunctionAsync(kernel.AutoFunctionInvocationFilters, functionCallCallback, context).ConfigureAwait(false); - - return context; - } - - /// - /// This method will execute auto function invocation filters and function recursively. - /// If there are no registered filters, just function will be executed. - /// If there are registered filters, filter on position will be executed. - /// Second parameter of filter is callback. It can be either filter on + 1 position or function if there are no remaining filters to execute. - /// Function will be always executed as last step after all filters. - /// - private static async Task InvokeFilterOrFunctionAsync( - IList? autoFunctionInvocationFilters, - Func functionCallCallback, - AutoFunctionInvocationContext context, - int index = 0) - { - if (autoFunctionInvocationFilters is { Count: > 0 } && index < autoFunctionInvocationFilters.Count) - { - await autoFunctionInvocationFilters[index].OnAutoFunctionInvocationAsync(context, - (context) => InvokeFilterOrFunctionAsync(autoFunctionInvocationFilters, functionCallCallback, context, index + 1)).ConfigureAwait(false); - } - else - { - await functionCallCallback(context).ConfigureAwait(false); - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/CustomHostPipelinePolicy.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/CustomHostPipelinePolicy.cs deleted file mode 100644 index e0f5733dd5c0..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/CustomHostPipelinePolicy.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using Azure.Core; -using Azure.Core.Pipeline; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI.Core.AzureSdk; - -internal sealed class CustomHostPipelinePolicy : HttpPipelineSynchronousPolicy -{ - private readonly Uri _endpoint; - - internal CustomHostPipelinePolicy(Uri endpoint) - { - this._endpoint = endpoint; - } - - public override void OnSendingRequest(HttpMessage message) - { - // Update current host to provided endpoint - message.Request?.Uri.Reset(this._endpoint); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs deleted file mode 100644 index d91f8e45fc40..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIChatMessageContent.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI specialized chat message content -/// -public sealed class OpenAIChatMessageContent : ChatMessageContent -{ - /// - /// Gets the metadata key for the name property. - /// - public static string ToolIdProperty => $"{nameof(ChatCompletionsToolCall)}.{nameof(ChatCompletionsToolCall.Id)}"; - - /// - /// Gets the metadata key for the list of . - /// - internal static string FunctionToolCallsProperty => $"{nameof(ChatResponseMessage)}.FunctionToolCalls"; - - /// - /// Initializes a new instance of the class. - /// - internal OpenAIChatMessageContent(ChatResponseMessage chatMessage, string modelId, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(chatMessage.Role.ToString()), chatMessage.Content, modelId, chatMessage, System.Text.Encoding.UTF8, CreateMetadataDictionary(chatMessage.ToolCalls, metadata)) - { - this.ToolCalls = chatMessage.ToolCalls; - } - - /// - /// Initializes a new instance of the class. - /// - internal OpenAIChatMessageContent(ChatRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(new AuthorRole(role.ToString()), content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) - { - this.ToolCalls = toolCalls; - } - - /// - /// Initializes a new instance of the class. - /// - internal OpenAIChatMessageContent(AuthorRole role, string? content, string modelId, IReadOnlyList toolCalls, IReadOnlyDictionary? metadata = null) - : base(role, content, modelId, content, System.Text.Encoding.UTF8, CreateMetadataDictionary(toolCalls, metadata)) - { - this.ToolCalls = toolCalls; - } - - /// - /// A list of the tools called by the model. - /// - public IReadOnlyList ToolCalls { get; } - - /// - /// Retrieve the resulting function from the chat result. - /// - /// The , or null if no function was returned by the model. - public IReadOnlyList GetOpenAIFunctionToolCalls() - { - List? functionToolCallList = null; - - foreach (var toolCall in this.ToolCalls) - { - if (toolCall is ChatCompletionsFunctionToolCall functionToolCall) - { - (functionToolCallList ??= []).Add(new OpenAIFunctionToolCall(functionToolCall)); - } - } - - if (functionToolCallList is not null) - { - return functionToolCallList; - } - - return []; - } - - private static IReadOnlyDictionary? CreateMetadataDictionary( - IReadOnlyList toolCalls, - IReadOnlyDictionary? original) - { - // We only need to augment the metadata if there are any tool calls. - if (toolCalls.Count > 0) - { - Dictionary newDictionary; - if (original is null) - { - // There's no existing metadata to clone; just allocate a new dictionary. - newDictionary = new Dictionary(1); - } - else if (original is IDictionary origIDictionary) - { - // Efficiently clone the old dictionary to a new one. - newDictionary = new Dictionary(origIDictionary); - } - else - { - // There's metadata to clone but we have to do so one item at a time. - newDictionary = new Dictionary(original.Count + 1); - foreach (var kvp in original) - { - newDictionary[kvp.Key] = kvp.Value; - } - } - - // Add the additional entry. - newDictionary.Add(FunctionToolCallsProperty, toolCalls.OfType().ToList()); - - return newDictionary; - } - - return original; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIClientCore.cs deleted file mode 100644 index 32cc0ab22f19..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIClientCore.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Runtime.CompilerServices; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI.Core.AzureSdk; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Core implementation for OpenAI clients, providing common functionality and properties. -/// -internal sealed class OpenAIClientCore : ClientCore -{ - private const string DefaultPublicEndpoint = "https://api.openai.com/v1"; - - /// - /// Gets the attribute name used to store the organization in the dictionary. - /// - public static string OrganizationKey => "Organization"; - - /// - /// OpenAI / Azure OpenAI Client - /// - internal override OpenAIClient Client { get; } - - /// - /// Initializes a new instance of the class. - /// - /// Model name. - /// OpenAI API Key. - /// OpenAI compatible API endpoint. - /// OpenAI Organization Id (usually optional). - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal OpenAIClientCore( - string modelId, - string? apiKey = null, - Uri? endpoint = null, - string? organization = null, - HttpClient? httpClient = null, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(modelId); - - this.DeploymentOrModelName = modelId; - - var options = GetOpenAIClientOptions(httpClient); - - if (!string.IsNullOrWhiteSpace(organization)) - { - options.AddPolicy(new AddHeaderRequestPolicy("OpenAI-Organization", organization!), HttpPipelinePosition.PerCall); - } - - // Accepts the endpoint if provided, otherwise uses the default OpenAI endpoint. - var providedEndpoint = endpoint ?? httpClient?.BaseAddress; - if (providedEndpoint is null) - { - Verify.NotNullOrWhiteSpace(apiKey); // For Public OpenAI Endpoint a key must be provided. - this.Endpoint = new Uri(DefaultPublicEndpoint); - } - else - { - options.AddPolicy(new CustomHostPipelinePolicy(providedEndpoint), Azure.Core.HttpPipelinePosition.PerRetry); - this.Endpoint = providedEndpoint; - } - - this.Client = new OpenAIClient(apiKey ?? string.Empty, options); - } - - /// - /// Initializes a new instance of the class using the specified OpenAIClient. - /// Note: instances created this way might not have the default diagnostics settings, - /// it's up to the caller to configure the client. - /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// The to use for logging. If null, no logging will be performed. - internal OpenAIClientCore( - string modelId, - OpenAIClient openAIClient, - ILogger? logger = null) : base(logger) - { - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNull(openAIClient); - - this.DeploymentOrModelName = modelId; - this.Client = openAIClient; - } - - /// - /// Logs OpenAI action details. - /// - /// Caller member name. Populated automatically by runtime. - internal void LogActionDetails([CallerMemberName] string? callerMemberName = default) - { - if (this.Logger.IsEnabled(LogLevel.Information)) - { - this.Logger.LogInformation("Action: {Action}. OpenAI Model ID: {ModelId}.", callerMemberName, this.DeploymentOrModelName); - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs deleted file mode 100644 index b51faa59c359..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunction.cs +++ /dev/null @@ -1,182 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using Azure.AI.OpenAI; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -// NOTE: Since this space is evolving rapidly, in order to reduce the risk of needing to take breaking -// changes as OpenAI's APIs evolve, these types are not externally constructible. In the future, once -// things stabilize, and if need demonstrates, we could choose to expose those constructors. - -/// -/// Represents a function parameter that can be passed to an OpenAI function tool call. -/// -public sealed class OpenAIFunctionParameter -{ - internal OpenAIFunctionParameter(string? name, string? description, bool isRequired, Type? parameterType, KernelJsonSchema? schema) - { - this.Name = name ?? string.Empty; - this.Description = description ?? string.Empty; - this.IsRequired = isRequired; - this.ParameterType = parameterType; - this.Schema = schema; - } - - /// Gets the name of the parameter. - public string Name { get; } - - /// Gets a description of the parameter. - public string Description { get; } - - /// Gets whether the parameter is required vs optional. - public bool IsRequired { get; } - - /// Gets the of the parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function return parameter that can be returned by a tool call to OpenAI. -/// -public sealed class OpenAIFunctionReturnParameter -{ - internal OpenAIFunctionReturnParameter(string? description, Type? parameterType, KernelJsonSchema? schema) - { - this.Description = description ?? string.Empty; - this.Schema = schema; - this.ParameterType = parameterType; - } - - /// Gets a description of the return parameter. - public string Description { get; } - - /// Gets the of the return parameter, if known. - public Type? ParameterType { get; } - - /// Gets a JSON schema for the return parameter, if known. - public KernelJsonSchema? Schema { get; } -} - -/// -/// Represents a function that can be passed to the OpenAI API -/// -public sealed class OpenAIFunction -{ - /// - /// Cached storing the JSON for a function with no parameters. - /// - /// - /// This is an optimization to avoid serializing the same JSON Schema over and over again - /// for this relatively common case. - /// - private static readonly BinaryData s_zeroFunctionParametersSchema = new("""{"type":"object","required":[],"properties":{}}"""); - /// - /// Cached schema for a descriptionless string. - /// - private static readonly KernelJsonSchema s_stringNoDescriptionSchema = KernelJsonSchema.Parse("""{"type":"string"}"""); - - /// Initializes the OpenAIFunction. - internal OpenAIFunction( - string? pluginName, - string functionName, - string? description, - IReadOnlyList? parameters, - OpenAIFunctionReturnParameter? returnParameter) - { - Verify.NotNullOrWhiteSpace(functionName); - - this.PluginName = pluginName; - this.FunctionName = functionName; - this.Description = description; - this.Parameters = parameters; - this.ReturnParameter = returnParameter; - } - - /// Gets the separator used between the plugin name and the function name, if a plugin name is present. - /// This separator was previously _, but has been changed to - to better align to the behavior elsewhere in SK and in response - /// to developers who want to use underscores in their function or plugin names. We plan to make this setting configurable in the future. - public static string NameSeparator { get; set; } = "-"; - - /// Gets the name of the plugin with which the function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , this is - /// the same as . - /// - public string FullyQualifiedName => - string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{NameSeparator}{this.FunctionName}"; - - /// Gets a description of the function. - public string? Description { get; } - - /// Gets a list of parameters to the function, if any. - public IReadOnlyList? Parameters { get; } - - /// Gets the return parameter of the function, if any. - public OpenAIFunctionReturnParameter? ReturnParameter { get; } - - /// - /// Converts the representation to the Azure SDK's - /// representation. - /// - /// A containing all the function information. - public FunctionDefinition ToFunctionDefinition() - { - BinaryData resultParameters = s_zeroFunctionParametersSchema; - - IReadOnlyList? parameters = this.Parameters; - if (parameters is { Count: > 0 }) - { - var properties = new Dictionary(); - var required = new List(); - - for (int i = 0; i < parameters.Count; i++) - { - var parameter = parameters[i]; - properties.Add(parameter.Name, parameter.Schema ?? GetDefaultSchemaForTypelessParameter(parameter.Description)); - if (parameter.IsRequired) - { - required.Add(parameter.Name); - } - } - - resultParameters = BinaryData.FromObjectAsJson(new - { - type = "object", - required, - properties, - }); - } - - return new FunctionDefinition - { - Name = this.FullyQualifiedName, - Description = this.Description, - Parameters = resultParameters, - }; - } - - /// Gets a for a typeless parameter with the specified description, defaulting to typeof(string) - private static KernelJsonSchema GetDefaultSchemaForTypelessParameter(string? description) - { - // If there's a description, incorporate it. - if (!string.IsNullOrWhiteSpace(description)) - { - return KernelJsonSchemaBuilder.Build(null, typeof(string), description); - } - - // Otherwise, we can use a cached schema for a string with no description. - return s_stringNoDescriptionSchema; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunctionToolCall.cs deleted file mode 100644 index af4688e06df1..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIFunctionToolCall.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using System.Text.Json; -using Azure.AI.OpenAI; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Represents an OpenAI function tool call with deserialized function name and arguments. -/// -public sealed class OpenAIFunctionToolCall -{ - private string? _fullyQualifiedFunctionName; - - /// Initialize the from a . - internal OpenAIFunctionToolCall(ChatCompletionsFunctionToolCall functionToolCall) - { - Verify.NotNull(functionToolCall); - Verify.NotNull(functionToolCall.Name); - - string fullyQualifiedFunctionName = functionToolCall.Name; - string functionName = fullyQualifiedFunctionName; - string? arguments = functionToolCall.Arguments; - string? pluginName = null; - - int separatorPos = fullyQualifiedFunctionName.IndexOf(OpenAIFunction.NameSeparator, StringComparison.Ordinal); - if (separatorPos >= 0) - { - pluginName = fullyQualifiedFunctionName.AsSpan(0, separatorPos).Trim().ToString(); - functionName = fullyQualifiedFunctionName.AsSpan(separatorPos + OpenAIFunction.NameSeparator.Length).Trim().ToString(); - } - - this.Id = functionToolCall.Id; - this._fullyQualifiedFunctionName = fullyQualifiedFunctionName; - this.PluginName = pluginName; - this.FunctionName = functionName; - if (!string.IsNullOrWhiteSpace(arguments)) - { - this.Arguments = JsonSerializer.Deserialize>(arguments!); - } - } - - /// Gets the ID of the tool call. - public string? Id { get; } - - /// Gets the name of the plugin with which this function is associated, if any. - public string? PluginName { get; } - - /// Gets the name of the function. - public string FunctionName { get; } - - /// Gets a name/value collection of the arguments to the function, if any. - public Dictionary? Arguments { get; } - - /// Gets the fully-qualified name of the function. - /// - /// This is the concatenation of the and the , - /// separated by . If there is no , - /// this is the same as . - /// - public string FullyQualifiedName => - this._fullyQualifiedFunctionName ??= - string.IsNullOrEmpty(this.PluginName) ? this.FunctionName : $"{this.PluginName}{OpenAIFunction.NameSeparator}{this.FunctionName}"; - - /// - public override string ToString() - { - var sb = new StringBuilder(this.FullyQualifiedName); - - sb.Append('('); - if (this.Arguments is not null) - { - string separator = ""; - foreach (var arg in this.Arguments) - { - sb.Append(separator).Append(arg.Key).Append(':').Append(arg.Value); - separator = ", "; - } - } - sb.Append(')'); - - return sb.ToString(); - } - - /// - /// Tracks tooling updates from streaming responses. - /// - /// The tool call update to incorporate. - /// Lazily-initialized dictionary mapping indices to IDs. - /// Lazily-initialized dictionary mapping indices to names. - /// Lazily-initialized dictionary mapping indices to arguments. - internal static void TrackStreamingToolingUpdate( - StreamingToolCallUpdate? update, - ref Dictionary? toolCallIdsByIndex, - ref Dictionary? functionNamesByIndex, - ref Dictionary? functionArgumentBuildersByIndex) - { - if (update is null) - { - // Nothing to track. - return; - } - - // If we have an ID, ensure the index is being tracked. Even if it's not a function update, - // we want to keep track of it so we can send back an error. - if (update.Id is string id) - { - (toolCallIdsByIndex ??= [])[update.ToolCallIndex] = id; - } - - if (update is StreamingFunctionToolCallUpdate ftc) - { - // Ensure we're tracking the function's name. - if (ftc.Name is string name) - { - (functionNamesByIndex ??= [])[ftc.ToolCallIndex] = name; - } - - // Ensure we're tracking the function's arguments. - if (ftc.ArgumentsUpdate is string argumentsUpdate) - { - if (!(functionArgumentBuildersByIndex ??= []).TryGetValue(ftc.ToolCallIndex, out StringBuilder? arguments)) - { - functionArgumentBuildersByIndex[ftc.ToolCallIndex] = arguments = new(); - } - - arguments.Append(argumentsUpdate); - } - } - } - - /// - /// Converts the data built up by into an array of s. - /// - /// Dictionary mapping indices to IDs. - /// Dictionary mapping indices to names. - /// Dictionary mapping indices to arguments. - internal static ChatCompletionsFunctionToolCall[] ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( - ref Dictionary? toolCallIdsByIndex, - ref Dictionary? functionNamesByIndex, - ref Dictionary? functionArgumentBuildersByIndex) - { - ChatCompletionsFunctionToolCall[] toolCalls = []; - if (toolCallIdsByIndex is { Count: > 0 }) - { - toolCalls = new ChatCompletionsFunctionToolCall[toolCallIdsByIndex.Count]; - - int i = 0; - foreach (KeyValuePair toolCallIndexAndId in toolCallIdsByIndex) - { - string? functionName = null; - StringBuilder? functionArguments = null; - - functionNamesByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionName); - functionArgumentBuildersByIndex?.TryGetValue(toolCallIndexAndId.Key, out functionArguments); - - toolCalls[i] = new ChatCompletionsFunctionToolCall(toolCallIndexAndId.Value, functionName ?? string.Empty, functionArguments?.ToString() ?? string.Empty); - i++; - } - - Debug.Assert(i == toolCalls.Length); - } - - return toolCalls; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs deleted file mode 100644 index 6859e1225dd6..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIKernelFunctionMetadataExtensions.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Extensions for specific to the OpenAI connector. -/// -public static class OpenAIKernelFunctionMetadataExtensions -{ - /// - /// Convert a to an . - /// - /// The object to convert. - /// An object. - public static OpenAIFunction ToOpenAIFunction(this KernelFunctionMetadata metadata) - { - IReadOnlyList metadataParams = metadata.Parameters; - - var openAIParams = new OpenAIFunctionParameter[metadataParams.Count]; - for (int i = 0; i < openAIParams.Length; i++) - { - var param = metadataParams[i]; - - openAIParams[i] = new OpenAIFunctionParameter( - param.Name, - GetDescription(param), - param.IsRequired, - param.ParameterType, - param.Schema); - } - - return new OpenAIFunction( - metadata.PluginName, - metadata.Name, - metadata.Description, - openAIParams, - new OpenAIFunctionReturnParameter( - metadata.ReturnParameter.Description, - metadata.ReturnParameter.ParameterType, - metadata.ReturnParameter.Schema)); - - static string GetDescription(KernelParameterMetadata param) - { - if (InternalTypeConverter.ConvertToString(param.DefaultValue) is string stringValue && !string.IsNullOrEmpty(stringValue)) - { - return $"{param.Description} (default value: {stringValue})"; - } - - return param.Description; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIPluginCollectionExtensions.cs deleted file mode 100644 index 135b17b83df3..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIPluginCollectionExtensions.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Extension methods for . -/// -public static class OpenAIPluginCollectionExtensions -{ - /// - /// Given an object, tries to retrieve the corresponding and populate with its parameters. - /// - /// The plugins. - /// The object. - /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, - /// When this method returns, the arguments for the function; otherwise, - /// if the function was found; otherwise, . - public static bool TryGetFunctionAndArguments( - this IReadOnlyKernelPluginCollection plugins, - ChatCompletionsFunctionToolCall functionToolCall, - [NotNullWhen(true)] out KernelFunction? function, - out KernelArguments? arguments) => - plugins.TryGetFunctionAndArguments(new OpenAIFunctionToolCall(functionToolCall), out function, out arguments); - - /// - /// Given an object, tries to retrieve the corresponding and populate with its parameters. - /// - /// The plugins. - /// The object. - /// When this method returns, the function that was retrieved if one with the specified name was found; otherwise, - /// When this method returns, the arguments for the function; otherwise, - /// if the function was found; otherwise, . - public static bool TryGetFunctionAndArguments( - this IReadOnlyKernelPluginCollection plugins, - OpenAIFunctionToolCall functionToolCall, - [NotNullWhen(true)] out KernelFunction? function, - out KernelArguments? arguments) - { - if (plugins.TryGetFunction(functionToolCall.PluginName, functionToolCall.FunctionName, out function)) - { - // Add parameters to arguments - arguments = null; - if (functionToolCall.Arguments is not null) - { - arguments = []; - foreach (var parameter in functionToolCall.Arguments) - { - arguments[parameter.Key] = parameter.Value?.ToString(); - } - } - - return true; - } - - // Function not found in collection - arguments = null; - return false; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingChatMessageContent.cs deleted file mode 100644 index fa3845782d0a..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingChatMessageContent.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI and OpenAI Specialized streaming chat message content. -/// -/// -/// Represents a chat message content chunk that was streamed from the remote model. -/// -public sealed class OpenAIStreamingChatMessageContent : StreamingChatMessageContent -{ - /// - /// The reason why the completion finished. - /// - public CompletionsFinishReason? FinishReason { get; set; } - - /// - /// Create a new instance of the class. - /// - /// Internal Azure SDK Message update representation - /// Index of the choice - /// The model ID used to generate the content - /// Additional metadata - internal OpenAIStreamingChatMessageContent( - StreamingChatCompletionsUpdate chatUpdate, - int choiceIndex, - string modelId, - IReadOnlyDictionary? metadata = null) - : base( - chatUpdate.Role.HasValue ? new AuthorRole(chatUpdate.Role.Value.ToString()) : null, - chatUpdate.ContentUpdate, - chatUpdate, - choiceIndex, - modelId, - Encoding.UTF8, - metadata) - { - this.ToolCallUpdate = chatUpdate.ToolCallUpdate; - this.FinishReason = chatUpdate?.FinishReason; - } - - /// - /// Create a new instance of the class. - /// - /// Author role of the message - /// Content of the message - /// Tool call update - /// Completion finish reason - /// Index of the choice - /// The model ID used to generate the content - /// Additional metadata - internal OpenAIStreamingChatMessageContent( - AuthorRole? authorRole, - string? content, - StreamingToolCallUpdate? tootToolCallUpdate = null, - CompletionsFinishReason? completionsFinishReason = null, - int choiceIndex = 0, - string? modelId = null, - IReadOnlyDictionary? metadata = null) - : base( - authorRole, - content, - null, - choiceIndex, - modelId, - Encoding.UTF8, - metadata) - { - this.ToolCallUpdate = tootToolCallUpdate; - this.FinishReason = completionsFinishReason; - } - - /// Gets any update information in the message about a tool call. - public StreamingToolCallUpdate? ToolCallUpdate { get; } - - /// - public override byte[] ToByteArray() => this.Encoding.GetBytes(this.ToString()); - - /// - public override string ToString() => this.Content ?? string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingTextContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingTextContent.cs deleted file mode 100644 index 126e1615a747..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAIStreamingTextContent.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI and OpenAI Specialized streaming text content. -/// -/// -/// Represents a text content chunk that was streamed from the remote model. -/// -public sealed class OpenAIStreamingTextContent : StreamingTextContent -{ - /// - /// Create a new instance of the class. - /// - /// Text update - /// Index of the choice - /// The model ID used to generate the content - /// Inner chunk object - /// Metadata information - internal OpenAIStreamingTextContent( - string text, - int choiceIndex, - string modelId, - object? innerContentObject = null, - IReadOnlyDictionary? metadata = null) - : base( - text, - choiceIndex, - modelId, - innerContentObject, - Encoding.UTF8, - metadata) - { - } - - /// - public override byte[] ToByteArray() - { - return this.Encoding.GetBytes(this.ToString()); - } - - /// - public override string ToString() - { - return this.Text ?? string.Empty; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAITextToAudioClient.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAITextToAudioClient.cs deleted file mode 100644 index 7f3daaa2d941..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/OpenAITextToAudioClient.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Http; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text-to-audio client for HTTP operations. -/// -[Experimental("SKEXP0001")] -internal sealed class OpenAITextToAudioClient -{ - private readonly ILogger _logger; - private readonly HttpClient _httpClient; - - private readonly string _modelId; - private readonly string _apiKey; - private readonly string? _organization; - - /// - /// Storage for AI service attributes. - /// - internal Dictionary Attributes { get; } = []; - - /// - /// Creates an instance of the with API key auth. - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal OpenAITextToAudioClient( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILogger? logger = null) - { - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - this._modelId = modelId; - this._apiKey = apiKey; - this._organization = organization; - - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._logger = logger ?? NullLogger.Instance; - } - - internal async Task> GetAudioContentsAsync( - string text, - PromptExecutionSettings? executionSettings, - CancellationToken cancellationToken) - { - OpenAITextToAudioExecutionSettings? audioExecutionSettings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - - Verify.NotNullOrWhiteSpace(audioExecutionSettings?.Voice); - - using var request = this.GetRequest(text, audioExecutionSettings); - using var response = await this.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - var data = await response.Content.ReadAsByteArrayAndTranslateExceptionAsync().ConfigureAwait(false); - - return [new(data, this._modelId)]; - } - - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } - } - - #region private - - private async Task SendRequestAsync( - HttpRequestMessage request, - CancellationToken cancellationToken) - { - request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); - request.Headers.Add("Authorization", $"Bearer {this._apiKey}"); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAITextToAudioClient))); - - if (!string.IsNullOrWhiteSpace(this._organization)) - { - request.Headers.Add("OpenAI-Organization", this._organization); - } - - try - { - return await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - } - catch (HttpOperationException ex) - { - this._logger.LogError( - "Error occurred on text-to-audio request execution: {ExceptionMessage}", ex.Message); - - throw; - } - } - - private HttpRequestMessage GetRequest(string text, OpenAITextToAudioExecutionSettings executionSettings) - { - const string DefaultBaseUrl = "https://api.openai.com"; - - var baseUrl = !string.IsNullOrWhiteSpace(this._httpClient.BaseAddress?.AbsoluteUri) ? - this._httpClient.BaseAddress!.AbsoluteUri : - DefaultBaseUrl; - - var payload = new TextToAudioRequest(this._modelId, text, executionSettings.Voice) - { - ResponseFormat = executionSettings.ResponseFormat, - Speed = executionSettings.Speed - }; - - return HttpRequest.CreatePostRequest($"{baseUrl.TrimEnd('/')}/v1/audio/speech", payload); - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/RequestFailedExceptionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/RequestFailedExceptionExtensions.cs deleted file mode 100644 index 51f99aa1c0cb..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/AzureSdk/RequestFailedExceptionExtensions.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Net; -using Azure; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Provides extension methods for the class. -/// -internal static class RequestFailedExceptionExtensions -{ - /// - /// Converts a to an . - /// - /// The original . - /// An instance. - public static HttpOperationException ToHttpOperationException(this RequestFailedException exception) - { - const int NoResponseReceived = 0; - - string? responseContent = null; - - try - { - responseContent = exception.GetRawResponse()?.Content?.ToString(); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch { } // We want to suppress any exceptions that occur while reading the content, ensuring that an HttpOperationException is thrown instead. -#pragma warning restore CA1031 - - return new HttpOperationException( - exception.Status == NoResponseReceived ? null : (HttpStatusCode?)exception.Status, - responseContent, - exception.Message, - exception); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs deleted file mode 100644 index 04da5d2dc1e3..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/AzureOpenAIChatCompletionService.cs +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextGeneration; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI chat completion service. -/// -public sealed class AzureOpenAIChatCompletionService : IChatCompletionService, ITextGenerationService -{ - /// Core implementation shared by Azure OpenAI clients. - private readonly AzureOpenAIClientCore _core; - - /// - /// Create an instance of the connector with API key auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIChatCompletionService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Create an instance of the connector with AAD auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIChatCompletionService( - string deploymentName, - string endpoint, - TokenCredential credentials, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, credentials, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Creates a new client instance using the specified . - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAIChatCompletionService( - string deploymentName, - OpenAIClient openAIClient, - string? modelId = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAIChatCompletionService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); - - /// - public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/OpenAIChatCompletionService.cs deleted file mode 100644 index a9f617efed73..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletion/OpenAIChatCompletionService.cs +++ /dev/null @@ -1,133 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextGeneration; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI chat completion service. -/// -public sealed class OpenAIChatCompletionService : IChatCompletionService, ITextGenerationService -{ - private readonly OpenAIClientCore _core; - - /// - /// Create an instance of the OpenAI chat completion connector - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIChatCompletionService( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null -) - { - this._core = new( - modelId, - apiKey, - endpoint: null, - organization, - httpClient, - loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization); - } - - /// - /// Create an instance of the Custom Message API OpenAI chat completion connector - /// - /// Model name - /// Custom Message API compatible endpoint - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - [Experimental("SKEXP0010")] - public OpenAIChatCompletionService( - string modelId, - Uri endpoint, - string? apiKey = null, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Uri? internalClientEndpoint = null; - var providedEndpoint = endpoint ?? httpClient?.BaseAddress; - if (providedEndpoint is not null) - { - // If the provided endpoint does not have a path specified, updates it to the default Message API Chat Completions endpoint - internalClientEndpoint = providedEndpoint.PathAndQuery == "/" ? - new Uri(providedEndpoint, "v1/chat/completions") - : providedEndpoint; - } - - this._core = new( - modelId, - apiKey, - internalClientEndpoint, - organization, - httpClient, - loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); - - if (providedEndpoint is not null) - { - this._core.AddAttribute(AIServiceExtensions.EndpointKey, providedEndpoint.ToString()); - } - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization); - } - - /// - /// Create an instance of the OpenAI chat completion connector - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIChatCompletionService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) - { - this._core = new( - modelId, - openAIClient, - loggerFactory?.CreateLogger(typeof(OpenAIChatCompletionService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); - - /// - public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this._core.GetChatAsTextStreamingContentsAsync(prompt, executionSettings, kernel, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataConfig.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataConfig.cs deleted file mode 100644 index 7f49e74c5fa4..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataConfig.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Required configuration for Azure OpenAI chat completion with data. -/// More information: -/// -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -public class AzureOpenAIChatCompletionWithDataConfig -{ - /// - /// Azure OpenAI model ID or deployment name, see - /// - public string CompletionModelId { get; set; } = string.Empty; - - /// - /// Azure OpenAI deployment URL, see - /// - public string CompletionEndpoint { get; set; } = string.Empty; - - /// - /// Azure OpenAI API key, see - /// - public string CompletionApiKey { get; set; } = string.Empty; - - /// - /// Azure OpenAI Completion API version (e.g. 2024-02-01) - /// - public string CompletionApiVersion { get; set; } = string.Empty; - - /// - /// Data source endpoint URL. - /// For Azure AI Search, see - /// - public string DataSourceEndpoint { get; set; } = string.Empty; - - /// - /// Data source API key. - /// For Azure AI Search keys, see - /// - public string DataSourceApiKey { get; set; } = string.Empty; - - /// - /// Data source index name. - /// For Azure AI Search indexes, see - /// - public string DataSourceIndex { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataService.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataService.cs deleted file mode 100644 index 793209704bbf..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataService.cs +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Http; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.Text; -using Microsoft.SemanticKernel.TextGeneration; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI Chat Completion with data service. -/// More information: -/// -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -public sealed class AzureOpenAIChatCompletionWithDataService : IChatCompletionService, ITextGenerationService -{ - /// - /// Initializes a new instance of the class. - /// - /// Instance of class with completion configuration. - /// Custom for HTTP requests. - /// Instance of to use for logging. - public AzureOpenAIChatCompletionWithDataService( - AzureOpenAIChatCompletionWithDataConfig config, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this.ValidateConfig(config); - - this._config = config; - - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._logger = loggerFactory?.CreateLogger(this.GetType()) ?? NullLogger.Instance; - this._attributes.Add(AIServiceExtensions.ModelIdKey, config.CompletionModelId); - } - - /// - public IReadOnlyDictionary Attributes => this._attributes; - - /// - public Task> GetChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this.InternalGetChatMessageContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public IAsyncEnumerable GetStreamingChatMessageContentsAsync(ChatHistory chatHistory, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - => this.InternalGetChatStreamingContentsAsync(chatHistory, executionSettings, kernel, cancellationToken); - - /// - public async Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return (await this.GetChatMessageContentsAsync(prompt, executionSettings, kernel, cancellationToken).ConfigureAwait(false)) - .Select(chat => new TextContent(chat.Content, chat.ModelId, chat, Encoding.UTF8, chat.Metadata)) - .ToList(); - } - - /// - public async IAsyncEnumerable GetStreamingTextContentsAsync( - string prompt, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var streamingChatContent in this.InternalGetChatStreamingContentsAsync(new ChatHistory(prompt), executionSettings, kernel, cancellationToken).ConfigureAwait(false)) - { - yield return new StreamingTextContent(streamingChatContent.Content, streamingChatContent.ChoiceIndex, streamingChatContent.ModelId, streamingChatContent, Encoding.UTF8, streamingChatContent.Metadata); - } - } - - #region private ================================================================================ - - private const string DefaultApiVersion = "2024-02-01"; - - private readonly AzureOpenAIChatCompletionWithDataConfig _config; - - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly Dictionary _attributes = []; - private void ValidateConfig(AzureOpenAIChatCompletionWithDataConfig config) - { - Verify.NotNull(config); - - Verify.NotNullOrWhiteSpace(config.CompletionModelId); - Verify.NotNullOrWhiteSpace(config.CompletionEndpoint); - Verify.NotNullOrWhiteSpace(config.CompletionApiKey); - Verify.NotNullOrWhiteSpace(config.DataSourceEndpoint); - Verify.NotNullOrWhiteSpace(config.DataSourceApiKey); - Verify.NotNullOrWhiteSpace(config.DataSourceIndex); - } - - private async Task> InternalGetChatMessageContentsAsync( - ChatHistory chat, - PromptExecutionSettings? executionSettings, - Kernel? kernel, - CancellationToken cancellationToken = default) - { - var openAIExecutionSettings = OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings, OpenAIPromptExecutionSettings.DefaultTextMaxTokens); - - using var request = this.GetRequest(chat, openAIExecutionSettings, isStreamEnabled: false); - using var response = await this.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - - var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - - var chatWithDataResponse = this.DeserializeResponse(body); - IReadOnlyDictionary metadata = GetResponseMetadata(chatWithDataResponse); - - return chatWithDataResponse.Choices.Select(choice => new AzureOpenAIWithDataChatMessageContent(choice, this.GetModelId(), metadata)).ToList(); - } - - private static Dictionary GetResponseMetadata(ChatWithDataResponse chatResponse) - { - return new Dictionary(5) - { - { nameof(chatResponse.Id), chatResponse.Id }, - { nameof(chatResponse.Model), chatResponse.Model }, - { nameof(chatResponse.Created), chatResponse.Created }, - { nameof(chatResponse.Object), chatResponse.Object }, - { nameof(chatResponse.Usage), chatResponse.Usage }, - }; - } - - private static Dictionary GetResponseMetadata(ChatWithDataStreamingResponse chatResponse) - { - return new Dictionary(4) - { - { nameof(chatResponse.Id), chatResponse.Id }, - { nameof(chatResponse.Model), chatResponse.Model }, - { nameof(chatResponse.Created), chatResponse.Created }, - { nameof(chatResponse.Object), chatResponse.Object }, - }; - } - - private async Task SendRequestAsync( - HttpRequestMessage request, - CancellationToken cancellationToken = default) - { - request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); - request.Headers.Add("Api-Key", this._config.CompletionApiKey); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(AzureOpenAIChatCompletionWithDataService))); - - try - { - return await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - } - catch (HttpOperationException ex) - { - this._logger.LogError( - "Error occurred on chat completion with data request execution: {ExceptionMessage}", ex.Message); - - throw; - } - } - - private async IAsyncEnumerable InternalGetChatStreamingContentsAsync( - ChatHistory chatHistory, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - OpenAIPromptExecutionSettings chatRequestSettings = OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings); - - using var request = this.GetRequest(chatHistory, chatRequestSettings, isStreamEnabled: true); - using var response = await this.SendRequestAsync(request, cancellationToken).ConfigureAwait(false); - - const string ServerEventPayloadPrefix = "data:"; - - using var stream = await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false); - using var reader = new StreamReader(stream); - - while (!reader.EndOfStream) - { - var body = await reader.ReadLineAsync( -#if NET - cancellationToken -#endif - ).ConfigureAwait(false); - - if (string.IsNullOrWhiteSpace(body)) - { - continue; - } - - if (body.StartsWith(ServerEventPayloadPrefix, StringComparison.Ordinal)) - { - body = body.Substring(ServerEventPayloadPrefix.Length); - } - - var chatWithDataResponse = this.DeserializeResponse(body); - IReadOnlyDictionary metadata = GetResponseMetadata(chatWithDataResponse); - - foreach (var choice in chatWithDataResponse.Choices) - { - yield return new AzureOpenAIWithDataStreamingChatMessageContent(choice, choice.Index, this.GetModelId()!, metadata); - } - } - } - - private T DeserializeResponse(string body) - { - var response = JsonSerializer.Deserialize(body, JsonOptionsCache.ReadPermissive); - - if (response is null) - { - const string ErrorMessage = "Error occurred on chat completion with data response deserialization"; - - this._logger.LogError(ErrorMessage); - - throw new KernelException(ErrorMessage); - } - - return response; - } - - private HttpRequestMessage GetRequest( - ChatHistory chat, - OpenAIPromptExecutionSettings executionSettings, - bool isStreamEnabled) - { - var payload = new ChatWithDataRequest - { - Temperature = executionSettings.Temperature, - TopP = executionSettings.TopP, - IsStreamEnabled = isStreamEnabled, - StopSequences = executionSettings.StopSequences, - MaxTokens = executionSettings.MaxTokens, - PresencePenalty = executionSettings.PresencePenalty, - FrequencyPenalty = executionSettings.FrequencyPenalty, - TokenSelectionBiases = executionSettings.TokenSelectionBiases ?? new Dictionary(), - DataSources = this.GetDataSources(), - Messages = this.GetMessages(chat) - }; - - return HttpRequest.CreatePostRequest(this.GetRequestUri(), payload); - } - - private List GetDataSources() - { - return - [ - new() - { - Parameters = new ChatWithDataSourceParameters - { - Endpoint = this._config.DataSourceEndpoint, - ApiKey = this._config.DataSourceApiKey, - IndexName = this._config.DataSourceIndex - } - } - ]; - } - - private List GetMessages(ChatHistory chat) - { - // The system role as the unique message is not allowed in the With Data APIs. - // This avoids the error: Invalid message request body. Learn how to use Completions extension API, please refer to https://learn.microsoft.com/azure/ai-services/openai/reference#completions-extensions - if (chat.Count == 1 && chat[0].Role == AuthorRole.System) - { - // Converts a system message to a user message if is the unique message in the chat. - chat[0].Role = AuthorRole.User; - } - - return chat - .Select(message => new ChatWithDataMessage - { - Role = message.Role.Label, - Content = message.Content ?? string.Empty - }) - .ToList(); - } - - private string GetRequestUri() - { - const string EndpointUriFormat = "{0}/openai/deployments/{1}/extensions/chat/completions?api-version={2}"; - - var apiVersion = this._config.CompletionApiVersion; - - if (string.IsNullOrWhiteSpace(apiVersion)) - { - apiVersion = DefaultApiVersion; - } - - return string.Format( - CultureInfo.InvariantCulture, - EndpointUriFormat, - this._config.CompletionEndpoint.TrimEnd('/'), - this._config.CompletionModelId, - apiVersion); - } - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataMessage.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataMessage.cs deleted file mode 100644 index ce3a5e5465e3..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataMessage.cs +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataMessage -{ - [JsonPropertyName("role")] - public string Role { get; set; } = string.Empty; - - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataRequest.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataRequest.cs deleted file mode 100644 index 214b917a8a13..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataRequest.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataRequest -{ - [JsonPropertyName("temperature")] - public double Temperature { get; set; } = 0; - - [JsonPropertyName("top_p")] - public double TopP { get; set; } = 0; - - [JsonPropertyName("stream")] - public bool IsStreamEnabled { get; set; } - - [JsonPropertyName("stop")] - public IList? StopSequences { get; set; } = Array.Empty(); - - [JsonPropertyName("max_tokens")] - public int? MaxTokens { get; set; } - - [JsonPropertyName("presence_penalty")] - public double PresencePenalty { get; set; } = 0; - - [JsonPropertyName("frequency_penalty")] - public double FrequencyPenalty { get; set; } = 0; - - [JsonPropertyName("logit_bias")] - public IDictionary TokenSelectionBiases { get; set; } = new Dictionary(); - - [JsonPropertyName("dataSources")] - public IList DataSources { get; set; } = Array.Empty(); - - [JsonPropertyName("messages")] - public IList Messages { get; set; } = Array.Empty(); -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataSource -{ - [JsonPropertyName("type")] - // The current API only supports "AzureCognitiveSearch" as name otherwise an error is returned. - // Validation error at #/dataSources/0: Input tag 'AzureAISearch' found using 'type' does not match any of - // the expected tags: 'AzureCognitiveSearch', 'Elasticsearch', 'AzureCosmosDB', 'Pinecone', 'AzureMLIndex', 'Microsoft365' - public string Type { get; set; } = "AzureCognitiveSearch"; - - [JsonPropertyName("parameters")] - public ChatWithDataSourceParameters Parameters { get; set; } = new ChatWithDataSourceParameters(); -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataSourceParameters -{ - [JsonPropertyName("endpoint")] - public string Endpoint { get; set; } = string.Empty; - - [JsonPropertyName("key")] - public string ApiKey { get; set; } = string.Empty; - - [JsonPropertyName("indexName")] - public string IndexName { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataResponse.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataResponse.cs deleted file mode 100644 index 4ba5e7761319..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataResponse.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -#pragma warning disable CA1812 // Avoid uninstantiated internal classes - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -[method: JsonConstructor] -internal sealed class ChatWithDataResponse(ChatWithDataUsage usage) -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("created")] - public int Created { get; set; } = default; - - [JsonPropertyName("choices")] - public IList Choices { get; set; } = Array.Empty(); - - [JsonPropertyName("usage")] - public ChatWithDataUsage Usage { get; set; } = usage; - - [JsonPropertyName("model")] - public string Model { get; set; } = string.Empty; - - [JsonPropertyName("object")] - public string Object { get; set; } = string.Empty; -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used for JSON deserialization")] -internal sealed class ChatWithDataChoice -{ - [JsonPropertyName("messages")] - public IList Messages { get; set; } = Array.Empty(); -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataUsage -{ - [JsonPropertyName("prompt_tokens")] - public int PromptTokens { get; set; } - - [JsonPropertyName("completion_tokens")] - public int CompletionTokens { get; set; } - - [JsonPropertyName("total_tokens")] - public int TotalTokens { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataStreamingResponse.cs b/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataStreamingResponse.cs deleted file mode 100644 index 9455553d9642..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ChatCompletionWithData/ChatWithDataStreamingResponse.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used for JSON deserialization")] -internal sealed class ChatWithDataStreamingResponse -{ - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("created")] - public int Created { get; set; } = default; - - [JsonPropertyName("model")] - public string Model { get; set; } = string.Empty; - - [JsonPropertyName("object")] - public string Object { get; set; } = string.Empty; - - [JsonPropertyName("choices")] - public IList Choices { get; set; } = Array.Empty(); -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used for JSON deserialization")] -internal sealed class ChatWithDataStreamingChoice -{ - [JsonPropertyName("messages")] - public IList Messages { get; set; } = Array.Empty(); - - [JsonPropertyName("index")] - public int Index { get; set; } = 0; -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -[SuppressMessage("Performance", "CA1812:Avoid uninstantiated internal classes", Justification = "Used for JSON deserialization")] -internal sealed class ChatWithDataStreamingMessage -{ - [JsonPropertyName("delta")] - public ChatWithDataStreamingDelta Delta { get; set; } = new(); - - [JsonPropertyName("end_turn")] - public bool EndTurn { get; set; } -} - -[Experimental("SKEXP0010")] -[Obsolete("This class is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] -internal sealed class ChatWithDataStreamingDelta -{ - [JsonPropertyName("role")] - public string? Role { get; set; } - - [JsonPropertyName("content")] - public string Content { get; set; } = string.Empty; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml b/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml deleted file mode 100644 index 3477ed220ea0..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml +++ /dev/null @@ -1,116 +0,0 @@ - - - - - CP0002 - F:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose.Assistants - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - F:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose.FineTune - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFileService.GetFileContent(System.String,System.Threading.CancellationToken) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToImageService.#ctor(System.String,System.String,System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - F:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose.Assistants - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - F:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose.FineTune - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFileService.GetFileContent(System.String,System.Threading.CancellationToken) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToImageService.#ctor(System.String,System.String,System.Net.Http.HttpClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0007 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0007 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0008 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0008 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFilePurpose - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj deleted file mode 100644 index f873d8d9cd29..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj +++ /dev/null @@ -1,34 +0,0 @@ - - - - - Microsoft.SemanticKernel.Connectors.OpenAI - $(AssemblyName) - net8.0;netstandard2.0 - true - $(NoWarn);NU5104;SKEXP0001,SKEXP0010 - true - - - - - - - - - Semantic Kernel - OpenAI and Azure OpenAI connectors - Semantic Kernel connectors for OpenAI and Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. - - - - - - - - - - - - - - diff --git a/dotnet/src/Connectors/Connectors.OpenAI/CustomClient/OpenAITextToImageClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/CustomClient/OpenAITextToImageClientCore.cs deleted file mode 100644 index 320a7b213bb3..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/CustomClient/OpenAITextToImageClientCore.cs +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Http; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// Base type for OpenAI text to image clients. -internal sealed class OpenAITextToImageClientCore -{ - /// - /// Initializes a new instance of the class. - /// - /// The HttpClient used for making HTTP requests. - /// The to use for logging. If null, no logging will be performed. - internal OpenAITextToImageClientCore(HttpClient? httpClient, ILogger? logger = null) - { - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._logger = logger ?? NullLogger.Instance; - } - - /// - /// Storage for AI service attributes. - /// - internal Dictionary Attributes { get; } = []; - - /// - /// Run the HTTP request to generate a list of images - /// - /// URL for the text to image request API - /// Request payload - /// Function to invoke to extract the desired portion of the text to image response. - /// The to monitor for cancellation requests. The default is . - /// List of image URLs - [Experimental("SKEXP0010")] - internal async Task> ExecuteImageGenerationRequestAsync( - string url, - string requestBody, - Func extractResponseFunc, - CancellationToken cancellationToken = default) - { - var result = await this.ExecutePostRequestAsync(url, requestBody, cancellationToken).ConfigureAwait(false); - return result.Images.Select(extractResponseFunc).ToList(); - } - - /// - /// Add attribute to the internal attribute dictionary if the value is not null or empty. - /// - /// Attribute key - /// Attribute value - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this.Attributes.Add(key, value); - } - } - - /// - /// Logger - /// - private readonly ILogger _logger; - - /// - /// The HttpClient used for making HTTP requests. - /// - private readonly HttpClient _httpClient; - - internal async Task ExecutePostRequestAsync(string url, string requestBody, CancellationToken cancellationToken = default) - { - using var content = new StringContent(requestBody, Encoding.UTF8, "application/json"); - using var response = await this.ExecuteRequestAsync(url, HttpMethod.Post, content, cancellationToken).ConfigureAwait(false); - string responseJson = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - T result = JsonSerializer.Deserialize(responseJson, JsonOptionsCache.ReadPermissive) ?? throw new KernelException("Response JSON parse error"); - return result; - } - - internal event EventHandler? RequestCreated; - - internal async Task ExecuteRequestAsync(string url, HttpMethod method, HttpContent? content, CancellationToken cancellationToken = default) - { - using var request = new HttpRequestMessage(method, url); - - if (content is not null) - { - request.Content = content; - } - - request.Headers.Add("User-Agent", HttpHeaderConstant.Values.UserAgent); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAITextToImageClientCore))); - - this.RequestCreated?.Invoke(this, request); - - var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - - if (this._logger.IsEnabled(LogLevel.Debug)) - { - this._logger.LogDebug("HTTP response: {0} {1}", (int)response.StatusCode, response.StatusCode.ToString("G")); - } - - return response; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs deleted file mode 100644 index 8d87720fa89f..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFilePurpose.cs +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Defines the purpose associated with the uploaded file: -/// https://platform.openai.com/docs/api-reference/files/object#files/object-purpose -/// -[Experimental("SKEXP0010")] -public readonly struct OpenAIFilePurpose : IEquatable -{ - /// - /// File to be used by assistants as input. - /// - public static OpenAIFilePurpose Assistants { get; } = new("assistants"); - - /// - /// File produced as assistants output. - /// - public static OpenAIFilePurpose AssistantsOutput { get; } = new("assistants_output"); - - /// - /// Files uploaded as a batch of API requests - /// - public static OpenAIFilePurpose Batch { get; } = new("batch"); - - /// - /// File produced as result of a file included as a batch request. - /// - public static OpenAIFilePurpose BatchOutput { get; } = new("batch_output"); - - /// - /// File to be used as input to fine-tune a model. - /// - public static OpenAIFilePurpose FineTune { get; } = new("fine-tune"); - - /// - /// File produced as result of fine-tuning a model. - /// - public static OpenAIFilePurpose FineTuneResults { get; } = new("fine-tune-results"); - - /// - /// File to be used for Assistants image file inputs. - /// - public static OpenAIFilePurpose Vision { get; } = new("vision"); - - /// - /// Gets the label associated with this . - /// - public string Label { get; } - - /// - /// Creates a new instance with the provided label. - /// - /// The label to associate with this . - public OpenAIFilePurpose(string label) - { - Verify.NotNullOrWhiteSpace(label, nameof(label)); - this.Label = label!; - } - - /// - /// Returns a value indicating whether two instances are equivalent, as determined by a - /// case-insensitive comparison of their labels. - /// - /// the first instance to compare - /// the second instance to compare - /// true if left and right are both null or have equivalent labels; false otherwise - public static bool operator ==(OpenAIFilePurpose left, OpenAIFilePurpose right) - => left.Equals(right); - - /// - /// Returns a value indicating whether two instances are not equivalent, as determined by a - /// case-insensitive comparison of their labels. - /// - /// the first instance to compare - /// the second instance to compare - /// false if left and right are both null or have equivalent labels; true otherwise - public static bool operator !=(OpenAIFilePurpose left, OpenAIFilePurpose right) - => !(left == right); - - /// - public override bool Equals([NotNullWhen(true)] object? obj) - => obj is OpenAIFilePurpose otherPurpose && this == otherPurpose; - - /// - public bool Equals(OpenAIFilePurpose other) - => string.Equals(this.Label, other.Label, StringComparison.OrdinalIgnoreCase); - - /// - public override int GetHashCode() - => StringComparer.OrdinalIgnoreCase.GetHashCode(this.Label); - - /// - public override string ToString() => this.Label; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs deleted file mode 100644 index 371be0d93a33..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileReference.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// References an uploaded file by id. -/// -[Experimental("SKEXP0010")] -public sealed class OpenAIFileReference -{ - /// - /// The file identifier. - /// - public string Id { get; set; } = string.Empty; - - /// - /// The timestamp the file was uploaded.s - /// - public DateTime CreatedTimestamp { get; set; } - - /// - /// The name of the file.s - /// - public string FileName { get; set; } = string.Empty; - - /// - /// Describes the associated purpose of the file. - /// - public OpenAIFilePurpose Purpose { get; set; } - - /// - /// The file size, in bytes. - /// - public int SizeInBytes { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs deleted file mode 100644 index 690954448eea..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileService.cs +++ /dev/null @@ -1,333 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text.Json; -using System.Text.Json.Serialization; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Http; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// File service access for OpenAI: https://api.openai.com/v1/files -/// -[Experimental("SKEXP0010")] -public sealed class OpenAIFileService -{ - private const string HeaderNameAuthorization = "Authorization"; - private const string HeaderNameAzureApiKey = "api-key"; - private const string HeaderNameOpenAIAssistant = "OpenAI-Beta"; - private const string HeaderNameUserAgent = "User-Agent"; - private const string HeaderOpenAIValueAssistant = "assistants=v1"; - private const string OpenAIApiEndpoint = "https://api.openai.com/v1/"; - private const string OpenAIApiRouteFiles = "files"; - private const string AzureOpenAIApiRouteFiles = "openai/files"; - private const string AzureOpenAIDefaultVersion = "2024-02-15-preview"; - - private readonly string _apiKey; - private readonly HttpClient _httpClient; - private readonly ILogger _logger; - private readonly Uri _serviceUri; - private readonly string? _version; - private readonly string? _organization; - - /// - /// Create an instance of the Azure OpenAI chat completion connector - /// - /// Azure Endpoint URL - /// Azure OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// The API version to target. - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIFileService( - Uri endpoint, - string apiKey, - string? organization = null, - string? version = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNull(apiKey, nameof(apiKey)); - - this._apiKey = apiKey; - this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance; - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._serviceUri = new Uri(this._httpClient.BaseAddress ?? endpoint, AzureOpenAIApiRouteFiles); - this._version = version ?? AzureOpenAIDefaultVersion; - this._organization = organization; - } - - /// - /// Create an instance of the OpenAI chat completion connector - /// - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAIFileService( - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNull(apiKey, nameof(apiKey)); - - this._apiKey = apiKey; - this._logger = loggerFactory?.CreateLogger(typeof(OpenAIFileService)) ?? NullLogger.Instance; - this._httpClient = HttpClientProvider.GetHttpClient(httpClient); - this._serviceUri = new Uri(this._httpClient.BaseAddress ?? new Uri(OpenAIApiEndpoint), OpenAIApiRouteFiles); - this._organization = organization; - } - - /// - /// Remove a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - public async Task DeleteFileAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - - await this.ExecuteDeleteRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false); - } - - /// - /// Retrieve the file content from a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The file content as - /// - /// Files uploaded with do not support content retrieval. - /// - public async Task GetFileContentAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - var contentUri = $"{this._serviceUri}/{id}/content"; - var (stream, mimetype) = await this.StreamGetRequestAsync(contentUri, cancellationToken).ConfigureAwait(false); - - using (stream) - { - using var memoryStream = new MemoryStream(); -#if NETSTANDARD2_0 - const int DefaultCopyBufferSize = 81920; - await stream.CopyToAsync(memoryStream, DefaultCopyBufferSize, cancellationToken).ConfigureAwait(false); -#else - await stream.CopyToAsync(memoryStream, cancellationToken).ConfigureAwait(false); -#endif - return - new(memoryStream.ToArray(), mimetype) - { - Metadata = new Dictionary() { { "id", id } }, - Uri = new Uri(contentUri), - }; - } - } - - /// - /// Retrieve metadata for a previously uploaded file. - /// - /// The uploaded file identifier. - /// The to monitor for cancellation requests. The default is . - /// The metadata associated with the specified file identifier. - public async Task GetFileAsync(string id, CancellationToken cancellationToken = default) - { - Verify.NotNull(id, nameof(id)); - - var result = await this.ExecuteGetRequestAsync($"{this._serviceUri}/{id}", cancellationToken).ConfigureAwait(false); - - return this.ConvertFileReference(result); - } - - /// - /// Retrieve metadata for all previously uploaded files. - /// - /// The to monitor for cancellation requests. The default is . - /// The metadata of all uploaded files. - public Task> GetFilesAsync(CancellationToken cancellationToken = default) - => this.GetFilesAsync(null, cancellationToken); - - /// - /// Retrieve metadata for previously uploaded files - /// - /// The purpose of the files by which to filter. - /// The to monitor for cancellation requests. The default is . - /// The metadata of all uploaded files. - public async Task> GetFilesAsync(OpenAIFilePurpose? filePurpose, CancellationToken cancellationToken = default) - { - var serviceUri = filePurpose.HasValue && !string.IsNullOrEmpty(filePurpose.Value.Label) ? $"{this._serviceUri}?purpose={filePurpose}" : this._serviceUri.ToString(); - var result = await this.ExecuteGetRequestAsync(serviceUri, cancellationToken).ConfigureAwait(false); - - return result.Data.Select(this.ConvertFileReference).ToArray(); - } - - /// - /// Upload a file. - /// - /// The file content as - /// The upload settings - /// The to monitor for cancellation requests. The default is . - /// The file metadata. - public async Task UploadContentAsync(BinaryContent fileContent, OpenAIFileUploadExecutionSettings settings, CancellationToken cancellationToken = default) - { - Verify.NotNull(settings, nameof(settings)); - Verify.NotNull(fileContent.Data, nameof(fileContent.Data)); - - using var formData = new MultipartFormDataContent(); - using var contentPurpose = new StringContent(settings.Purpose.Label); - using var contentFile = new ByteArrayContent(fileContent.Data.Value.ToArray()); - formData.Add(contentPurpose, "purpose"); - formData.Add(contentFile, "file", settings.FileName); - - var result = await this.ExecutePostRequestAsync(this._serviceUri.ToString(), formData, cancellationToken).ConfigureAwait(false); - - return this.ConvertFileReference(result); - } - - private async Task ExecuteDeleteRequestAsync(string url, CancellationToken cancellationToken) - { - using var request = HttpRequest.CreateDeleteRequest(this.PrepareUrl(url)); - this.AddRequestHeaders(request); - using var _ = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - } - - private async Task ExecuteGetRequestAsync(string url, CancellationToken cancellationToken) - { - using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url)); - this.AddRequestHeaders(request); - using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - - var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - - var model = JsonSerializer.Deserialize(body); - - return - model ?? - throw new KernelException($"Unexpected response from {url}") - { - Data = { { "ResponseData", body } }, - }; - } - - private async Task<(Stream Stream, string? MimeType)> StreamGetRequestAsync(string url, CancellationToken cancellationToken) - { - using var request = HttpRequest.CreateGetRequest(this.PrepareUrl(url)); - this.AddRequestHeaders(request); - var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - try - { - return - (new HttpResponseStream( - await response.Content.ReadAsStreamAndTranslateExceptionAsync().ConfigureAwait(false), - response), - response.Content.Headers.ContentType?.MediaType); - } - catch - { - response.Dispose(); - throw; - } - } - - private async Task ExecutePostRequestAsync(string url, HttpContent payload, CancellationToken cancellationToken) - { - using var request = new HttpRequestMessage(HttpMethod.Post, this.PrepareUrl(url)) { Content = payload }; - this.AddRequestHeaders(request); - using var response = await this._httpClient.SendWithSuccessCheckAsync(request, cancellationToken).ConfigureAwait(false); - - var body = await response.Content.ReadAsStringWithExceptionMappingAsync().ConfigureAwait(false); - - var model = JsonSerializer.Deserialize(body); - - return - model ?? - throw new KernelException($"Unexpected response from {url}") - { - Data = { { "ResponseData", body } }, - }; - } - - private string PrepareUrl(string url) - { - if (string.IsNullOrWhiteSpace(this._version)) - { - return url; - } - - return $"{url}?api-version={this._version}"; - } - - private void AddRequestHeaders(HttpRequestMessage request) - { - request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); - request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIFileService))); - - if (!string.IsNullOrWhiteSpace(this._version)) - { - // Azure OpenAI - request.Headers.Add(HeaderNameAzureApiKey, this._apiKey); - return; - } - - // OpenAI - request.Headers.Add(HeaderNameAuthorization, $"Bearer {this._apiKey}"); - - if (!string.IsNullOrEmpty(this._organization)) - { - this._httpClient.DefaultRequestHeaders.Add(OpenAIClientCore.OrganizationKey, this._organization); - } - } - - private OpenAIFileReference ConvertFileReference(FileInfo result) - { - return - new OpenAIFileReference - { - Id = result.Id, - FileName = result.FileName, - CreatedTimestamp = DateTimeOffset.FromUnixTimeSeconds(result.CreatedAt).UtcDateTime, - SizeInBytes = result.Bytes ?? 0, - Purpose = new(result.Purpose), - }; - } - - private sealed class FileInfoList - { - [JsonPropertyName("data")] - public FileInfo[] Data { get; set; } = []; - - [JsonPropertyName("object")] - public string Object { get; set; } = "list"; - } - - private sealed class FileInfo - { - [JsonPropertyName("id")] - public string Id { get; set; } = string.Empty; - - [JsonPropertyName("object")] - public string Object { get; set; } = "file"; - - [JsonPropertyName("bytes")] - public int? Bytes { get; set; } - - [JsonPropertyName("created_at")] - public long CreatedAt { get; set; } - - [JsonPropertyName("filename")] - public string FileName { get; set; } = string.Empty; - - [JsonPropertyName("purpose")] - public string Purpose { get; set; } = string.Empty; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs deleted file mode 100644 index 42011da487f0..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/Files/OpenAIFileUploadExecutionSettings.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Execution serttings associated with Open AI file upload . -/// -[Experimental("SKEXP0010")] -public sealed class OpenAIFileUploadExecutionSettings -{ - /// - /// Initializes a new instance of the class. - /// - /// The file name - /// The file purpose - public OpenAIFileUploadExecutionSettings(string fileName, OpenAIFilePurpose purpose) - { - Verify.NotNull(fileName, nameof(fileName)); - - this.FileName = fileName; - this.Purpose = purpose; - } - - /// - /// The file name. - /// - public string FileName { get; } - - /// - /// The file purpose. - /// - public OpenAIFilePurpose Purpose { get; } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs deleted file mode 100644 index 2a3d2ce7dd61..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIMemoryBuilderExtensions.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using Azure.Core; -using Microsoft.SemanticKernel.Http; -using Microsoft.SemanticKernel.Memory; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Provides extension methods for the class to configure OpenAI and AzureOpenAI connectors. -/// -public static class OpenAIMemoryBuilderExtensions -{ - /// - /// Adds an Azure OpenAI text embeddings service. - /// See https://learn.microsoft.com/azure/cognitive-services/openai for service details. - /// - /// The instance - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Model identifier - /// Custom for HTTP requests. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// Self instance - [Experimental("SKEXP0010")] - public static MemoryBuilder WithAzureOpenAITextEmbeddingGeneration( - this MemoryBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), - loggerFactory, - dimensions)); - } - - /// - /// Adds an Azure OpenAI text embeddings service. - /// See https://learn.microsoft.com/azure/cognitive-services/openai for service details. - /// - /// The instance - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Model identifier - /// Custom for HTTP requests. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// Self instance - [Experimental("SKEXP0010")] - public static MemoryBuilder WithAzureOpenAITextEmbeddingGeneration( - this MemoryBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credential, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - credential, - modelId, - HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), - loggerFactory, - dimensions)); - } - - /// - /// Adds the OpenAI text embeddings service. - /// See https://platform.openai.com/docs for service details. - /// - /// The instance - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// Custom for HTTP requests. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// Self instance - [Experimental("SKEXP0010")] - public static MemoryBuilder WithOpenAITextEmbeddingGeneration( - this MemoryBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - return builder.WithTextEmbeddingGeneration((loggerFactory, builderHttpClient) => - new OpenAITextEmbeddingGenerationService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient ?? builderHttpClient), - loggerFactory, - dimensions)); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs deleted file mode 100644 index 36796c62f7b9..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIPromptExecutionSettings.cs +++ /dev/null @@ -1,432 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Execution settings for an OpenAI completion request. -/// -[JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] -public sealed class OpenAIPromptExecutionSettings : PromptExecutionSettings -{ - /// - /// Temperature controls the randomness of the completion. - /// The higher the temperature, the more random the completion. - /// Default is 1.0. - /// - [JsonPropertyName("temperature")] - public double Temperature - { - get => this._temperature; - - set - { - this.ThrowIfFrozen(); - this._temperature = value; - } - } - - /// - /// TopP controls the diversity of the completion. - /// The higher the TopP, the more diverse the completion. - /// Default is 1.0. - /// - [JsonPropertyName("top_p")] - public double TopP - { - get => this._topP; - - set - { - this.ThrowIfFrozen(); - this._topP = value; - } - } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens - /// based on whether they appear in the text so far, increasing the - /// model's likelihood to talk about new topics. - /// - [JsonPropertyName("presence_penalty")] - public double PresencePenalty - { - get => this._presencePenalty; - - set - { - this.ThrowIfFrozen(); - this._presencePenalty = value; - } - } - - /// - /// Number between -2.0 and 2.0. Positive values penalize new tokens - /// based on their existing frequency in the text so far, decreasing - /// the model's likelihood to repeat the same line verbatim. - /// - [JsonPropertyName("frequency_penalty")] - public double FrequencyPenalty - { - get => this._frequencyPenalty; - - set - { - this.ThrowIfFrozen(); - this._frequencyPenalty = value; - } - } - - /// - /// The maximum number of tokens to generate in the completion. - /// - [JsonPropertyName("max_tokens")] - public int? MaxTokens - { - get => this._maxTokens; - - set - { - this.ThrowIfFrozen(); - this._maxTokens = value; - } - } - - /// - /// Sequences where the completion will stop generating further tokens. - /// - [JsonPropertyName("stop_sequences")] - public IList? StopSequences - { - get => this._stopSequences; - - set - { - this.ThrowIfFrozen(); - this._stopSequences = value; - } - } - - /// - /// How many completions to generate for each prompt. Default is 1. - /// Note: Because this parameter generates many completions, it can quickly consume your token quota. - /// Use carefully and ensure that you have reasonable settings for max_tokens and stop. - /// - [JsonPropertyName("results_per_prompt")] - public int ResultsPerPrompt - { - get => this._resultsPerPrompt; - - set - { - this.ThrowIfFrozen(); - this._resultsPerPrompt = value; - } - } - - /// - /// If specified, the system will make a best effort to sample deterministically such that repeated requests with the - /// same seed and parameters should return the same result. Determinism is not guaranteed. - /// - [JsonPropertyName("seed")] - public long? Seed - { - get => this._seed; - - set - { - this.ThrowIfFrozen(); - this._seed = value; - } - } - - /// - /// Gets or sets the response format to use for the completion. - /// - /// - /// Possible values are: "json_object", "text", object. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("response_format")] - public object? ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The system prompt to use when generating text using a chat model. - /// Defaults to "Assistant is a large language model." - /// - [JsonPropertyName("chat_system_prompt")] - public string? ChatSystemPrompt - { - get => this._chatSystemPrompt; - - set - { - this.ThrowIfFrozen(); - this._chatSystemPrompt = value; - } - } - - /// - /// Modify the likelihood of specified tokens appearing in the completion. - /// - [JsonPropertyName("token_selection_biases")] - public IDictionary? TokenSelectionBiases - { - get => this._tokenSelectionBiases; - - set - { - this.ThrowIfFrozen(); - this._tokenSelectionBiases = value; - } - } - - /// - /// Gets or sets the behavior for how tool calls are handled. - /// - /// - /// - /// To disable all tool calling, set the property to null (the default). - /// - /// To request that the model use a specific function, set the property to an instance returned - /// from . - /// - /// - /// To allow the model to request one of any number of functions, set the property to an - /// instance returned from , called with - /// a list of the functions available. - /// - /// - /// To allow the model to request one of any of the functions in the supplied , - /// set the property to if the client should simply - /// send the information about the functions and not handle the response in any special manner, or - /// if the client should attempt to automatically - /// invoke the function and send the result back to the service. - /// - /// - /// For all options where an instance is provided, auto-invoke behavior may be selected. If the service - /// sends a request for a function call, if auto-invoke has been requested, the client will attempt to - /// resolve that function from the functions available in the , and if found, rather - /// than returning the response back to the caller, it will handle the request automatically, invoking - /// the function, and sending back the result. The intermediate messages will be retained in the - /// if an instance was provided. - /// - public ToolCallBehavior? ToolCallBehavior - { - get => this._toolCallBehavior; - - set - { - this.ThrowIfFrozen(); - this._toolCallBehavior = value; - } - } - - /// - /// A unique identifier representing your end-user, which can help OpenAI to monitor and detect abuse - /// - public string? User - { - get => this._user; - - set - { - this.ThrowIfFrozen(); - this._user = value; - } - } - - /// - /// Whether to return log probabilities of the output tokens or not. - /// If true, returns the log probabilities of each output token returned in the `content` of `message`. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("logprobs")] - public bool? Logprobs - { - get => this._logprobs; - - set - { - this.ThrowIfFrozen(); - this._logprobs = value; - } - } - - /// - /// An integer specifying the number of most likely tokens to return at each token position, each with an associated log probability. - /// - [Experimental("SKEXP0010")] - [JsonPropertyName("top_logprobs")] - public int? TopLogprobs - { - get => this._topLogprobs; - - set - { - this.ThrowIfFrozen(); - this._topLogprobs = value; - } - } - - /// - /// An abstraction of additional settings for chat completion, see https://learn.microsoft.com/en-us/dotnet/api/azure.ai.openai.azurechatextensionsoptions. - /// This property is compatible only with Azure OpenAI. - /// - [Experimental("SKEXP0010")] - [JsonIgnore] - public AzureChatExtensionsOptions? AzureChatExtensionsOptions - { - get => this._azureChatExtensionsOptions; - - set - { - this.ThrowIfFrozen(); - this._azureChatExtensionsOptions = value; - } - } - - /// - public override void Freeze() - { - if (this.IsFrozen) - { - return; - } - - base.Freeze(); - - if (this._stopSequences is not null) - { - this._stopSequences = new ReadOnlyCollection(this._stopSequences); - } - - if (this._tokenSelectionBiases is not null) - { - this._tokenSelectionBiases = new ReadOnlyDictionary(this._tokenSelectionBiases); - } - } - - /// - public override PromptExecutionSettings Clone() - { - return new OpenAIPromptExecutionSettings() - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Temperature = this.Temperature, - TopP = this.TopP, - PresencePenalty = this.PresencePenalty, - FrequencyPenalty = this.FrequencyPenalty, - MaxTokens = this.MaxTokens, - StopSequences = this.StopSequences is not null ? new List(this.StopSequences) : null, - ResultsPerPrompt = this.ResultsPerPrompt, - Seed = this.Seed, - ResponseFormat = this.ResponseFormat, - TokenSelectionBiases = this.TokenSelectionBiases is not null ? new Dictionary(this.TokenSelectionBiases) : null, - ToolCallBehavior = this.ToolCallBehavior, - User = this.User, - ChatSystemPrompt = this.ChatSystemPrompt, - Logprobs = this.Logprobs, - TopLogprobs = this.TopLogprobs, - AzureChatExtensionsOptions = this.AzureChatExtensionsOptions, - }; - } - - /// - /// Default max tokens for a text generation - /// - internal static int DefaultTextMaxTokens { get; } = 256; - - /// - /// Create a new settings object with the values from another settings object. - /// - /// Template configuration - /// Default max tokens - /// An instance of OpenAIPromptExecutionSettings - public static OpenAIPromptExecutionSettings FromExecutionSettings(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) - { - if (executionSettings is null) - { - return new OpenAIPromptExecutionSettings() - { - MaxTokens = defaultMaxTokens - }; - } - - if (executionSettings is OpenAIPromptExecutionSettings settings) - { - return settings; - } - - var json = JsonSerializer.Serialize(executionSettings); - - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - if (openAIExecutionSettings is not null) - { - return openAIExecutionSettings; - } - - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(OpenAIPromptExecutionSettings)}", nameof(executionSettings)); - } - - /// - /// Create a new settings object with the values from another settings object. - /// - /// Template configuration - /// Default max tokens - /// An instance of OpenAIPromptExecutionSettings - [Obsolete("This method is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] - public static OpenAIPromptExecutionSettings FromExecutionSettingsWithData(PromptExecutionSettings? executionSettings, int? defaultMaxTokens = null) - { - var settings = FromExecutionSettings(executionSettings, defaultMaxTokens); - - if (settings.StopSequences?.Count == 0) - { - // Azure OpenAI WithData API does not allow to send empty array of stop sequences - // Gives back "Validation error at #/stop/str: Input should be a valid string\nValidation error at #/stop/list[str]: List should have at least 1 item after validation, not 0" - settings.StopSequences = null; - } - - return settings; - } - - #region private ================================================================================ - - private double _temperature = 1; - private double _topP = 1; - private double _presencePenalty; - private double _frequencyPenalty; - private int? _maxTokens; - private IList? _stopSequences; - private int _resultsPerPrompt = 1; - private long? _seed; - private object? _responseFormat; - private IDictionary? _tokenSelectionBiases; - private ToolCallBehavior? _toolCallBehavior; - private string? _user; - private string? _chatSystemPrompt; - private bool? _logprobs; - private int? _topLogprobs; - private AzureChatExtensionsOptions? _azureChatExtensionsOptions; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs deleted file mode 100644 index 80cc60944965..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/OpenAIServiceCollectionExtensions.cs +++ /dev/null @@ -1,2042 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using Azure; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.Http; -using Microsoft.SemanticKernel.TextGeneration; -using Microsoft.SemanticKernel.TextToAudio; -using Microsoft.SemanticKernel.TextToImage; - -#pragma warning disable CA2000 // Dispose objects before losing scope -#pragma warning disable IDE0039 // Use local function - -namespace Microsoft.SemanticKernel; - -/// -/// Provides extension methods for and related classes to configure OpenAI and Azure OpenAI connectors. -/// -public static class OpenAIServiceCollectionExtensions -{ - #region Text Completion - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAITextGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - { - var client = CreateAzureOpenAIClient(endpoint, new AzureKeyCredential(apiKey), httpClient ?? serviceProvider.GetService()); - return new AzureOpenAITextGenerationService(deploymentName, client, modelId, serviceProvider.GetService()); - }); - - return builder; - } - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAITextGeneration( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - { - var client = CreateAzureOpenAIClient(endpoint, new AzureKeyCredential(apiKey), serviceProvider.GetService()); - return new AzureOpenAITextGenerationService(deploymentName, client, modelId, serviceProvider.GetService()); - }); - } - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAITextGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - { - var client = CreateAzureOpenAIClient(endpoint, credentials, httpClient ?? serviceProvider.GetService()); - return new AzureOpenAITextGenerationService(deploymentName, client, modelId, serviceProvider.GetService()); - }); - - return builder; - } - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAITextGeneration( - this IServiceCollection services, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - { - var client = CreateAzureOpenAIClient(endpoint, credentials, serviceProvider.GetService()); - return new AzureOpenAITextGenerationService(deploymentName, client, modelId, serviceProvider.GetService()); - }); - } - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IKernelBuilder AddAzureOpenAITextGeneration( - this IKernelBuilder builder, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextGenerationService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds an Azure OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAITextGeneration( - this IServiceCollection services, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextGenerationService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService())); - } - - /// - /// Adds an OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddOpenAITextGeneration( - this IKernelBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextGenerationService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds an OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - public static IServiceCollection AddOpenAITextGeneration( - this IServiceCollection services, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextGenerationService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - /// - /// Adds an OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - public static IKernelBuilder AddOpenAITextGeneration( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextGenerationService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds an OpenAI text generation service with the specified configuration. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - public static IServiceCollection AddOpenAITextGeneration(this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextGenerationService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService())); - } - - #endregion - - #region Text Embedding - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - int? dimensions = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService(), - dimensions)); - } - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credential, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credential); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - credential, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( - this IServiceCollection services, - string deploymentName, - string endpoint, - TokenCredential credential, - string? serviceId = null, - string? modelId = null, - int? dimensions = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credential); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - endpoint, - credential, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService(), - dimensions)); - } - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService(), - dimensions)); - - return builder; - } - - /// - /// Adds an Azure OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextEmbeddingGeneration( - this IServiceCollection services, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null, - int? dimensions = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextEmbeddingGenerationService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService(), - dimensions)); - } - - /// - /// Adds the OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextEmbeddingGenerationService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - - /// - /// Adds the OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAITextEmbeddingGeneration( - this IServiceCollection services, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - int? dimensions = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextEmbeddingGenerationService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService(), - dimensions)); - } - - /// - /// Adds the OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAITextEmbeddingGeneration( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null, - int? dimensions = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextEmbeddingGenerationService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService(), - dimensions)); - - return builder; - } - - /// - /// Adds the OpenAI text embeddings service to the list. - /// - /// The instance to augment. - /// The OpenAI model id. - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAITextEmbeddingGeneration(this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null, - int? dimensions = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextEmbeddingGenerationService( - modelId, - openAIClient ?? serviceProvider.GetRequiredService(), - serviceProvider.GetService(), - dimensions)); - } - - #endregion - - #region Chat Completion - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - new AzureKeyCredential(apiKey), - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAIChatCompletion( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - new AzureKeyCredential(apiKey), - HttpClientProvider.GetHttpClient(serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - credentials, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAIChatCompletion( - this IServiceCollection services, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - credentials, - HttpClientProvider.GetHttpClient(serviceProvider)); - - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - Func factory = (serviceProvider, _) => - new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - public static IServiceCollection AddAzureOpenAIChatCompletion( - this IServiceCollection services, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - - Func factory = (serviceProvider, _) => - new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Azure OpenAI chat completion with data service to the list. - /// - /// The instance. - /// Required configuration for Azure OpenAI chat completion with data. - /// A local identifier for the given AI service. - /// The same instance as . - /// - /// More information: - /// - [Experimental("SKEXP0010")] - [Obsolete("This method is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] - public static IKernelBuilder AddAzureOpenAIChatCompletion( - this IKernelBuilder builder, - AzureOpenAIChatCompletionWithDataConfig config, - string? serviceId = null) - { - Verify.NotNull(builder); - Verify.NotNull(config); - - Func factory = (serviceProvider, _) => - new(config, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI chat completion with data service to the list. - /// - /// The instance. - /// Required configuration for Azure OpenAI chat completion with data. - /// A local identifier for the given AI service. - /// The same instance as . - /// - /// More information: - /// - [Experimental("SKEXP0010")] - [Obsolete("This method is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions")] - public static IServiceCollection AddAzureOpenAIChatCompletion( - this IServiceCollection services, - AzureOpenAIChatCompletionWithDataConfig config, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNull(config); - - Func factory = (serviceProvider, _) => - new(config, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - public static IKernelBuilder AddOpenAIChatCompletion( - this IKernelBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - new(modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - public static IServiceCollection AddOpenAIChatCompletion( - this IServiceCollection services, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - new(modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model id - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - public static IKernelBuilder AddOpenAIChatCompletion( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model id - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - public static IServiceCollection AddOpenAIChatCompletion(this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Custom OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// A Custom Message API compatible endpoint. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAIChatCompletion( - this IServiceCollection services, - string modelId, - Uri endpoint, - string? apiKey = null, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId, - endpoint, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Custom Endpoint OpenAI chat completion service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// Custom OpenAI Compatible Message API endpoint - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAIChatCompletion( - this IKernelBuilder builder, - string modelId, - Uri endpoint, - string? apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId: modelId, - apiKey: apiKey, - endpoint: endpoint, - organization: orgId, - httpClient: HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - loggerFactory: serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - #endregion - - #region Images - - /// - /// Add the Azure OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Model identifier - /// A local identifier for the given AI service - /// Azure OpenAI API version - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextToImage( - this IServiceCollection services, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? modelId = null, - string? serviceId = null, - string? apiVersion = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - endpoint, - credentials, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService(), - apiVersion)); - } - - /// - /// Add the Azure OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Model identifier - /// A local identifier for the given AI service - /// Azure OpenAI API version - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextToImage( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? modelId = null, - string? serviceId = null, - string? apiVersion = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - endpoint, - credentials, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService(), - apiVersion)); - - return builder; - } - - /// - /// Add the Azure OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Azure OpenAI API key - /// Model identifier - /// A local identifier for the given AI service - /// Azure OpenAI API version - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextToImage( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - string? serviceId = null, - string? apiVersion = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService(), - apiVersion)); - - return builder; - } - - /// - /// Add the Azure OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Azure OpenAI API key - /// A local identifier for the given AI service - /// Model identifier - /// Maximum number of attempts to retrieve the text to image operation result. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextToImage( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - int maxRetryCount = 5) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - /// - /// Add the OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// The model to use for image generation. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAITextToImage( - this IKernelBuilder builder, - string apiKey, - string? orgId = null, - string? modelId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToImageService( - apiKey, - orgId, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Add the OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// The model to use for image generation. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAITextToImage(this IServiceCollection services, - string apiKey, - string? orgId = null, - string? modelId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToImageService( - apiKey, - orgId, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - /// - /// Add the OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// Model identifier - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAITextToImage( - this IServiceCollection services, - string deploymentName, - OpenAIClient? openAIClient = null, - string? modelId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService())); - } - - /// - /// Add the OpenAI Dall-E text to image service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// Model identifier - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAITextToImage( - this IKernelBuilder builder, - string deploymentName, - OpenAIClient? openAIClient = null, - string? modelId = null, - string? serviceId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToImageService( - deploymentName, - openAIClient ?? serviceProvider.GetRequiredService(), - modelId, - serviceProvider.GetService())); - - return builder; - } - - #endregion - - #region Files - - /// - /// Add the OpenAI file service to the list - /// - /// The instance to augment. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddOpenAIFiles( - this IKernelBuilder builder, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAIFileService( - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Add the OpenAI file service to the list - /// - /// The instance to augment. - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddOpenAIFiles( - this IServiceCollection services, - string apiKey, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(apiKey); - - services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAIFileService( - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - - return services; - } - - /// - /// Add the OpenAI file service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment URL - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// The API version to target. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0010")] - public static IKernelBuilder AddAzureOpenAIFiles( - this IKernelBuilder builder, - string endpoint, - string apiKey, - string? orgId = null, - string? version = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAIFileService( - new Uri(endpoint), - apiKey, - orgId, - version, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Add the OpenAI file service to the list - /// - /// The instance to augment. - /// Azure OpenAI deployment URL - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// The API version to target. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0010")] - public static IServiceCollection AddAzureOpenAIFiles( - this IServiceCollection services, - string endpoint, - string apiKey, - string? orgId = null, - string? version = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(apiKey); - - services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAIFileService( - new Uri(endpoint), - apiKey, - orgId, - version, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - - return services; - } - - #endregion - - #region Text-to-Audio - - /// - /// Adds the Azure OpenAI text-to-audio service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Azure OpenAI API key - /// A local identifier for the given AI service - /// Model identifier - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddAzureOpenAITextToAudio( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToAudioService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds the Azure OpenAI text-to-audio service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name - /// Azure OpenAI deployment URL - /// Azure OpenAI API key - /// A local identifier for the given AI service - /// Model identifier - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddAzureOpenAITextToAudio( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new AzureOpenAITextToAudioService( - deploymentName, - endpoint, - apiKey, - modelId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - /// - /// Adds the OpenAI text-to-audio service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddOpenAITextToAudio( - this IKernelBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - builder.Services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToAudioService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService())); - - return builder; - } - - /// - /// Adds the OpenAI text-to-audio service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddOpenAITextToAudio( - this IServiceCollection services, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - return services.AddKeyedSingleton(serviceId, (serviceProvider, _) => - new OpenAITextToAudioService( - modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService())); - } - - #endregion - - #region Audio-to-Text - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddAzureOpenAIAudioToText( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - new AzureKeyCredential(apiKey), - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddAzureOpenAIAudioToText( - this IServiceCollection services, - string deploymentName, - string endpoint, - string apiKey, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - new AzureKeyCredential(apiKey), - HttpClientProvider.GetHttpClient(serviceProvider)); - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddAzureOpenAIAudioToText( - this IKernelBuilder builder, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - credentials, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider)); - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddAzureOpenAIAudioToText( - this IServiceCollection services, - string deploymentName, - string endpoint, - TokenCredential credentials, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - Verify.NotNullOrWhiteSpace(endpoint); - Verify.NotNull(credentials); - - Func factory = (serviceProvider, _) => - { - OpenAIClient client = CreateAzureOpenAIClient( - endpoint, - credentials, - HttpClientProvider.GetHttpClient(serviceProvider)); - return new(deploymentName, client, modelId, serviceProvider.GetService()); - }; - - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddAzureOpenAIAudioToText( - this IKernelBuilder builder, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(deploymentName); - - Func factory = (serviceProvider, _) => - new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the Azure OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// Model identifier, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddAzureOpenAIAudioToText( - this IServiceCollection services, - string deploymentName, - OpenAIClient? openAIClient = null, - string? serviceId = null, - string? modelId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(deploymentName); - - Func factory = (serviceProvider, _) => - new(deploymentName, openAIClient ?? serviceProvider.GetRequiredService(), modelId, serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The HttpClient to use with this service. - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddOpenAIAudioToText( - this IKernelBuilder builder, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null, - HttpClient? httpClient = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - new(modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(httpClient, serviceProvider), - serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// OpenAI model name, see https://platform.openai.com/docs/models - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddOpenAIAudioToText( - this IServiceCollection services, - string modelId, - string apiKey, - string? orgId = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - Verify.NotNullOrWhiteSpace(apiKey); - - Func factory = (serviceProvider, _) => - new(modelId, - apiKey, - orgId, - HttpClientProvider.GetHttpClient(serviceProvider), - serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - /// - /// Adds the OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// OpenAI model id - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0001")] - public static IKernelBuilder AddOpenAIAudioToText( - this IKernelBuilder builder, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(builder); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - - builder.Services.AddKeyedSingleton(serviceId, factory); - - return builder; - } - - /// - /// Adds the OpenAI audio-to-text service to the list. - /// - /// The instance to augment. - /// OpenAI model id - /// to use for the service. If null, one must be available in the service provider when this service is resolved. - /// A local identifier for the given AI service - /// The same instance as . - [Experimental("SKEXP0001")] - public static IServiceCollection AddOpenAIAudioToText( - this IServiceCollection services, - string modelId, - OpenAIClient? openAIClient = null, - string? serviceId = null) - { - Verify.NotNull(services); - Verify.NotNullOrWhiteSpace(modelId); - - Func factory = (serviceProvider, _) => - new(modelId, openAIClient ?? serviceProvider.GetRequiredService(), serviceProvider.GetService()); - - services.AddKeyedSingleton(serviceId, factory); - - return services; - } - - #endregion - - private static OpenAIClient CreateAzureOpenAIClient(string endpoint, AzureKeyCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); - - private static OpenAIClient CreateAzureOpenAIClient(string endpoint, TokenCredential credentials, HttpClient? httpClient) => - new(new Uri(endpoint), credentials, ClientCore.GetOpenAIClientOptions(httpClient)); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs deleted file mode 100644 index 63fbdbdccb2b..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationService.cs +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI text embedding service. -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService -{ - private readonly AzureOpenAIClientCore _core; - private readonly int? _dimensions; - - /// - /// Creates a new client instance using API Key auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - public AzureOpenAITextEmbeddingGenerationService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null, - int? dimensions = null) - { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - - this._dimensions = dimensions; - } - - /// - /// Creates a new client instance supporting AAD auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - public AzureOpenAITextEmbeddingGenerationService( - string deploymentName, - string endpoint, - TokenCredential credential, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null, - int? dimensions = null) - { - this._core = new(deploymentName, endpoint, credential, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - - this._dimensions = dimensions; - } - - /// - /// Creates a new client. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// The to use for logging. If null, no logging will be performed. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - public AzureOpenAITextEmbeddingGenerationService( - string deploymentName, - OpenAIClient openAIClient, - string? modelId = null, - ILoggerFactory? loggerFactory = null, - int? dimensions = null) - { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextEmbeddingGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - - this._dimensions = dimensions; - } - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - public Task>> GenerateEmbeddingsAsync( - IList data, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - { - return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationService.cs deleted file mode 100644 index c940a7caf291..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationService.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.Services; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text embedding service. -/// -[Experimental("SKEXP0010")] -public sealed class OpenAITextEmbeddingGenerationService : ITextEmbeddingGenerationService -{ - private readonly OpenAIClientCore _core; - private readonly int? _dimensions; - - /// - /// Create an instance of the OpenAI text embedding connector - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - public OpenAITextEmbeddingGenerationService( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null, - int? dimensions = null) - { - this._core = new( - modelId: modelId, - apiKey: apiKey, - organization: organization, - httpClient: httpClient, - logger: loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - - this._dimensions = dimensions; - } - - /// - /// Create an instance of the OpenAI text embedding connector - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - /// The number of dimensions the resulting output embeddings should have. Only supported in "text-embedding-3" and later models. - public OpenAITextEmbeddingGenerationService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null, - int? dimensions = null) - { - this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextEmbeddingGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - - this._dimensions = dimensions; - } - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - public Task>> GenerateEmbeddingsAsync( - IList data, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - { - this._core.LogActionDetails(); - return this._core.GetEmbeddingsAsync(data, kernel, this._dimensions, cancellationToken); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/AzureOpenAITextGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/AzureOpenAITextGenerationService.cs deleted file mode 100644 index 20111ca99f88..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/AzureOpenAITextGenerationService.cs +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextGeneration; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI text generation client. -/// -public sealed class AzureOpenAITextGenerationService : ITextGenerationService -{ - private readonly AzureOpenAIClientCore _core; - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - /// Creates a new client instance using API Key auth - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAITextGenerationService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, apiKey, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextGenerationService))); - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Creates a new client instance supporting AAD auth - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAITextGenerationService( - string deploymentName, - string endpoint, - TokenCredential credential, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, endpoint, credential, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - /// Creates a new client instance using the specified OpenAIClient - /// - /// Azure OpenAI model ID or deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom . - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAITextGenerationService( - string deploymentName, - OpenAIClient openAIClient, - string? modelId = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new(deploymentName, openAIClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return this._core.GetTextResultsAsync(prompt, executionSettings, kernel, cancellationToken); - } - - /// - public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return this._core.GetStreamingTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/OpenAITextGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/OpenAITextGenerationService.cs deleted file mode 100644 index 1133865171fd..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextGeneration/OpenAITextGenerationService.cs +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextGeneration; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text generation service. -/// -public sealed class OpenAITextGenerationService : ITextGenerationService -{ - private readonly OpenAIClientCore _core; - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - /// Create an instance of the OpenAI text generation connector - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextGenerationService( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._core = new( - modelId: modelId, - apiKey: apiKey, - organization: organization, - httpClient: httpClient, - logger: loggerFactory?.CreateLogger(typeof(OpenAITextGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization); - } - - /// - /// Create an instance of the OpenAI text generation connector - /// - /// Model name - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextGenerationService( - string modelId, - OpenAIClient openAIClient, - ILoggerFactory? loggerFactory = null) - { - this._core = new(modelId, openAIClient, loggerFactory?.CreateLogger(typeof(OpenAITextGenerationService))); - - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public Task> GetTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return this._core.GetTextResultsAsync(prompt, executionSettings, kernel, cancellationToken); - } - - /// - public IAsyncEnumerable GetStreamingTextContentsAsync(string prompt, PromptExecutionSettings? executionSettings = null, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - return this._core.GetStreamingTextContentsAsync(prompt, executionSettings, kernel, cancellationToken); - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/AzureOpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/AzureOpenAITextToAudioService.cs deleted file mode 100644 index 47aac090ab05..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/AzureOpenAITextToAudioService.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextToAudio; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI text-to-audio service. -/// -[Experimental("SKEXP0001")] -public sealed class AzureOpenAITextToAudioService : ITextToAudioService -{ - /// - /// Azure OpenAI text-to-audio client for HTTP operations. - /// - private readonly AzureOpenAITextToAudioClient _client; - - /// - public IReadOnlyDictionary Attributes => this._client.Attributes; - - /// - /// Gets the key used to store the deployment name in the dictionary. - /// - public static string DeploymentNameKey => "DeploymentName"; - - /// - /// Creates an instance of the connector with API key auth. - /// - /// Azure OpenAI deployment name, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Azure OpenAI deployment URL, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI API key, see https://learn.microsoft.com/azure/cognitive-services/openai/quickstart - /// Azure OpenAI model id, see https://learn.microsoft.com/azure/cognitive-services/openai/how-to/create-resource - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public AzureOpenAITextToAudioService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._client = new(deploymentName, endpoint, apiKey, modelId, httpClient, loggerFactory?.CreateLogger(typeof(AzureOpenAITextToAudioService))); - - this._client.AddAttribute(DeploymentNameKey, deploymentName); - this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - /// - public Task> GetAudioContentsAsync( - string text, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioExecutionSettings.cs deleted file mode 100644 index ddb97ff93c35..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioExecutionSettings.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json; -using System.Text.Json.Serialization; -using Microsoft.SemanticKernel.Text; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Execution settings for OpenAI text-to-audio request. -/// -[Experimental("SKEXP0001")] -public sealed class OpenAITextToAudioExecutionSettings : PromptExecutionSettings -{ - /// - /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. - /// - [JsonPropertyName("voice")] - public string Voice - { - get => this._voice; - - set - { - this.ThrowIfFrozen(); - this._voice = value; - } - } - - /// - /// The format to audio in. Supported formats are mp3, opus, aac, and flac. - /// - [JsonPropertyName("response_format")] - public string ResponseFormat - { - get => this._responseFormat; - - set - { - this.ThrowIfFrozen(); - this._responseFormat = value; - } - } - - /// - /// The speed of the generated audio. Select a value from 0.25 to 4.0. 1.0 is the default. - /// - [JsonPropertyName("speed")] - public float Speed - { - get => this._speed; - - set - { - this.ThrowIfFrozen(); - this._speed = value; - } - } - - /// - /// Creates an instance of class with default voice - "alloy". - /// - public OpenAITextToAudioExecutionSettings() - : this(DefaultVoice) - { - } - - /// - /// Creates an instance of class. - /// - /// The voice to use when generating the audio. Supported voices are alloy, echo, fable, onyx, nova, and shimmer. - public OpenAITextToAudioExecutionSettings(string voice) - { - this._voice = voice; - } - - /// - public override PromptExecutionSettings Clone() - { - return new OpenAITextToAudioExecutionSettings(this.Voice) - { - ModelId = this.ModelId, - ExtensionData = this.ExtensionData is not null ? new Dictionary(this.ExtensionData) : null, - Speed = this.Speed, - ResponseFormat = this.ResponseFormat - }; - } - - /// - /// Converts to derived type. - /// - /// Instance of . - /// Instance of . - public static OpenAITextToAudioExecutionSettings? FromExecutionSettings(PromptExecutionSettings? executionSettings) - { - if (executionSettings is null) - { - return new OpenAITextToAudioExecutionSettings(); - } - - if (executionSettings is OpenAITextToAudioExecutionSettings settings) - { - return settings; - } - - var json = JsonSerializer.Serialize(executionSettings); - - var openAIExecutionSettings = JsonSerializer.Deserialize(json, JsonOptionsCache.ReadPermissive); - - if (openAIExecutionSettings is not null) - { - return openAIExecutionSettings; - } - - throw new ArgumentException($"Invalid execution settings, cannot convert to {nameof(OpenAITextToAudioExecutionSettings)}", nameof(executionSettings)); - } - - #region private ================================================================================ - - private const string DefaultVoice = "alloy"; - - private float _speed = 1.0f; - private string _responseFormat = "mp3"; - private string _voice; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioService.cs deleted file mode 100644 index 177acf539a41..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/OpenAITextToAudioService.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextToAudio; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text-to-audio service. -/// -[Experimental("SKEXP0001")] -public sealed class OpenAITextToAudioService : ITextToAudioService -{ - /// - /// OpenAI text-to-audio client for HTTP operations. - /// - private readonly OpenAITextToAudioClient _client; - - /// - /// Gets the attribute name used to store the organization in the dictionary. - /// - public static string OrganizationKey => "Organization"; - - /// - public IReadOnlyDictionary Attributes => this._client.Attributes; - - /// - /// Creates an instance of the with API key auth. - /// - /// Model name - /// OpenAI API Key - /// OpenAI Organization Id (usually optional) - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextToAudioService( - string modelId, - string apiKey, - string? organization = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - this._client = new(modelId, apiKey, organization, httpClient, loggerFactory?.CreateLogger(typeof(OpenAITextToAudioService))); - - this._client.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - this._client.AddAttribute(OrganizationKey, organization); - } - - /// - public Task> GetAudioContentsAsync( - string text, - PromptExecutionSettings? executionSettings = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - => this._client.GetAudioContentsAsync(text, executionSettings, cancellationToken); -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/TextToAudioRequest.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/TextToAudioRequest.cs deleted file mode 100644 index bc7aeede3b57..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToAudio/TextToAudioRequest.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text-to-audio request model, see . -/// -internal sealed class TextToAudioRequest(string model, string input, string voice) -{ - [JsonPropertyName("model")] - public string Model { get; set; } = model; - - [JsonPropertyName("input")] - public string Input { get; set; } = input; - - [JsonPropertyName("voice")] - public string Voice { get; set; } = voice; - - [JsonPropertyName("response_format")] - public string ResponseFormat { get; set; } = "mp3"; - - [JsonPropertyName("speed")] - public float Speed { get; set; } = 1.0f; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/AzureOpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/AzureOpenAITextToImageService.cs deleted file mode 100644 index efa3ffcc87c0..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/AzureOpenAITextToImageService.cs +++ /dev/null @@ -1,212 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Azure; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextToImage; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Azure OpenAI Image generation -/// -/// -[Experimental("SKEXP0010")] -public sealed class AzureOpenAITextToImageService : ITextToImageService -{ - private readonly OpenAIClient _client; - private readonly ILogger _logger; - private readonly string _deploymentName; - private readonly Dictionary _attributes = []; - - /// - public IReadOnlyDictionary Attributes => this._attributes; - - /// - /// Gets the key used to store the deployment name in the dictionary. - /// - public static string DeploymentNameKey => "DeploymentName"; - - /// - /// Create a new instance of Azure OpenAI image generation service - /// - /// Deployment name identifier - /// Azure OpenAI deployment URL - /// Azure OpenAI API key - /// Model identifier - /// Custom for HTTP requests. - /// The ILoggerFactory used to create a logger for logging. If null, no logging will be performed. - /// Azure OpenAI Endpoint ApiVersion - public AzureOpenAITextToImageService( - string deploymentName, - string endpoint, - string apiKey, - string? modelId, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null, - string? apiVersion = null) - { - Verify.NotNullOrWhiteSpace(apiKey); - Verify.NotNullOrWhiteSpace(deploymentName); - - this._deploymentName = deploymentName; - - if (modelId is not null) - { - this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - this.AddAttribute(DeploymentNameKey, deploymentName); - - this._logger = loggerFactory?.CreateLogger(typeof(AzureOpenAITextToImageService)) ?? NullLogger.Instance; - - var connectorEndpoint = (!string.IsNullOrWhiteSpace(endpoint) ? endpoint! : httpClient?.BaseAddress?.AbsoluteUri) ?? - throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); - - this._client = new(new Uri(connectorEndpoint), - new AzureKeyCredential(apiKey), - GetClientOptions(httpClient, apiVersion)); - } - - /// - /// Create a new instance of Azure OpenAI image generation service - /// - /// Deployment name identifier - /// Azure OpenAI deployment URL - /// Token credentials, e.g. DefaultAzureCredential, ManagedIdentityCredential, EnvironmentCredential, etc. - /// Model identifier - /// Custom for HTTP requests. - /// The ILoggerFactory used to create a logger for logging. If null, no logging will be performed. - /// Azure OpenAI Endpoint ApiVersion - public AzureOpenAITextToImageService( - string deploymentName, - string endpoint, - TokenCredential credential, - string? modelId, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null, - string? apiVersion = null) - { - Verify.NotNull(credential); - Verify.NotNullOrWhiteSpace(deploymentName); - - this._deploymentName = deploymentName; - - if (modelId is not null) - { - this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - this.AddAttribute(DeploymentNameKey, deploymentName); - - this._logger = loggerFactory?.CreateLogger(typeof(AzureOpenAITextToImageService)) ?? NullLogger.Instance; - - var connectorEndpoint = !string.IsNullOrWhiteSpace(endpoint) ? endpoint! : httpClient?.BaseAddress?.AbsoluteUri; - if (connectorEndpoint is null) - { - throw new ArgumentException($"The {nameof(httpClient)}.{nameof(HttpClient.BaseAddress)} and {nameof(endpoint)} are both null or empty. Please ensure at least one is provided."); - } - - this._client = new(new Uri(connectorEndpoint), - credential, - GetClientOptions(httpClient, apiVersion)); - } - - /// - /// Create a new instance of Azure OpenAI image generation service - /// - /// Deployment name identifier - /// to use for the service. - /// Model identifier - /// The ILoggerFactory used to create a logger for logging. If null, no logging will be performed. - public AzureOpenAITextToImageService( - string deploymentName, - OpenAIClient openAIClient, - string? modelId, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNull(openAIClient); - Verify.NotNullOrWhiteSpace(deploymentName); - - this._deploymentName = deploymentName; - - if (modelId is not null) - { - this.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - this.AddAttribute(DeploymentNameKey, deploymentName); - - this._logger = loggerFactory?.CreateLogger(typeof(AzureOpenAITextToImageService)) ?? NullLogger.Instance; - - this._client = openAIClient; - } - - /// - public async Task GenerateImageAsync( - string description, - int width, - int height, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - { - Verify.NotNull(description); - - var size = (width, height) switch - { - (1024, 1024) => ImageSize.Size1024x1024, - (1792, 1024) => ImageSize.Size1792x1024, - (1024, 1792) => ImageSize.Size1024x1792, - _ => throw new NotSupportedException("Dall-E 3 can only generate images of the following sizes 1024x1024, 1792x1024, or 1024x1792") - }; - - Response imageGenerations; - try - { - imageGenerations = await this._client.GetImageGenerationsAsync( - new ImageGenerationOptions - { - DeploymentName = this._deploymentName, - Prompt = description, - Size = size, - }, cancellationToken).ConfigureAwait(false); - } - catch (RequestFailedException e) - { - throw e.ToHttpOperationException(); - } - - if (!imageGenerations.HasValue) - { - throw new KernelException("The response does not contain an image result"); - } - - if (imageGenerations.Value.Data.Count == 0) - { - throw new KernelException("The response does not contain any image"); - } - - return imageGenerations.Value.Data[0].Url.AbsoluteUri; - } - - private static OpenAIClientOptions GetClientOptions(HttpClient? httpClient, string? apiVersion) => - ClientCore.GetOpenAIClientOptions(httpClient, apiVersion switch - { - // DALL-E 3 is supported in the latest API releases - _ => OpenAIClientOptions.ServiceVersion.V2024_02_15_Preview - }); - - internal void AddAttribute(string key, string? value) - { - if (!string.IsNullOrEmpty(value)) - { - this._attributes.Add(key, value); - } - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs deleted file mode 100644 index 335fe8cad5ee..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/OpenAITextToImageService.cs +++ /dev/null @@ -1,117 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Net.Http; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Services; -using Microsoft.SemanticKernel.TextToImage; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// OpenAI text to image service. -/// -[Experimental("SKEXP0010")] -public sealed class OpenAITextToImageService : ITextToImageService -{ - private readonly OpenAITextToImageClientCore _core; - - /// - /// OpenAI REST API endpoint - /// - private const string OpenAIEndpoint = "https://api.openai.com/v1/images/generations"; - - /// - /// Optional value for the OpenAI-Organization header. - /// - private readonly string? _organizationHeaderValue; - - /// - /// Value for the authorization header. - /// - private readonly string _authorizationHeaderValue; - - /// - /// The model to use for image generation. - /// - private readonly string? _modelId; - - /// - /// Initializes a new instance of the class. - /// - /// OpenAI API key, see https://platform.openai.com/account/api-keys - /// OpenAI organization id. This is usually optional unless your account belongs to multiple organizations. - /// The model to use for image generation. - /// Custom for HTTP requests. - /// The to use for logging. If null, no logging will be performed. - public OpenAITextToImageService( - string apiKey, - string? organization = null, - string? modelId = null, - HttpClient? httpClient = null, - ILoggerFactory? loggerFactory = null) - { - Verify.NotNullOrWhiteSpace(apiKey); - this._authorizationHeaderValue = $"Bearer {apiKey}"; - this._organizationHeaderValue = organization; - this._modelId = modelId; - - this._core = new(httpClient, loggerFactory?.CreateLogger(this.GetType())); - this._core.AddAttribute(OpenAIClientCore.OrganizationKey, organization); - if (modelId is not null) - { - this._core.AddAttribute(AIServiceExtensions.ModelIdKey, modelId); - } - - this._core.RequestCreated += (_, request) => - { - request.Headers.Add("Authorization", this._authorizationHeaderValue); - if (!string.IsNullOrEmpty(this._organizationHeaderValue)) - { - request.Headers.Add("OpenAI-Organization", this._organizationHeaderValue); - } - }; - } - - /// - public IReadOnlyDictionary Attributes => this._core.Attributes; - - /// - public Task GenerateImageAsync(string description, int width, int height, Kernel? kernel = null, CancellationToken cancellationToken = default) - { - Verify.NotNull(description); - if (width != height || (width != 256 && width != 512 && width != 1024)) - { - throw new ArgumentOutOfRangeException(nameof(width), width, "OpenAI can generate only square images of size 256x256, 512x512, or 1024x1024."); - } - - return this.GenerateImageAsync(this._modelId, description, width, height, "url", x => x.Url, cancellationToken); - } - - private async Task GenerateImageAsync( - string? model, - string description, - int width, int height, - string format, Func extractResponse, - CancellationToken cancellationToken) - { - Verify.NotNull(extractResponse); - - var requestBody = JsonSerializer.Serialize(new TextToImageRequest - { - Model = model, - Prompt = description, - Size = $"{width}x{height}", - Count = 1, - Format = format, - }); - - var list = await this._core.ExecuteImageGenerationRequestAsync(OpenAIEndpoint, requestBody, extractResponse!, cancellationToken).ConfigureAwait(false); - return list[0]; - } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageRequest.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageRequest.cs deleted file mode 100644 index 70b5ac5418ee..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageRequest.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Text to image request -/// -internal sealed class TextToImageRequest -{ - /// - /// Model to use for image generation - /// - [JsonPropertyName("model")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Model { get; set; } - - /// - /// Image prompt - /// - [JsonPropertyName("prompt")] - public string Prompt { get; set; } = string.Empty; - - /// - /// Image size - /// - [JsonPropertyName("size")] - public string Size { get; set; } = "256x256"; - - /// - /// How many images to generate - /// - [JsonPropertyName("n")] - public int Count { get; set; } = 1; - - /// - /// Image format, "url" or "b64_json" - /// - [JsonPropertyName("response_format")] - public string Format { get; set; } = "url"; -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageResponse.cs b/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageResponse.cs deleted file mode 100644 index cba10ba14331..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/TextToImage/TextToImageResponse.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Text.Json.Serialization; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// -/// Text to image response -/// -internal sealed class TextToImageResponse -{ - /// - /// OpenAI Image response - /// - public sealed class Image - { - /// - /// URL to the image created - /// - [JsonPropertyName("url")] - [SuppressMessage("Design", "CA1056:URI return values should not be strings", Justification = "Using the original value")] - public string Url { get; set; } = string.Empty; - - /// - /// Image content in base64 format - /// - [JsonPropertyName("b64_json")] - public string AsBase64 { get; set; } = string.Empty; - } - - /// - /// List of possible images - /// - [JsonPropertyName("data")] - public IList Images { get; set; } = []; - - /// - /// Creation time - /// - [JsonPropertyName("created")] - public int CreatedTime { get; set; } -} diff --git a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs deleted file mode 100644 index 7a5490c736ea..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Text.Json; -using Azure.AI.OpenAI; - -namespace Microsoft.SemanticKernel.Connectors.OpenAI; - -/// Represents a behavior for OpenAI tool calls. -public abstract class ToolCallBehavior -{ - // NOTE: Right now, the only tools that are available are for function calling. In the future, - // this class can be extended to support additional kinds of tools, including composite ones: - // the OpenAIPromptExecutionSettings has a single ToolCallBehavior property, but we could - // expose a `public static ToolCallBehavior Composite(params ToolCallBehavior[] behaviors)` - // or the like to allow multiple distinct tools to be provided, should that be appropriate. - // We can also consider additional forms of tools, such as ones that dynamically examine - // the Kernel, KernelArguments, etc., and dynamically contribute tools to the ChatCompletionsOptions. - - /// - /// The default maximum number of tool-call auto-invokes that can be made in a single request. - /// - /// - /// After this number of iterations as part of a single user request is reached, auto-invocation - /// will be disabled (e.g. will behave like )). - /// This is a safeguard against possible runaway execution if the model routinely re-requests - /// the same function over and over. It is currently hardcoded, but in the future it could - /// be made configurable by the developer. Other configuration is also possible in the future, - /// such as a delegate on the instance that can be invoked upon function call failure (e.g. failure - /// to find the requested function, failure to invoke the function, etc.), with behaviors for - /// what to do in such a case, e.g. respond to the model telling it to try again. With parallel tool call - /// support, where the model can request multiple tools in a single response, it is significantly - /// less likely that this limit is reached, as most of the time only a single request is needed. - /// - private const int DefaultMaximumAutoInvokeAttempts = 128; - - /// - /// Gets an instance that will provide all of the 's plugins' function information. - /// Function call requests from the model will be propagated back to the caller. - /// - /// - /// If no is available, no function information will be provided to the model. - /// - public static ToolCallBehavior EnableKernelFunctions { get; } = new KernelFunctions(autoInvoke: false); - - /// - /// Gets an instance that will both provide all of the 's plugins' function information - /// to the model and attempt to automatically handle any function call requests. - /// - /// - /// When successful, tool call requests from the model become an implementation detail, with the service - /// handling invoking any requested functions and supplying the results back to the model. - /// If no is available, no function information will be provided to the model. - /// - public static ToolCallBehavior AutoInvokeKernelFunctions { get; } = new KernelFunctions(autoInvoke: true); - - /// Gets an instance that will provide the specified list of functions to the model. - /// The functions that should be made available to the model. - /// true to attempt to automatically handle function call requests; otherwise, false. - /// - /// The that may be set into - /// to indicate that the specified functions should be made available to the model. - /// - public static ToolCallBehavior EnableFunctions(IEnumerable functions, bool autoInvoke = false) - { - Verify.NotNull(functions); - return new EnabledFunctions(functions, autoInvoke); - } - - /// Gets an instance that will request the model to use the specified function. - /// The function the model should request to use. - /// true to attempt to automatically handle function call requests; otherwise, false. - /// - /// The that may be set into - /// to indicate that the specified function should be requested by the model. - /// - public static ToolCallBehavior RequireFunction(OpenAIFunction function, bool autoInvoke = false) - { - Verify.NotNull(function); - return new RequiredFunction(function, autoInvoke); - } - - /// Initializes the instance; prevents external instantiation. - private ToolCallBehavior(bool autoInvoke) - { - this.MaximumAutoInvokeAttempts = autoInvoke ? DefaultMaximumAutoInvokeAttempts : 0; - } - - /// - /// Options to control tool call result serialization behavior. - /// - [Obsolete("This property is deprecated in favor of Kernel.SerializerOptions that will be introduced in one of the following releases.")] - [EditorBrowsable(EditorBrowsableState.Never)] - public virtual JsonSerializerOptions? ToolCallResultSerializerOptions { get; set; } - - /// Gets how many requests are part of a single interaction should include this tool in the request. - /// - /// This should be greater than or equal to . It defaults to . - /// Once this limit is reached, the tools will no longer be included in subsequent retries as part of the operation, e.g. - /// if this is 1, the first request will include the tools, but the subsequent response sending back the tool's result - /// will not include the tools for further use. - /// - internal virtual int MaximumUseAttempts => int.MaxValue; - - /// Gets how many tool call request/response roundtrips are supported with auto-invocation. - /// - /// To disable auto invocation, this can be set to 0. - /// - internal int MaximumAutoInvokeAttempts { get; } - - /// - /// Gets whether validation against a specified list is required before allowing the model to request a function from the kernel. - /// - /// true if it's ok to invoke any kernel function requested by the model if it's found; false if a request needs to be validated against an allow list. - internal virtual bool AllowAnyRequestedKernelFunction => false; - - /// Configures the with any tools this provides. - /// The used for the operation. This can be queried to determine what tools to provide into the . - /// The destination to configure. - internal abstract void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options); - - /// - /// Represents a that will provide to the model all available functions from a - /// provided by the client. Setting this will have no effect if no is provided. - /// - internal sealed class KernelFunctions : ToolCallBehavior - { - internal KernelFunctions(bool autoInvoke) : base(autoInvoke) { } - - public override string ToString() => $"{nameof(KernelFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0})"; - - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) - { - // If no kernel is provided, we don't have any tools to provide. - if (kernel is not null) - { - // Provide all functions from the kernel. - IList functions = kernel.Plugins.GetFunctionsMetadata(); - if (functions.Count > 0) - { - options.ToolChoice = ChatCompletionsToolChoice.Auto; - for (int i = 0; i < functions.Count; i++) - { - options.Tools.Add(new ChatCompletionsFunctionToolDefinition(functions[i].ToOpenAIFunction().ToFunctionDefinition())); - } - } - } - } - - internal override bool AllowAnyRequestedKernelFunction => true; - } - - /// - /// Represents a that provides a specified list of functions to the model. - /// - internal sealed class EnabledFunctions : ToolCallBehavior - { - private readonly OpenAIFunction[] _openAIFunctions; - private readonly ChatCompletionsFunctionToolDefinition[] _functions; - - public EnabledFunctions(IEnumerable functions, bool autoInvoke) : base(autoInvoke) - { - this._openAIFunctions = functions.ToArray(); - - var defs = new ChatCompletionsFunctionToolDefinition[this._openAIFunctions.Length]; - for (int i = 0; i < defs.Length; i++) - { - defs[i] = new ChatCompletionsFunctionToolDefinition(this._openAIFunctions[i].ToFunctionDefinition()); - } - this._functions = defs; - } - - public override string ToString() => $"{nameof(EnabledFunctions)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {string.Join(", ", this._functions.Select(f => f.Name))}"; - - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) - { - OpenAIFunction[] openAIFunctions = this._openAIFunctions; - ChatCompletionsFunctionToolDefinition[] functions = this._functions; - Debug.Assert(openAIFunctions.Length == functions.Length); - - if (openAIFunctions.Length > 0) - { - bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) - { - throw new KernelException($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided."); - } - - options.ToolChoice = ChatCompletionsToolChoice.Auto; - for (int i = 0; i < openAIFunctions.Length; i++) - { - // Make sure that if auto-invocation is specified, every enabled function can be found in the kernel. - if (autoInvoke) - { - Debug.Assert(kernel is not null); - OpenAIFunction f = openAIFunctions[i]; - if (!kernel!.Plugins.TryGetFunction(f.PluginName, f.FunctionName, out _)) - { - throw new KernelException($"The specified {nameof(EnabledFunctions)} function {f.FullyQualifiedName} is not available in the kernel."); - } - } - - // Add the function. - options.Tools.Add(functions[i]); - } - } - } - } - - /// Represents a that requests the model use a specific function. - internal sealed class RequiredFunction : ToolCallBehavior - { - private readonly OpenAIFunction _function; - private readonly ChatCompletionsFunctionToolDefinition _tool; - private readonly ChatCompletionsToolChoice _choice; - - public RequiredFunction(OpenAIFunction function, bool autoInvoke) : base(autoInvoke) - { - this._function = function; - this._tool = new ChatCompletionsFunctionToolDefinition(function.ToFunctionDefinition()); - this._choice = new ChatCompletionsToolChoice(this._tool); - } - - public override string ToString() => $"{nameof(RequiredFunction)}(autoInvoke:{this.MaximumAutoInvokeAttempts != 0}): {this._tool.Name}"; - - internal override void ConfigureOptions(Kernel? kernel, ChatCompletionsOptions options) - { - bool autoInvoke = base.MaximumAutoInvokeAttempts > 0; - - // If auto-invocation is specified, we need a kernel to be able to invoke the functions. - // Lack of a kernel is fatal: we don't want to tell the model we can handle the functions - // and then fail to do so, so we fail before we get to that point. This is an error - // on the consumers behalf: if they specify auto-invocation with any functions, they must - // specify the kernel and the kernel must contain those functions. - if (autoInvoke && kernel is null) - { - throw new KernelException($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided."); - } - - // Make sure that if auto-invocation is specified, the required function can be found in the kernel. - if (autoInvoke && !kernel!.Plugins.TryGetFunction(this._function.PluginName, this._function.FunctionName, out _)) - { - throw new KernelException($"The specified {nameof(RequiredFunction)} function {this._function.FullyQualifiedName} is not available in the kernel."); - } - - options.ToolChoice = this._choice; - options.Tools.Add(this._tool); - } - - /// Gets how many requests are part of a single interaction should include this tool in the request. - /// - /// Unlike and , this must use 1 as the maximum - /// use attempts. Otherwise, every call back to the model _requires_ it to invoke the function (as opposed - /// to allows it), which means we end up doing the same work over and over and over until the maximum is reached. - /// Thus for "requires", we must send the tool information only once. - /// - internal override int MaximumUseAttempts => 1; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj index a4b7bd6ace44..17ac2e2510a9 100644 --- a/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.UnitTests/Connectors.UnitTests.csproj @@ -12,9 +12,9 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -29,31 +29,23 @@ - - + + + - - - - - - - - - - - - - - - - - Always - + + + + + + + + + + + diff --git a/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs b/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs deleted file mode 100644 index d7e81f129c9c..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/MultipleHttpMessageHandlerStub.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -namespace SemanticKernel.Connectors.UnitTests; - -internal sealed class MultipleHttpMessageHandlerStub : DelegatingHandler -{ - private int _callIteration = 0; - - public List RequestHeaders { get; private set; } - - public List ContentHeaders { get; private set; } - - public List RequestContents { get; private set; } - - public List RequestUris { get; private set; } - - public List Methods { get; private set; } - - public List ResponsesToReturn { get; set; } - - public MultipleHttpMessageHandlerStub() - { - this.RequestHeaders = []; - this.ContentHeaders = []; - this.RequestContents = []; - this.RequestUris = []; - this.Methods = []; - this.ResponsesToReturn = []; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - this._callIteration++; - - this.Methods.Add(request.Method); - this.RequestUris.Add(request.RequestUri); - this.RequestHeaders.Add(request.Headers); - this.ContentHeaders.Add(request.Content?.Headers); - - var content = request.Content is null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken); - - this.RequestContents.Add(content); - - return await Task.FromResult(this.ResponsesToReturn[this._callIteration - 1]); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs deleted file mode 100644 index 39bc2803fe19..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AIServicesOpenAIExtensionsTests.cs +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.TextGeneration; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -/// -/// Unit tests of . -/// -public class AIServicesOpenAIExtensionsTests -{ - [Fact] - public void ItSucceedsWhenAddingDifferentServiceTypeWithSameId() - { - Kernel targetKernel = Kernel.CreateBuilder() - .AddAzureOpenAITextGeneration("depl", "https://url", "key", "azure") - .AddAzureOpenAITextEmbeddingGeneration("depl2", "https://url", "key", "azure") - .Build(); - - Assert.NotNull(targetKernel.GetRequiredService("azure")); - Assert.NotNull(targetKernel.GetRequiredService("azure")); - } - - [Fact] - public void ItTellsIfAServiceIsAvailable() - { - Kernel targetKernel = Kernel.CreateBuilder() - .AddAzureOpenAITextGeneration("depl", "https://url", "key", serviceId: "azure") - .AddOpenAITextGeneration("model", "apikey", serviceId: "oai") - .AddAzureOpenAITextEmbeddingGeneration("depl2", "https://url2", "key", serviceId: "azure") - .AddOpenAITextEmbeddingGeneration("model2", "apikey2", serviceId: "oai2") - .Build(); - - // Assert - Assert.NotNull(targetKernel.GetRequiredService("azure")); - Assert.NotNull(targetKernel.GetRequiredService("oai")); - Assert.NotNull(targetKernel.GetRequiredService("azure")); - Assert.NotNull(targetKernel.GetRequiredService("oai")); - } - - [Fact] - public void ItCanOverwriteServices() - { - // Arrange - // Act - Assert no exception occurs - var builder = Kernel.CreateBuilder(); - - builder.Services.AddAzureOpenAITextGeneration("depl", "https://localhost", "key", serviceId: "one"); - builder.Services.AddAzureOpenAITextGeneration("depl", "https://localhost", "key", serviceId: "one"); - - builder.Services.AddOpenAITextGeneration("model", "key", serviceId: "one"); - builder.Services.AddOpenAITextGeneration("model", "key", serviceId: "one"); - - builder.Services.AddAzureOpenAITextEmbeddingGeneration("dep", "https://localhost", "key", serviceId: "one"); - builder.Services.AddAzureOpenAITextEmbeddingGeneration("dep", "https://localhost", "key", serviceId: "one"); - - builder.Services.AddOpenAITextEmbeddingGeneration("model", "key", serviceId: "one"); - builder.Services.AddOpenAITextEmbeddingGeneration("model", "key", serviceId: "one"); - - builder.Services.AddAzureOpenAIChatCompletion("dep", "https://localhost", "key", serviceId: "one"); - builder.Services.AddAzureOpenAIChatCompletion("dep", "https://localhost", "key", serviceId: "one"); - - builder.Services.AddOpenAIChatCompletion("model", "key", serviceId: "one"); - builder.Services.AddOpenAIChatCompletion("model", "key", serviceId: "one"); - - builder.Services.AddOpenAITextToImage("model", "key", serviceId: "one"); - builder.Services.AddOpenAITextToImage("model", "key", serviceId: "one"); - - builder.Services.AddSingleton(new OpenAITextGenerationService("model", "key")); - builder.Services.AddSingleton(new OpenAITextGenerationService("model", "key")); - - builder.Services.AddSingleton((_) => new OpenAITextGenerationService("model", "key")); - builder.Services.AddSingleton((_) => new OpenAITextGenerationService("model", "key")); - - builder.Services.AddKeyedSingleton("one", new OpenAITextGenerationService("model", "key")); - builder.Services.AddKeyedSingleton("one", new OpenAITextGenerationService("model", "key")); - - builder.Services.AddKeyedSingleton("one", (_, _) => new OpenAITextGenerationService("model", "key")); - builder.Services.AddKeyedSingleton("one", (_, _) => new OpenAITextGenerationService("model", "key")); - - builder.Build(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/AzureOpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/AzureOpenAIAudioToTextServiceTests.cs deleted file mode 100644 index 6100c434c878..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/AzureOpenAIAudioToTextServiceTests.cs +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIAudioToTextServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAIAudioToTextServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAIAudioToTextService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIAudioToTextService("deployment-name", "https://endpoint", "api-key", "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAIAudioToTextService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIAudioToTextService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new AzureOpenAIAudioToTextService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIAudioToTextService("deployment", client, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [MemberData(nameof(ExecutionSettings))] - public async Task GetTextContentWithInvalidSettingsThrowsExceptionAsync(OpenAIAudioToTextExecutionSettings? settings, Type expectedExceptionType) - { - // Arrange - var service = new AzureOpenAIAudioToTextService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("Test audio-to-text response") - }; - - // Act - var exception = await Record.ExceptionAsync(() => service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), settings)); - - // Assert - Assert.NotNull(exception); - Assert.IsType(expectedExceptionType, exception); - } - - [Fact] - public async Task GetTextContentByDefaultWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIAudioToTextService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("Test audio-to-text response") - }; - - // Act - var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("file.mp3")); - - // Assert - Assert.NotNull(result); - Assert.Equal("Test audio-to-text response", result[0].Text); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - public static TheoryData ExecutionSettings => new() - { - { new OpenAIAudioToTextExecutionSettings(""), typeof(ArgumentException) }, - { new OpenAIAudioToTextExecutionSettings("file"), typeof(ArgumentException) } - }; -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextExecutionSettingsTests.cs deleted file mode 100644 index 96dd9c1a290b..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextExecutionSettingsTests.cs +++ /dev/null @@ -1,122 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIAudioToTextExecutionSettingsTests -{ - [Fact] - public void ItReturnsDefaultSettingsWhenSettingsAreNull() - { - Assert.NotNull(OpenAIAudioToTextExecutionSettings.FromExecutionSettings(null)); - } - - [Fact] - public void ItReturnsValidOpenAIAudioToTextExecutionSettings() - { - // Arrange - var audioToTextSettings = new OpenAIAudioToTextExecutionSettings("file.mp3") - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "text", - Temperature = 0.2f - }; - - // Act - var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(audioToTextSettings); - - // Assert - Assert.Same(audioToTextSettings, settings); - } - - [Fact] - public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() - { - // Arrange - var json = """ - { - "model_id": "model_id", - "language": "en", - "filename": "file.mp3", - "prompt": "prompt", - "response_format": "text", - "temperature": 0.2 - } - """; - - var executionSettings = JsonSerializer.Deserialize(json); - - // Act - var settings = OpenAIAudioToTextExecutionSettings.FromExecutionSettings(executionSettings); - - // Assert - Assert.NotNull(settings); - Assert.Equal("model_id", settings.ModelId); - Assert.Equal("en", settings.Language); - Assert.Equal("file.mp3", settings.Filename); - Assert.Equal("prompt", settings.Prompt); - Assert.Equal("text", settings.ResponseFormat); - Assert.Equal(0.2f, settings.Temperature); - } - - [Fact] - public void ItClonesAllProperties() - { - var settings = new OpenAIAudioToTextExecutionSettings() - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "text", - Temperature = 0.2f, - Filename = "something.mp3", - }; - - var clone = (OpenAIAudioToTextExecutionSettings)settings.Clone(); - Assert.NotSame(settings, clone); - - Assert.Equal("model_id", clone.ModelId); - Assert.Equal("en", clone.Language); - Assert.Equal("prompt", clone.Prompt); - Assert.Equal("text", clone.ResponseFormat); - Assert.Equal(0.2f, clone.Temperature); - Assert.Equal("something.mp3", clone.Filename); - } - - [Fact] - public void ItFreezesAndPreventsMutation() - { - var settings = new OpenAIAudioToTextExecutionSettings() - { - ModelId = "model_id", - Language = "en", - Prompt = "prompt", - ResponseFormat = "text", - Temperature = 0.2f, - Filename = "something.mp3", - }; - - settings.Freeze(); - Assert.True(settings.IsFrozen); - - Assert.Throws(() => settings.ModelId = "new_model"); - Assert.Throws(() => settings.Language = "some_format"); - Assert.Throws(() => settings.Prompt = "prompt"); - Assert.Throws(() => settings.ResponseFormat = "something"); - Assert.Throws(() => settings.Temperature = 0.2f); - Assert.Throws(() => settings.Filename = "something"); - - settings.Freeze(); // idempotent - Assert.True(settings.IsFrozen); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextServiceTests.cs deleted file mode 100644 index 40959c7c67ed..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AudioToText/OpenAIAudioToTextServiceTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AudioToText; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIAudioToTextServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAIAudioToTextServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAIAudioToTextService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIAudioToTextService("model-id", "api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new OpenAIAudioToTextService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIAudioToTextService("model-id", client); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GetTextContentByDefaultWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAIAudioToTextService("model-id", "api-key", "organization", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent("Test audio-to-text response") - }; - - // Act - var result = await service.GetTextContentsAsync(new AudioContent(new BinaryData("data"), mimeType: null), new OpenAIAudioToTextExecutionSettings("file.mp3")); - - // Assert - Assert.NotNull(result); - Assert.Equal("Test audio-to-text response", result[0].Text); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs deleted file mode 100644 index f3dd1850d56e..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataChatMessageContentTests.cs +++ /dev/null @@ -1,120 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections; -using System.Collections.Generic; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIWithDataChatMessageContentTests -{ - [Fact] - public void ConstructorThrowsExceptionWhenAssistantMessageIsNotProvided() - { - // Arrange - var choice = new ChatWithDataChoice(); - - // Act & Assert - var exception = Assert.Throws(() => new AzureOpenAIWithDataChatMessageContent(choice, "model-id")); - - Assert.Contains("Chat is not valid", exception.Message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public void ConstructorReturnsInstanceWithNullToolContent() - { - // Arrange - var choice = new ChatWithDataChoice { Messages = [new() { Content = "Assistant content", Role = "assistant" }] }; - - // Act - var content = new AzureOpenAIWithDataChatMessageContent(choice, "model-id"); - - // Assert - Assert.Equal("Assistant content", content.Content); - Assert.Equal(AuthorRole.Assistant, content.Role); - - Assert.Null(content.ToolContent); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorReturnsInstanceWithNonNullToolContent(bool includeMetadata) - { - // Arrange - var choice = new ChatWithDataChoice - { - Messages = [ - new() { Content = "Assistant content", Role = "assistant" }, - new() { Content = "Tool content", Role = "tool" }] - }; - - // Act - var content = includeMetadata ? - new AzureOpenAIWithDataChatMessageContent(choice, "model-id", new Dictionary()) : - new AzureOpenAIWithDataChatMessageContent(choice, "model-id"); - - // Assert - Assert.Equal("Assistant content", content.Content); - Assert.Equal("Tool content", content.ToolContent); - Assert.Equal(AuthorRole.Assistant, content.Role); - - Assert.NotNull(content.Metadata); - Assert.Equal("Tool content", content.Metadata["ToolContent"]); - } - - [Fact] - public void ConstructorCloneReadOnlyMetadataDictionary() - { - // Arrange - var choice = new ChatWithDataChoice - { - Messages = [new() { Content = "Assistant content", Role = "assistant" }] - }; - - var metadata = new ReadOnlyInternalDictionary(new Dictionary() { ["Extra"] = "Data" }); - - // Act - var content = new AzureOpenAIWithDataChatMessageContent(choice, "model-id", metadata); - - // Assert - Assert.Equal("Assistant content", content.Content); - Assert.Equal(AuthorRole.Assistant, content.Role); - - Assert.NotNull(content.Metadata); - Assert.Equal("Data", content.Metadata["Extra"]); - } - - private sealed class ReadOnlyInternalDictionary : IReadOnlyDictionary - { - public ReadOnlyInternalDictionary(IDictionary initializingData) - { - this._internalDictionary = new Dictionary(initializingData); - } - private readonly Dictionary _internalDictionary; - - public object? this[string key] => this._internalDictionary[key]; - - public IEnumerable Keys => this._internalDictionary.Keys; - - public IEnumerable Values => this._internalDictionary.Values; - - public int Count => this._internalDictionary.Count; - - public bool ContainsKey(string key) => this._internalDictionary.ContainsKey(key); - - public IEnumerator> GetEnumerator() => this._internalDictionary.GetEnumerator(); - - public bool TryGetValue(string key, out object? value) => this._internalDictionary.TryGetValue(key, out value); - - IEnumerator IEnumerable.GetEnumerator() => this._internalDictionary.GetEnumerator(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs deleted file mode 100644 index 45597c616270..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/AzureOpenAIWithDataStreamingChatMessageContentTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAIWithDataStreamingChatMessageContentTests -{ - [Theory] - [MemberData(nameof(ValidChoices))] - public void ConstructorWithValidChoiceSetsNonEmptyContent(object choice, string expectedContent) - { - // Arrange - var streamingChoice = choice as ChatWithDataStreamingChoice; - - // Act - var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id"); - - // Assert - Assert.Equal(expectedContent, content.Content); - } - - [Theory] - [MemberData(nameof(InvalidChoices))] - public void ConstructorWithInvalidChoiceSetsNullContent(object choice) - { - // Arrange - var streamingChoice = choice as ChatWithDataStreamingChoice; - - // Act - var content = new AzureOpenAIWithDataStreamingChatMessageContent(streamingChoice!, 0, "model-id"); - - // Assert - Assert.Null(content.Content); - } - - public static IEnumerable ValidChoices - { - get - { - yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 1" } }] }, "Content 1" }; - yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content 2", Role = "Assistant" } }] }, "Content 2" }; - } - } - - public static IEnumerable InvalidChoices - { - get - { - yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { EndTurn = true }] } }; - yield return new object[] { new ChatWithDataStreamingChoice { Messages = [new() { Delta = new() { Content = "Content", Role = "tool" } }] } }; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs deleted file mode 100644 index cf2d32d3b52e..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIChatMessageContentTests.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIChatMessageContentTests -{ - [Fact] - public void ConstructorsWorkCorrectly() - { - // Arrange - List toolCalls = [new FakeChatCompletionsToolCall("id")]; - - // Act - var content1 = new OpenAIChatMessageContent(new ChatRole("user"), "content1", "model-id1", toolCalls) { AuthorName = "Fred" }; - var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls); - - // Assert - this.AssertChatMessageContent(AuthorRole.User, "content1", "model-id1", toolCalls, content1, "Fred"); - this.AssertChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, content2); - } - - [Fact] - public void GetOpenAIFunctionToolCallsReturnsCorrectList() - { - // Arrange - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; - - var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", toolCalls); - var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content", "model-id", []); - - // Act - var actualToolCalls1 = content1.GetOpenAIFunctionToolCalls(); - var actualToolCalls2 = content2.GetOpenAIFunctionToolCalls(); - - // Assert - Assert.Equal(2, actualToolCalls1.Count); - Assert.Equal("id1", actualToolCalls1[0].Id); - Assert.Equal("id2", actualToolCalls1[1].Id); - - Assert.Empty(actualToolCalls2); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void MetadataIsInitializedCorrectly(bool readOnlyMetadata) - { - // Arrange - IReadOnlyDictionary metadata = readOnlyMetadata ? - new CustomReadOnlyDictionary(new Dictionary { { "key", "value" } }) : - new Dictionary { { "key", "value" } }; - - List toolCalls = [ - new ChatCompletionsFunctionToolCall("id1", "name", string.Empty), - new ChatCompletionsFunctionToolCall("id2", "name", string.Empty), - new FakeChatCompletionsToolCall("id3"), - new FakeChatCompletionsToolCall("id4")]; - - // Act - var content1 = new OpenAIChatMessageContent(AuthorRole.User, "content1", "model-id1", [], metadata); - var content2 = new OpenAIChatMessageContent(AuthorRole.User, "content2", "model-id2", toolCalls, metadata); - - // Assert - Assert.NotNull(content1.Metadata); - Assert.Single(content1.Metadata); - - Assert.NotNull(content2.Metadata); - Assert.Equal(2, content2.Metadata.Count); - Assert.Equal("value", content2.Metadata["key"]); - - Assert.IsType>(content2.Metadata["ChatResponseMessage.FunctionToolCalls"]); - - var actualToolCalls = content2.Metadata["ChatResponseMessage.FunctionToolCalls"] as List; - Assert.NotNull(actualToolCalls); - - Assert.Equal(2, actualToolCalls.Count); - Assert.Equal("id1", actualToolCalls[0].Id); - Assert.Equal("id2", actualToolCalls[1].Id); - } - - private void AssertChatMessageContent( - AuthorRole expectedRole, - string expectedContent, - string expectedModelId, - IReadOnlyList expectedToolCalls, - OpenAIChatMessageContent actualContent, - string? expectedName = null) - { - Assert.Equal(expectedRole, actualContent.Role); - Assert.Equal(expectedContent, actualContent.Content); - Assert.Equal(expectedName, actualContent.AuthorName); - Assert.Equal(expectedModelId, actualContent.ModelId); - Assert.Same(expectedToolCalls, actualContent.ToolCalls); - } - - private sealed class FakeChatCompletionsToolCall(string id) : ChatCompletionsToolCall(id) - { } - - private sealed class CustomReadOnlyDictionary(IDictionary dictionary) : IReadOnlyDictionary // explicitly not implementing IDictionary<> - { - public TValue this[TKey key] => dictionary[key]; - public IEnumerable Keys => dictionary.Keys; - public IEnumerable Values => dictionary.Values; - public int Count => dictionary.Count; - public bool ContainsKey(TKey key) => dictionary.ContainsKey(key); - public IEnumerator> GetEnumerator() => dictionary.GetEnumerator(); - public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) => dictionary.TryGetValue(key, out value); - IEnumerator IEnumerable.GetEnumerator() => dictionary.GetEnumerator(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs deleted file mode 100644 index 3b4d8b4ca0d4..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIFunctionToolCallTests.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Text; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIFunctionToolCallTests -{ - [Theory] - [InlineData("MyFunction", "MyFunction")] - [InlineData("MyPlugin_MyFunction", "MyPlugin_MyFunction")] - public void FullyQualifiedNameReturnsValidName(string toolCallName, string expectedName) - { - // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", toolCallName, string.Empty); - var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); - - // Act & Assert - Assert.Equal(expectedName, openAIFunctionToolCall.FullyQualifiedName); - Assert.Same(openAIFunctionToolCall.FullyQualifiedName, openAIFunctionToolCall.FullyQualifiedName); - } - - [Fact] - public void ToStringReturnsCorrectValue() - { - // Arrange - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n}"); - var openAIFunctionToolCall = new OpenAIFunctionToolCall(toolCall); - - // Act & Assert - Assert.Equal("MyPlugin_MyFunction(location:San Diego, max_price:300)", openAIFunctionToolCall.ToString()); - } - - [Fact] - public void ConvertToolCallUpdatesWithEmptyIndexesReturnsEmptyToolCalls() - { - // Arrange - var toolCallIdsByIndex = new Dictionary(); - var functionNamesByIndex = new Dictionary(); - var functionArgumentBuildersByIndex = new Dictionary(); - - // Act - var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( - ref toolCallIdsByIndex, - ref functionNamesByIndex, - ref functionArgumentBuildersByIndex); - - // Assert - Assert.Empty(toolCalls); - } - - [Fact] - public void ConvertToolCallUpdatesWithNotEmptyIndexesReturnsNotEmptyToolCalls() - { - // Arrange - var toolCallIdsByIndex = new Dictionary { { 3, "test-id" } }; - var functionNamesByIndex = new Dictionary { { 3, "test-function" } }; - var functionArgumentBuildersByIndex = new Dictionary { { 3, new("test-argument") } }; - - // Act - var toolCalls = OpenAIFunctionToolCall.ConvertToolCallUpdatesToChatCompletionsFunctionToolCalls( - ref toolCallIdsByIndex, - ref functionNamesByIndex, - ref functionArgumentBuildersByIndex); - - // Assert - Assert.Single(toolCalls); - - var toolCall = toolCalls[0]; - - Assert.Equal("test-id", toolCall.Id); - Assert.Equal("test-function", toolCall.Name); - Assert.Equal("test-argument", toolCall.Arguments); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs deleted file mode 100644 index c3ee67df7515..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIPluginCollectionExtensionsTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIPluginCollectionExtensionsTests -{ - [Fact] - public void TryGetFunctionAndArgumentsWithNonExistingFunctionReturnsFalse() - { - // Arrange - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin"); - var plugins = new KernelPluginCollection([plugin]); - - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin_MyFunction", string.Empty); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.False(result); - Assert.Null(actualFunction); - Assert.Null(actualArguments); - } - - [Fact] - public void TryGetFunctionAndArgumentsWithoutArgumentsReturnsTrue() - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - - var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", string.Empty); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.True(result); - Assert.Equal(function.Name, actualFunction?.Name); - Assert.Null(actualArguments); - } - - [Fact] - public void TryGetFunctionAndArgumentsWithArgumentsReturnsTrue() - { - // Arrange - var function = KernelFunctionFactory.CreateFromMethod(() => "Result", "MyFunction"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - - var plugins = new KernelPluginCollection([plugin]); - var toolCall = new ChatCompletionsFunctionToolCall("id", "MyPlugin-MyFunction", "{\n \"location\": \"San Diego\",\n \"max_price\": 300\n,\n \"null_argument\": null\n}"); - - // Act - var result = plugins.TryGetFunctionAndArguments(toolCall, out var actualFunction, out var actualArguments); - - // Assert - Assert.True(result); - Assert.Equal(function.Name, actualFunction?.Name); - - Assert.NotNull(actualArguments); - - Assert.Equal("San Diego", actualArguments["location"]); - Assert.Equal("300", actualArguments["max_price"]); - - Assert.Null(actualArguments["null_argument"]); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs deleted file mode 100644 index fd0a830cc2d9..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/OpenAIStreamingTextContentTests.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Text; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIStreamingTextContentTests -{ - [Fact] - public void ToByteArrayWorksCorrectly() - { - // Arrange - var expectedBytes = Encoding.UTF8.GetBytes("content"); - var content = new OpenAIStreamingTextContent("content", 0, "model-id"); - - // Act - var actualBytes = content.ToByteArray(); - - // Assert - Assert.Equal(expectedBytes, actualBytes); - } - - [Theory] - [InlineData(null, "")] - [InlineData("content", "content")] - public void ToStringWorksCorrectly(string? content, string expectedString) - { - // Arrange - var textContent = new OpenAIStreamingTextContent(content!, 0, "model-id"); - - // Act - var actualString = textContent.ToString(); - - // Assert - Assert.Equal(expectedString, actualString); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs deleted file mode 100644 index 54a183eca330..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/AzureSdk/RequestFailedExceptionExtensionsTests.cs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using Azure; -using Azure.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.AzureSdk; - -/// -/// Unit tests for class. -/// -public sealed class RequestFailedExceptionExtensionsTests -{ - [Theory] - [InlineData(0, null)] - [InlineData(500, HttpStatusCode.InternalServerError)] - public void ToHttpOperationExceptionWithStatusReturnsValidException(int responseStatus, HttpStatusCode? httpStatusCode) - { - // Arrange - var exception = new RequestFailedException(responseStatus, "Error Message"); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(httpStatusCode, actualException.StatusCode); - Assert.Equal("Error Message", actualException.Message); - Assert.Same(exception, actualException.InnerException); - } - - [Fact] - public void ToHttpOperationExceptionWithContentReturnsValidException() - { - // Arrange - using var response = new FakeResponse("Response Content", 500); - var exception = new RequestFailedException(response); - - // Act - var actualException = exception.ToHttpOperationException(); - - // Assert - Assert.IsType(actualException); - Assert.Equal(HttpStatusCode.InternalServerError, actualException.StatusCode); - Assert.Equal("Response Content", actualException.ResponseContent); - Assert.Same(exception, actualException.InnerException); - } - - #region private - - private sealed class FakeResponse(string responseContent, int status) : Response - { - private readonly string _responseContent = responseContent; - private readonly IEnumerable _headers = []; - - public override BinaryData Content => BinaryData.FromString(this._responseContent); - public override int Status { get; } = status; - public override string ReasonPhrase => "Reason Phrase"; - public override Stream? ContentStream { get => null; set => throw new NotImplementedException(); } - public override string ClientRequestId { get => "Client Request Id"; set => throw new NotImplementedException(); } - - public override void Dispose() { } - protected override bool ContainsHeader(string name) => throw new NotImplementedException(); - protected override IEnumerable EnumerateHeaders() => this._headers; -#pragma warning disable CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - protected override bool TryGetHeader(string name, out string? value) => throw new NotImplementedException(); - protected override bool TryGetHeaderValues(string name, out IEnumerable? values) => throw new NotImplementedException(); -#pragma warning restore CS8765 // Nullability of type of parameter doesn't match overridden member (possibly because of nullability attributes). - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs deleted file mode 100644 index 22be8458c2cc..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/AzureOpenAIChatCompletionServiceTests.cs +++ /dev/null @@ -1,959 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletion; - -/// -/// Unit tests for -/// -public sealed class AzureOpenAIChatCompletionServiceTests : IDisposable -{ - private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAIChatCompletionServiceTests() - { - this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - - var mockLogger = new Mock(); - - mockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(l => l.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIChatCompletionService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new AzureOpenAIChatCompletionService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAIChatCompletionService("deployment", client, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GetTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - // Act - var result = await service.GetTextContentsAsync("Prompt"); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Text); - - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - } - - [Fact] - public async Task GetChatMessageContentsWithEmptyChoicesThrowsExceptionAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{\"id\":\"response-id\",\"object\":\"chat.completion\",\"created\":1704208954,\"model\":\"gpt-4\",\"choices\":[],\"usage\":{\"prompt_tokens\":55,\"completion_tokens\":100,\"total_tokens\":155},\"system_fingerprint\":null}") - }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([])); - - Assert.Equal("Chat completions not found", exception.Message); - } - - [Theory] - [InlineData(0)] - [InlineData(129)] - public async Task GetChatMessageContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetChatMessageContentsAsync([], settings)); - - Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task GetChatMessageContentsHandlesSettingsCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings() - { - MaxTokens = 123, - Temperature = 0.6, - TopP = 0.5, - FrequencyPenalty = 1.6, - PresencePenalty = 1.2, - ResultsPerPrompt = 5, - Seed = 567, - TokenSelectionBiases = new Dictionary { { 2, 3 } }, - StopSequences = ["stop_sequence"], - Logprobs = true, - TopLogprobs = 5, - AzureChatExtensionsOptions = new AzureChatExtensionsOptions - { - Extensions = - { - new AzureSearchChatExtensionConfiguration - { - SearchEndpoint = new Uri("http://test-search-endpoint"), - IndexName = "test-index-name" - } - } - } - }; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("User Message"); - chatHistory.AddUserMessage([new ImageContent(new Uri("https://image")), new TextContent("User Message")]); - chatHistory.AddSystemMessage("System Message"); - chatHistory.AddAssistantMessage("Assistant Message"); - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - // Act - var result = await service.GetChatMessageContentsAsync(chatHistory, settings); - - // Assert - var requestContent = this._messageHandlerStub.RequestContents[0]; - - Assert.NotNull(requestContent); - - var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); - - var messages = content.GetProperty("messages"); - - var userMessage = messages[0]; - var userMessageCollection = messages[1]; - var systemMessage = messages[2]; - var assistantMessage = messages[3]; - - Assert.Equal("user", userMessage.GetProperty("role").GetString()); - Assert.Equal("User Message", userMessage.GetProperty("content").GetString()); - - Assert.Equal("user", userMessageCollection.GetProperty("role").GetString()); - var contentItems = userMessageCollection.GetProperty("content"); - Assert.Equal(2, contentItems.GetArrayLength()); - Assert.Equal("https://image/", contentItems[0].GetProperty("image_url").GetProperty("url").GetString()); - Assert.Equal("image_url", contentItems[0].GetProperty("type").GetString()); - Assert.Equal("User Message", contentItems[1].GetProperty("text").GetString()); - Assert.Equal("text", contentItems[1].GetProperty("type").GetString()); - - Assert.Equal("system", systemMessage.GetProperty("role").GetString()); - Assert.Equal("System Message", systemMessage.GetProperty("content").GetString()); - - Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); - Assert.Equal("Assistant Message", assistantMessage.GetProperty("content").GetString()); - - Assert.Equal(123, content.GetProperty("max_tokens").GetInt32()); - Assert.Equal(0.6, content.GetProperty("temperature").GetDouble()); - Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); - Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); - Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); - Assert.Equal(5, content.GetProperty("n").GetInt32()); - Assert.Equal(567, content.GetProperty("seed").GetInt32()); - Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); - Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); - Assert.True(content.GetProperty("logprobs").GetBoolean()); - Assert.Equal(5, content.GetProperty("top_logprobs").GetInt32()); - - var dataSources = content.GetProperty("data_sources"); - Assert.Equal(1, dataSources.GetArrayLength()); - Assert.Equal("azure_search", dataSources[0].GetProperty("type").GetString()); - - var dataSourceParameters = dataSources[0].GetProperty("parameters"); - Assert.Equal("http://test-search-endpoint/", dataSourceParameters.GetProperty("endpoint").GetString()); - Assert.Equal("test-index-name", dataSourceParameters.GetProperty("index_name").GetString()); - } - - [Theory] - [MemberData(nameof(ResponseFormats))] - public async Task GetChatMessageContentsHandlesResponseFormatCorrectlyAsync(object responseFormat, string? expectedResponseType) - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings - { - ResponseFormat = responseFormat - }; - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - // Act - var result = await service.GetChatMessageContentsAsync([], settings); - - // Assert - var requestContent = this._messageHandlerStub.RequestContents[0]; - - Assert.NotNull(requestContent); - - var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); - - Assert.Equal(expectedResponseType, content.GetProperty("response_format").GetProperty("type").GetString()); - } - - [Theory] - [MemberData(nameof(ToolCallBehaviors))] - public async Task GetChatMessageContentsWorksCorrectlyAsync(ToolCallBehavior behavior) - { - // Arrange - var kernel = Kernel.CreateBuilder().Build(); - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = behavior }; - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Content); - - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - - Assert.Equal("stop", result[0].Metadata?["FinishReason"]); - } - - [Fact] - public async Task GetChatMessageContentsWithFunctionCallAsync() - { - // Arrange - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function1 = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => - { - functionCallCount++; - throw new ArgumentException("Some exception"); - }, "FunctionWithException"); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Content); - - Assert.Equal(2, functionCallCount); - } - - [Fact] - public async Task GetChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() - { - // Arrange - const int DefaultMaximumAutoInvokeAttempts = 128; - const int ModelResponsesCount = 129; - - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - var responses = new List(); - - for (var i = 0; i < ModelResponsesCount; i++) - { - responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }); - } - - this._messageHandlerStub.ResponsesToReturn = responses; - - // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); - - // Assert - Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); - } - - [Fact] - public async Task GetChatMessageContentsWithRequiredFunctionCallAsync() - { - // Arrange - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); - - kernel.Plugins.Add(plugin); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_single_function_call_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act - var result = await service.GetChatMessageContentsAsync([], settings, kernel); - - // Assert - Assert.Equal(1, functionCallCount); - - var requestContents = this._messageHandlerStub.RequestContents; - - Assert.Equal(2, requestContents.Count); - - requestContents.ForEach(Assert.NotNull); - - var firstContent = Encoding.UTF8.GetString(requestContents[0]!); - var secondContent = Encoding.UTF8.GetString(requestContents[1]!); - - var firstContentJson = JsonSerializer.Deserialize(firstContent); - var secondContentJson = JsonSerializer.Deserialize(secondContent); - - Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("MyPlugin-GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); - - Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); - } - - [Fact] - public async Task GetStreamingTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }); - - // Act & Assert - var enumerator = service.GetStreamingTextContentsAsync("Prompt").GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Text); - - await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }); - - // Act & Assert - var enumerator = service.GetStreamingChatMessageContentsAsync([]).GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Content); - - await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWithFunctionCallAsync() - { - // Arrange - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function1 = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - var function2 = KernelFunctionFactory.CreateFromMethod((string argument) => - { - functionCallCount++; - throw new ArgumentException("Some exception"); - }, "FunctionWithException"); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2])); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_multiple_function_calls_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act & Assert - var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); - - await enumerator.MoveNextAsync(); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); - - // Keep looping until the end of stream - while (await enumerator.MoveNextAsync()) - { - } - - Assert.Equal(2, functionCallCount); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWithFunctionCallMaximumAutoInvokeAttemptsAsync() - { - // Arrange - const int DefaultMaximumAutoInvokeAttempts = 128; - const int ModelResponsesCount = 129; - - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions("MyPlugin", [function])); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - var responses = new List(); - - for (var i = 0; i < ModelResponsesCount; i++) - { - responses.Add(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }); - } - - this._messageHandlerStub.ResponsesToReturn = responses; - - // Act & Assert - await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([], settings, kernel)) - { - Assert.Equal("Test chat streaming response", chunk.Content); - } - - Assert.Equal(DefaultMaximumAutoInvokeAttempts, functionCallCount); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWithRequiredFunctionCallAsync() - { - // Arrange - int functionCallCount = 0; - - var kernel = Kernel.CreateBuilder().Build(); - var function = KernelFunctionFactory.CreateFromMethod((string location) => - { - functionCallCount++; - return "Some weather"; - }, "GetCurrentWeather"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - var openAIFunction = plugin.GetFunctionsMetadata().First().ToOpenAIFunction(); - - kernel.Plugins.Add(plugin); - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient, this._mockLoggerFactory.Object); - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.RequireFunction(openAIFunction, autoInvoke: true) }; - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_single_function_call_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act & Assert - var enumerator = service.GetStreamingChatMessageContentsAsync([], settings, kernel).GetAsyncEnumerator(); - - // Function Tool Call Streaming (One Chunk) - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Content); - Assert.Equal("tool_calls", enumerator.Current.Metadata?["FinishReason"]); - - // Chat Completion Streaming (1st Chunk) - await enumerator.MoveNextAsync(); - Assert.Null(enumerator.Current.Metadata?["FinishReason"]); - - // Chat Completion Streaming (2nd Chunk) - await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); - - Assert.Equal(1, functionCallCount); - - var requestContents = this._messageHandlerStub.RequestContents; - - Assert.Equal(2, requestContents.Count); - - requestContents.ForEach(Assert.NotNull); - - var firstContent = Encoding.UTF8.GetString(requestContents[0]!); - var secondContent = Encoding.UTF8.GetString(requestContents[1]!); - - var firstContentJson = JsonSerializer.Deserialize(firstContent); - var secondContentJson = JsonSerializer.Deserialize(secondContent); - - Assert.Equal(1, firstContentJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("MyPlugin-GetCurrentWeather", firstContentJson.GetProperty("tool_choice").GetProperty("function").GetProperty("name").GetString()); - - Assert.Equal("none", secondContentJson.GetProperty("tool_choice").GetString()); - } - - [Fact] - public async Task GetChatMessageContentsUsesPromptAndSettingsCorrectlyAsync() - { - // Arrange - const string Prompt = "This is test prompt"; - const string SystemMessage = "This is test system message"; - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - IKernelBuilder builder = Kernel.CreateBuilder(); - builder.Services.AddTransient((sp) => service); - Kernel kernel = builder.Build(); - - // Act - var result = await kernel.InvokePromptAsync(Prompt, new(settings)); - - // Assert - Assert.Equal("Test chat response", result.ToString()); - - var requestContentByteArray = this._messageHandlerStub.RequestContents[0]; - - Assert.NotNull(requestContentByteArray); - - var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray)); - - var messages = requestContent.GetProperty("messages"); - - Assert.Equal(2, messages.GetArrayLength()); - - Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); - Assert.Equal("system", messages[0].GetProperty("role").GetString()); - - Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); - Assert.Equal("user", messages[1].GetProperty("role").GetString()); - } - - [Fact] - public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync() - { - // Arrange - const string Prompt = "This is test prompt"; - const string SystemMessage = "This is test system message"; - const string AssistantMessage = "This is assistant message"; - const string CollectionItemPrompt = "This is collection item prompt"; - - var service = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; - - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage(Prompt); - chatHistory.AddAssistantMessage(AssistantMessage); - chatHistory.AddUserMessage( - [ - new TextContent(CollectionItemPrompt), - new ImageContent(new Uri("https://image")) - ]); - - // Act - var result = await service.GetChatMessageContentsAsync(chatHistory, settings); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Content); - - var requestContentByteArray = this._messageHandlerStub.RequestContents[0]; - - Assert.NotNull(requestContentByteArray); - - var requestContent = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContentByteArray)); - - var messages = requestContent.GetProperty("messages"); - - Assert.Equal(4, messages.GetArrayLength()); - - Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); - Assert.Equal("system", messages[0].GetProperty("role").GetString()); - - Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); - Assert.Equal("user", messages[1].GetProperty("role").GetString()); - - Assert.Equal(AssistantMessage, messages[2].GetProperty("content").GetString()); - Assert.Equal("assistant", messages[2].GetProperty("role").GetString()); - - var contentItems = messages[3].GetProperty("content"); - Assert.Equal(2, contentItems.GetArrayLength()); - Assert.Equal(CollectionItemPrompt, contentItems[0].GetProperty("text").GetString()); - Assert.Equal("text", contentItems[0].GetProperty("type").GetString()); - Assert.Equal("https://image/", contentItems[1].GetProperty("image_url").GetProperty("url").GetString()); - Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); - } - - [Fact] - public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() - { - // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) - }); - - var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Fake prompt"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - Assert.NotNull(result); - Assert.Equal(5, result.Items.Count); - - var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; - Assert.NotNull(getCurrentWeatherFunctionCall); - Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); - Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); - Assert.Equal("1", getCurrentWeatherFunctionCall.Id); - Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); - - var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; - Assert.NotNull(functionWithExceptionFunctionCall); - Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); - Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); - Assert.Equal("2", functionWithExceptionFunctionCall.Id); - Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); - - var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; - Assert.NotNull(nonExistentFunctionCall); - Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); - Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); - Assert.Equal("3", nonExistentFunctionCall.Id); - Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); - - var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; - Assert.NotNull(invalidArgumentsFunctionCall); - Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); - Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); - Assert.Equal("4", invalidArgumentsFunctionCall.Id); - Assert.Null(invalidArgumentsFunctionCall.Arguments); - Assert.NotNull(invalidArgumentsFunctionCall.Exception); - Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); - Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); - - var intArgumentsFunctionCall = result.Items[4] as FunctionCallContent; - Assert.NotNull(intArgumentsFunctionCall); - Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); - Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); - Assert.Equal("5", intArgumentsFunctionCall.Id); - Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); - } - - [Fact] - public async Task FunctionCallsShouldBeReturnedToLLMAsync() - { - // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - - var items = new ChatMessageContentItemCollection - { - new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), - new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) - }; - - ChatHistory chatHistory = - [ - new ChatMessageContent(AuthorRole.Assistant, items) - ]; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(1, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); - - Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); - - var tool1 = assistantMessage.GetProperty("tool_calls")[0]; - Assert.Equal("1", tool1.GetProperty("id").GetString()); - Assert.Equal("function", tool1.GetProperty("type").GetString()); - - var function1 = tool1.GetProperty("function"); - Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); - Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); - - var tool2 = assistantMessage.GetProperty("tool_calls")[1]; - Assert.Equal("2", tool2.GetProperty("id").GetString()); - Assert.Equal("function", tool2.GetProperty("type").GetString()); - - var function2 = tool2.GetProperty("function"); - Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); - Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); - } - - [Fact] - public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() - { - // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - - var chatHistory = new ChatHistory - { - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - ]), - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") - ]) - }; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); - Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); - Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - - var assistantMessage2 = messages[1]; - Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); - Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); - Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); - } - - [Fact] - public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() - { - // Arrange - this._messageHandlerStub.ResponsesToReturn.Add(new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) - }); - - var sut = new AzureOpenAIChatCompletionService("deployment", "https://endpoint", "api-key", "model-id", this._httpClient); - - var chatHistory = new ChatHistory - { - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") - ]) - }; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[0]!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); - Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); - Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - - var assistantMessage2 = messages[1]; - Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); - Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); - Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - public static TheoryData ToolCallBehaviors => new() - { - ToolCallBehavior.EnableKernelFunctions, - ToolCallBehavior.AutoInvokeKernelFunctions - }; - - public static TheoryData ResponseFormats => new() - { - { new FakeChatCompletionsResponseFormat(), null }, - { "json_object", "json_object" }, - { "text", "text" } - }; - - private sealed class FakeChatCompletionsResponseFormat : ChatCompletionsResponseFormat; -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs deleted file mode 100644 index 7d1c47388f91..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletion/OpenAIChatCompletionServiceTests.cs +++ /dev/null @@ -1,687 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.TextGeneration; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletion; - -/// -/// Unit tests for -/// -public sealed class OpenAIChatCompletionServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly OpenAIFunction _timepluginDate, _timepluginNow; - private readonly OpenAIPromptExecutionSettings _executionSettings; - private readonly Mock _mockLoggerFactory; - - public OpenAIChatCompletionServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - - IList functions = KernelPluginFactory.CreateFromFunctions("TimePlugin", new[] - { - KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.Date.ToString(format, CultureInfo.InvariantCulture), "Date", "TimePlugin.Date"), - KernelFunctionFactory.CreateFromMethod((string? format = null) => DateTime.Now.ToString(format, CultureInfo.InvariantCulture), "Now", "TimePlugin.Now"), - }).GetFunctionsMetadata(); - - this._timepluginDate = functions[0].ToOpenAIFunction(); - this._timepluginNow = functions[1].ToOpenAIFunction(); - - this._executionSettings = new() - { - ToolCallBehavior = ToolCallBehavior.EnableFunctions([this._timepluginDate, this._timepluginNow]) - }; - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAIChatCompletionService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIChatCompletionService("model-id", "api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData("http://localhost:1234/chat/completions", "http://localhost:1234/chat/completions")] // Uses full path when provided - [InlineData("http://localhost:1234/v2/chat/completions", "http://localhost:1234/v2/chat/completions")] // Uses full path when provided - [InlineData("http://localhost:1234", "http://localhost:1234/v1/chat/completions")] - [InlineData("http://localhost:8080", "http://localhost:8080/v1/chat/completions")] - [InlineData("https://something:8080", "https://something:8080/v1/chat/completions")] // Accepts TLS Secured endpoints - public async Task ItUsesCustomEndpointsWhenProvidedAsync(string endpointProvided, string expectedEndpoint) - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: null, httpClient: this._httpClient, endpoint: new Uri(endpointProvided)); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - Assert.Equal(expectedEndpoint, this._messageHandlerStub.RequestUri!.ToString()); - } - - [Fact] - public async Task ItUsesHttpClientEndpointIfProvidedEndpointIsMissingAsync() - { - // Arrange - this._httpClient.BaseAddress = new Uri("http://localhost:12312"); - var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: null, httpClient: this._httpClient, endpoint: null!); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - Assert.Equal("http://localhost:12312/v1/chat/completions", this._messageHandlerStub.RequestUri!.ToString()); - } - - [Fact] - public async Task ItUsesDefaultEndpointIfProvidedEndpointIsMissingAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "any", apiKey: "abc", httpClient: this._httpClient, endpoint: null!); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - Assert.Equal("https://api.openai.com/v1/chat/completions", this._messageHandlerStub.RequestUri!.ToString()); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new OpenAIChatCompletionService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIChatCompletionService("model-id", client); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task ItCreatesCorrectFunctionToolCallsWhenUsingAutoAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.Equal(2, optionsJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("TimePlugin-Date", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); - Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[1].GetProperty("function").GetProperty("name").GetString()); - } - - [Fact] - public async Task ItCreatesCorrectFunctionToolCallsWhenUsingNowAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - this._executionSettings.ToolCallBehavior = ToolCallBehavior.RequireFunction(this._timepluginNow); - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.Equal(1, optionsJson.GetProperty("tools").GetArrayLength()); - Assert.Equal("TimePlugin-Now", optionsJson.GetProperty("tools")[0].GetProperty("function").GetProperty("name").GetString()); - } - - [Fact] - public async Task ItCreatesNoFunctionsWhenUsingNoneAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - this._executionSettings.ToolCallBehavior = null; - - // Act - await chatCompletion.GetChatMessageContentsAsync([], this._executionSettings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.False(optionsJson.TryGetProperty("functions", out var _)); - } - - [Fact] - public async Task ItAddsIdToChatMessageAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - var chatHistory = new ChatHistory(); - chatHistory.AddMessage(AuthorRole.Tool, "Hello", metadata: new Dictionary() { { OpenAIChatMessageContent.ToolIdProperty, "John Doe" } }); - - // Act - await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - Assert.Equal(1, optionsJson.GetProperty("messages").GetArrayLength()); - Assert.Equal("John Doe", optionsJson.GetProperty("messages")[0].GetProperty("tool_call_id").GetString()); - } - - [Fact] - public async Task ItGetChatMessageContentsShouldHaveModelIdDefinedAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(AzureChatCompletionResponse, Encoding.UTF8, "application/json") }; - - var chatHistory = new ChatHistory(); - chatHistory.AddMessage(AuthorRole.User, "Hello"); - - // Act - var chatMessage = await chatCompletion.GetChatMessageContentAsync(chatHistory, this._executionSettings); - - // Assert - Assert.NotNull(chatMessage.ModelId); - Assert.Equal("gpt-3.5-turbo", chatMessage.ModelId); - } - - [Fact] - public async Task ItGetTextContentsShouldHaveModelIdDefinedAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(AzureChatCompletionResponse, Encoding.UTF8, "application/json") }; - - var chatHistory = new ChatHistory(); - chatHistory.AddMessage(AuthorRole.User, "Hello"); - - // Act - var textContent = await chatCompletion.GetTextContentAsync("hello", this._executionSettings); - - // Assert - Assert.NotNull(textContent.ModelId); - Assert.Equal("gpt-3.5-turbo", textContent.ModelId); - } - - [Fact] - public async Task GetStreamingTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - var enumerator = service.GetStreamingTextContentsAsync("Prompt").GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Text); - - await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAIChatCompletionService("model-id", "api-key", "organization", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - var enumerator = service.GetStreamingChatMessageContentsAsync([]).GetAsyncEnumerator(); - - await enumerator.MoveNextAsync(); - Assert.Equal("Test chat streaming response", enumerator.Current.Content); - - await enumerator.MoveNextAsync(); - Assert.Equal("stop", enumerator.Current.Metadata?["FinishReason"]); - } - - [Fact] - public async Task ItAddsSystemMessageAsync() - { - // Arrange - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - var chatHistory = new ChatHistory(); - chatHistory.AddMessage(AuthorRole.User, "Hello"); - - // Act - await chatCompletion.GetChatMessageContentsAsync(chatHistory, this._executionSettings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(1, messages.GetArrayLength()); - - Assert.Equal("Hello", messages[0].GetProperty("content").GetString()); - Assert.Equal("user", messages[0].GetProperty("role").GetString()); - } - - [Fact] - public async Task GetChatMessageContentsWithChatMessageContentItemCollectionAndSettingsCorrectlyAsync() - { - // Arrange - const string Prompt = "This is test prompt"; - const string SystemMessage = "This is test system message"; - const string AssistantMessage = "This is assistant message"; - const string CollectionItemPrompt = "This is collection item prompt"; - - var chatCompletion = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - var settings = new OpenAIPromptExecutionSettings() { ChatSystemPrompt = SystemMessage }; - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { Content = new StringContent(ChatCompletionResponse) }; - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage(Prompt); - chatHistory.AddAssistantMessage(AssistantMessage); - chatHistory.AddUserMessage( - [ - new TextContent(CollectionItemPrompt), - new ImageContent(new Uri("https://image")) - ]); - - // Act - await chatCompletion.GetChatMessageContentsAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - - Assert.Equal(4, messages.GetArrayLength()); - - Assert.Equal(SystemMessage, messages[0].GetProperty("content").GetString()); - Assert.Equal("system", messages[0].GetProperty("role").GetString()); - - Assert.Equal(Prompt, messages[1].GetProperty("content").GetString()); - Assert.Equal("user", messages[1].GetProperty("role").GetString()); - - Assert.Equal(AssistantMessage, messages[2].GetProperty("content").GetString()); - Assert.Equal("assistant", messages[2].GetProperty("role").GetString()); - - var contentItems = messages[3].GetProperty("content"); - Assert.Equal(2, contentItems.GetArrayLength()); - Assert.Equal(CollectionItemPrompt, contentItems[0].GetProperty("text").GetString()); - Assert.Equal("text", contentItems[0].GetProperty("type").GetString()); - Assert.Equal("https://image/", contentItems[1].GetProperty("image_url").GetProperty("url").GetString()); - Assert.Equal("image_url", contentItems[1].GetProperty("type").GetString()); - } - - [Fact] - public async Task FunctionCallsShouldBePropagatedToCallersViaChatMessageItemsOfTypeFunctionCallContentAsync() - { - // Arrange - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_multiple_function_calls_test_response.json")) - }; - - var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Fake prompt"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - Assert.NotNull(result); - Assert.Equal(5, result.Items.Count); - - var getCurrentWeatherFunctionCall = result.Items[0] as FunctionCallContent; - Assert.NotNull(getCurrentWeatherFunctionCall); - Assert.Equal("GetCurrentWeather", getCurrentWeatherFunctionCall.FunctionName); - Assert.Equal("MyPlugin", getCurrentWeatherFunctionCall.PluginName); - Assert.Equal("1", getCurrentWeatherFunctionCall.Id); - Assert.Equal("Boston, MA", getCurrentWeatherFunctionCall.Arguments?["location"]?.ToString()); - - var functionWithExceptionFunctionCall = result.Items[1] as FunctionCallContent; - Assert.NotNull(functionWithExceptionFunctionCall); - Assert.Equal("FunctionWithException", functionWithExceptionFunctionCall.FunctionName); - Assert.Equal("MyPlugin", functionWithExceptionFunctionCall.PluginName); - Assert.Equal("2", functionWithExceptionFunctionCall.Id); - Assert.Equal("value", functionWithExceptionFunctionCall.Arguments?["argument"]?.ToString()); - - var nonExistentFunctionCall = result.Items[2] as FunctionCallContent; - Assert.NotNull(nonExistentFunctionCall); - Assert.Equal("NonExistentFunction", nonExistentFunctionCall.FunctionName); - Assert.Equal("MyPlugin", nonExistentFunctionCall.PluginName); - Assert.Equal("3", nonExistentFunctionCall.Id); - Assert.Equal("value", nonExistentFunctionCall.Arguments?["argument"]?.ToString()); - - var invalidArgumentsFunctionCall = result.Items[3] as FunctionCallContent; - Assert.NotNull(invalidArgumentsFunctionCall); - Assert.Equal("InvalidArguments", invalidArgumentsFunctionCall.FunctionName); - Assert.Equal("MyPlugin", invalidArgumentsFunctionCall.PluginName); - Assert.Equal("4", invalidArgumentsFunctionCall.Id); - Assert.Null(invalidArgumentsFunctionCall.Arguments); - Assert.NotNull(invalidArgumentsFunctionCall.Exception); - Assert.Equal("Error: Function call arguments were invalid JSON.", invalidArgumentsFunctionCall.Exception.Message); - Assert.NotNull(invalidArgumentsFunctionCall.Exception.InnerException); - - var intArgumentsFunctionCall = result.Items[4] as FunctionCallContent; - Assert.NotNull(intArgumentsFunctionCall); - Assert.Equal("IntArguments", intArgumentsFunctionCall.FunctionName); - Assert.Equal("MyPlugin", intArgumentsFunctionCall.PluginName); - Assert.Equal("5", intArgumentsFunctionCall.Id); - Assert.Equal("36", intArgumentsFunctionCall.Arguments?["age"]?.ToString()); - } - - [Fact] - public async Task FunctionCallsShouldBeReturnedToLLMAsync() - { - // Arrange - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(ChatCompletionResponse) - }; - - var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - - var items = new ChatMessageContentItemCollection - { - new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), - new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }) - }; - - var chatHistory = new ChatHistory - { - new ChatMessageContent(AuthorRole.Assistant, items) - }; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(1, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("assistant", assistantMessage.GetProperty("role").GetString()); - - Assert.Equal(2, assistantMessage.GetProperty("tool_calls").GetArrayLength()); - - var tool1 = assistantMessage.GetProperty("tool_calls")[0]; - Assert.Equal("1", tool1.GetProperty("id").GetString()); - Assert.Equal("function", tool1.GetProperty("type").GetString()); - - var function1 = tool1.GetProperty("function"); - Assert.Equal("MyPlugin-GetCurrentWeather", function1.GetProperty("name").GetString()); - Assert.Equal("{\"location\":\"Boston, MA\"}", function1.GetProperty("arguments").GetString()); - - var tool2 = assistantMessage.GetProperty("tool_calls")[1]; - Assert.Equal("2", tool2.GetProperty("id").GetString()); - Assert.Equal("function", tool2.GetProperty("type").GetString()); - - var function2 = tool2.GetProperty("function"); - Assert.Equal("MyPlugin-GetWeatherForecast", function2.GetProperty("name").GetString()); - Assert.Equal("{\"location\":\"Boston, MA\"}", function2.GetProperty("arguments").GetString()); - } - - [Fact] - public async Task FunctionResultsCanBeProvidedToLLMAsOneResultPerChatMessageAsync() - { - // Arrange - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(ChatCompletionResponse) - }; - - var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - - var chatHistory = new ChatHistory - { - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - ]), - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") - ]) - }; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); - Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); - Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - - var assistantMessage2 = messages[1]; - Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); - Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); - Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); - } - - [Fact] - public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessageAsync() - { - // Arrange - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(ChatCompletionResponse) - }; - - var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); - - var chatHistory = new ChatHistory - { - new ChatMessageContent(AuthorRole.Tool, - [ - new FunctionResultContent(new FunctionCallContent("GetCurrentWeather", "MyPlugin", "1", new KernelArguments() { ["location"] = "Boston, MA" }), "rainy"), - new FunctionResultContent(new FunctionCallContent("GetWeatherForecast", "MyPlugin", "2", new KernelArguments() { ["location"] = "Boston, MA" }), "sunny") - ]) - }; - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings); - - // Assert - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - Assert.NotNull(actualRequestContent); - - var optionsJson = JsonSerializer.Deserialize(actualRequestContent); - - var messages = optionsJson.GetProperty("messages"); - Assert.Equal(2, messages.GetArrayLength()); - - var assistantMessage = messages[0]; - Assert.Equal("tool", assistantMessage.GetProperty("role").GetString()); - Assert.Equal("rainy", assistantMessage.GetProperty("content").GetString()); - Assert.Equal("1", assistantMessage.GetProperty("tool_call_id").GetString()); - - var assistantMessage2 = messages[1]; - Assert.Equal("tool", assistantMessage2.GetProperty("role").GetString()); - Assert.Equal("sunny", assistantMessage2.GetProperty("content").GetString()); - Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - private const string ChatCompletionResponse = """ - { - "id": "chatcmpl-8IlRBQU929ym1EqAY2J4T7GGkW5Om", - "object": "chat.completion", - "created": 1699482945, - "model": "gpt-3.5-turbo", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "function_call": { - "name": "TimePlugin_Date", - "arguments": "{}" - } - }, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 52, - "completion_tokens": 1, - "total_tokens": 53 - } - } - """; - private const string AzureChatCompletionResponse = """ - { - "id": "chatcmpl-8S914omCBNQ0KU1NFtxmupZpzKWv2", - "object": "chat.completion", - "created": 1701718534, - "model": "gpt-3.5-turbo", - "prompt_filter_results": [ - { - "prompt_index": 0, - "content_filter_results": { - "hate": { - "filtered": false, - "severity": "safe" - }, - "self_harm": { - "filtered": false, - "severity": "safe" - }, - "sexual": { - "filtered": false, - "severity": "safe" - }, - "violence": { - "filtered": false, - "severity": "safe" - } - } - } - ], - "choices": [ - { - "index": 0, - "finish_reason": "stop", - "message": { - "role": "assistant", - "content": "Hello! How can I help you today? Please provide me with a question or topic you would like information on." - }, - "content_filter_results": { - "hate": { - "filtered": false, - "severity": "safe" - }, - "self_harm": { - "filtered": false, - "severity": "safe" - }, - "sexual": { - "filtered": false, - "severity": "safe" - }, - "violence": { - "filtered": false, - "severity": "safe" - } - } - } - ], - "usage": { - "prompt_tokens": 23, - "completion_tokens": 23, - "total_tokens": 46 - } - } - """; -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs deleted file mode 100644 index 782267039c59..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatCompletionWithData/AzureOpenAIChatCompletionWithDataTests.cs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.ChatCompletionWithData; - -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - -/// -/// Unit tests for -/// -public sealed class AzureOpenAIChatCompletionWithDataTests : IDisposable -{ - private readonly AzureOpenAIChatCompletionWithDataConfig _config; - - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAIChatCompletionWithDataTests() - { - this._config = this.GetConfig(); - - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient, this._mockLoggerFactory.Object) : - new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - - // Assert - Assert.NotNull(service); - Assert.Equal("fake-completion-model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task SpecifiedConfigurationShouldBeUsedAsync() - { - // Arrange - const string ExpectedUri = "https://fake-completion-endpoint/openai/deployments/fake-completion-model-id/extensions/chat/completions?api-version=fake-api-version"; - var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - - // Act - await service.GetChatMessageContentsAsync([]); - - // Assert - var actualUri = this._messageHandlerStub.RequestUri?.AbsoluteUri; - var actualRequestHeaderValues = this._messageHandlerStub.RequestHeaders!.GetValues("Api-Key"); - var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - - Assert.Equal(ExpectedUri, actualUri); - - Assert.Contains("fake-completion-api-key", actualRequestHeaderValues); - Assert.Contains("https://fake-data-source-endpoint", actualRequestContent, StringComparison.OrdinalIgnoreCase); - Assert.Contains("fake-data-source-api-key", actualRequestContent, StringComparison.OrdinalIgnoreCase); - Assert.Contains("fake-data-source-index", actualRequestContent, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task DefaultApiVersionShouldBeUsedAsync() - { - // Arrange - var config = this.GetConfig(); - config.CompletionApiVersion = string.Empty; - - var service = new AzureOpenAIChatCompletionWithDataService(config, this._httpClient); - - // Act - await service.GetChatMessageContentsAsync([]); - - // Assert - var actualUri = this._messageHandlerStub.RequestUri?.AbsoluteUri; - - Assert.Contains("2024-02-01", actualUri, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task GetChatMessageContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_with_data_test_response.json")) - }; - - // Act - var result = await service.GetChatMessageContentsAsync([]); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat with data response", result[0].Content); - - var usage = result[0].Metadata?["Usage"] as ChatWithDataUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - } - - [Fact] - public async Task GetStreamingChatMessageContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_with_data_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - await foreach (var chunk in service.GetStreamingChatMessageContentsAsync([])) - { - Assert.Equal("Test chat with data streaming response", chunk.Content); - } - } - - [Fact] - public async Task GetTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_with_data_test_response.json")) - }; - - // Act - var result = await service.GetTextContentsAsync("Prompt"); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat with data response", result[0].Text); - - var usage = result[0].Metadata?["Usage"] as ChatWithDataUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - } - - [Fact] - public async Task GetStreamingTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAIChatCompletionWithDataService(this._config, this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("chat_completion_with_data_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) - { - Assert.Equal("Test chat with data streaming response", chunk.Text); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - private AzureOpenAIChatCompletionWithDataConfig GetConfig() - { - return new AzureOpenAIChatCompletionWithDataConfig - { - CompletionModelId = "fake-completion-model-id", - CompletionEndpoint = "https://fake-completion-endpoint", - CompletionApiKey = "fake-completion-api-key", - CompletionApiVersion = "fake-api-version", - DataSourceEndpoint = "https://fake-data-source-endpoint", - DataSourceApiKey = "fake-data-source-api-key", - DataSourceIndex = "fake-data-source-index" - }; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatHistoryExtensionsTests.cs deleted file mode 100644 index 722ee4d0817c..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ChatHistoryExtensionsTests.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; -public class ChatHistoryExtensionsTests -{ - [Fact] - public async Task ItCanAddMessageFromStreamingChatContentsAsync() - { - var metadata = new Dictionary() - { - { "message", "something" }, - }; - - var chatHistoryStreamingContents = new List - { - new(AuthorRole.User, "Hello ", metadata: metadata), - new(null, ", ", metadata: metadata), - new(null, "I ", metadata: metadata), - new(null, "am ", metadata : metadata), - new(null, "a ", metadata : metadata), - new(null, "test ", metadata : metadata), - }.ToAsyncEnumerable(); - - var chatHistory = new ChatHistory(); - var finalContent = "Hello , I am a test "; - string processedContent = string.Empty; - await foreach (var chatMessageChunk in chatHistory.AddStreamingMessageAsync(chatHistoryStreamingContents)) - { - processedContent += chatMessageChunk.Content; - } - - Assert.Single(chatHistory); - Assert.Equal(finalContent, processedContent); - Assert.Equal(finalContent, chatHistory[0].Content); - Assert.Equal(AuthorRole.User, chatHistory[0].Role); - Assert.Equal(metadata["message"], chatHistory[0].Metadata!["message"]); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs deleted file mode 100644 index b9619fc1bc58..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/Files/OpenAIFileServiceTests.cs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.Files; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIFileServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAIFileServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWorksCorrectlyForOpenAI(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAIFileService("api-key", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIFileService("api-key"); - - // Assert - Assert.NotNull(service); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWorksCorrectlyForAzure(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAIFileService(new Uri("http://localhost"), "api-key", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAIFileService(new Uri("http://localhost"), "api-key"); - - // Assert - Assert.NotNull(service); - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task DeleteFileWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) - { - // Arrange - var service = this.CreateFileService(isAzure); - using var response = - isFailedRequest ? - this.CreateFailedResponse() : - this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "test.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - this._messageHandlerStub.ResponseToReturn = response; - - // Act & Assert - if (isFailedRequest) - { - await Assert.ThrowsAsync(() => service.DeleteFileAsync("file-id")); - } - else - { - await service.DeleteFileAsync("file-id"); - } - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task GetFileWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) - { - // Arrange - var service = this.CreateFileService(isAzure); - using var response = - isFailedRequest ? - this.CreateFailedResponse() : - this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "file.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - this._messageHandlerStub.ResponseToReturn = response; - - // Act & Assert - if (isFailedRequest) - { - await Assert.ThrowsAsync(() => service.GetFileAsync("file-id")); - } - else - { - var file = await service.GetFileAsync("file-id"); - Assert.NotNull(file); - Assert.NotEqual(string.Empty, file.Id); - Assert.NotEqual(string.Empty, file.FileName); - Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); - Assert.NotEqual(0, file.SizeInBytes); - } - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task GetFilesWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) - { - // Arrange - var service = this.CreateFileService(isAzure); - using var response = - isFailedRequest ? - this.CreateFailedResponse() : - this.CreateSuccessResponse( - """ - { - "data": [ - { - "id": "123", - "filename": "file1.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - }, - { - "id": "456", - "filename": "file2.txt", - "purpose": "assistants", - "bytes": 999, - "created_at": 1677610606 - } - ] - } - """); - this._messageHandlerStub.ResponseToReturn = response; - - // Act & Assert - if (isFailedRequest) - { - await Assert.ThrowsAsync(() => service.GetFilesAsync()); - } - else - { - var files = (await service.GetFilesAsync()).ToArray(); - Assert.NotNull(files); - Assert.NotEmpty(files); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task GetFileContentWorksCorrectlyAsync(bool isAzure) - { - // Arrange - var data = BinaryData.FromString("Hello AI!"); - var service = this.CreateFileService(isAzure); - this._messageHandlerStub.ResponseToReturn = - new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new ByteArrayContent(data.ToArray()) - }; - - // Act & Assert - var content = await service.GetFileContentAsync("file-id"); - var result = content.Data!.Value; - Assert.Equal(data.ToArray(), result.ToArray()); - } - - [Theory] - [InlineData(true, true)] - [InlineData(false, true)] - [InlineData(true, false)] - [InlineData(false, false)] - public async Task UploadContentWorksCorrectlyAsync(bool isAzure, bool isFailedRequest) - { - // Arrange - var service = this.CreateFileService(isAzure); - using var response = - isFailedRequest ? - this.CreateFailedResponse() : - this.CreateSuccessResponse( - """ - { - "id": "123", - "filename": "test.txt", - "purpose": "assistants", - "bytes": 120000, - "created_at": 1677610602 - } - """); - this._messageHandlerStub.ResponseToReturn = response; - - var settings = new OpenAIFileUploadExecutionSettings("test.txt", OpenAIFilePurpose.Assistants); - - await using var stream = new MemoryStream(); - await using (var writer = new StreamWriter(stream, leaveOpen: true)) - { - await writer.WriteLineAsync("test"); - await writer.FlushAsync(); - } - - stream.Position = 0; - - var content = new BinaryContent(stream.ToArray(), "text/plain"); - - // Act & Assert - if (isFailedRequest) - { - await Assert.ThrowsAsync(() => service.UploadContentAsync(content, settings)); - } - else - { - var file = await service.UploadContentAsync(content, settings); - Assert.NotNull(file); - Assert.NotEqual(string.Empty, file.Id); - Assert.NotEqual(string.Empty, file.FileName); - Assert.NotEqual(DateTime.MinValue, file.CreatedTimestamp); - Assert.NotEqual(0, file.SizeInBytes); - } - } - - private OpenAIFileService CreateFileService(bool isAzure = false) - { - return - isAzure ? - new OpenAIFileService(new Uri("http://localhost"), "api-key", httpClient: this._httpClient) : - new OpenAIFileService("api-key", "organization", this._httpClient); - } - - private HttpResponseMessage CreateSuccessResponse(string payload) - { - return - new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = - new StringContent( - payload, - Encoding.UTF8, - "application/json") - }; - } - - private HttpResponseMessage CreateFailedResponse(string? payload = null) - { - return - new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest) - { - Content = - string.IsNullOrEmpty(payload) ? - null : - new StringContent( - payload, - Encoding.UTF8, - "application/json") - }; - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/AutoFunctionInvocationFilterTests.cs deleted file mode 100644 index 9a5103f83e6e..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/AutoFunctionInvocationFilterTests.cs +++ /dev/null @@ -1,752 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.FunctionCalling; - -public sealed class AutoFunctionInvocationFilterTests : IDisposable -{ - private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - - public AutoFunctionInvocationFilterTests() - { - this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); - - this._httpClient = new HttpClient(this._messageHandlerStub, false); - } - - [Fact] - public async Task FiltersAreExecutedCorrectlyAsync() - { - // Arrange - int filterInvocations = 0; - int functionInvocations = 0; - int[] expectedRequestSequenceNumbers = [0, 0, 1, 1]; - int[] expectedFunctionSequenceNumbers = [0, 1, 0, 1]; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - Kernel? contextKernel = null; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - contextKernel = context.Kernel; - - if (context.ChatHistory.Last() is OpenAIChatMessageContent content) - { - Assert.Equal(2, content.ToolCalls.Count); - } - - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - filterInvocations++; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(4, filterInvocations); - Assert.Equal(4, functionInvocations); - Assert.Equal(expectedRequestSequenceNumbers, requestSequenceNumbers); - Assert.Equal(expectedFunctionSequenceNumbers, functionSequenceNumbers); - Assert.Same(kernel, contextKernel); - Assert.Equal("Test chat response", result.ToString()); - } - - [Fact] - public async Task FiltersAreExecutedCorrectlyOnStreamingAsync() - { - // Arrange - int filterInvocations = 0; - int functionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { functionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - if (context.ChatHistory.Last() is OpenAIChatMessageContent content) - { - Assert.Equal(2, content.ToolCalls.Count); - } - - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - filterInvocations++; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { } - - // Assert - Assert.Equal(4, filterInvocations); - Assert.Equal(4, functionInvocations); - Assert.Equal([0, 0, 1, 1], requestSequenceNumbers); - Assert.Equal([0, 1, 0, 1], functionSequenceNumbers); - } - - [Fact] - public async Task DifferentWaysOfAddingFiltersWorkCorrectlyAsync() - { - // Arrange - var executionOrder = new List(); - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var filter1 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter1-Invoking"); - await next(context); - }); - - var filter2 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter2-Invoking"); - await next(context); - }); - - var builder = Kernel.CreateBuilder(); - - builder.Plugins.Add(plugin); - - builder.AddOpenAIChatCompletion( - modelId: "test-model-id", - apiKey: "test-api-key", - httpClient: this._httpClient); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - - // Case #1 - Add filter to services - builder.Services.AddSingleton(filter1); - - var kernel = builder.Build(); - - // Case #2 - Add filter to kernel - kernel.AutoFunctionInvocationFilters.Add(filter2); - - var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal("Filter1-Invoking", executionOrder[0]); - Assert.Equal("Filter2-Invoking", executionOrder[1]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task MultipleFiltersAreExecutedInOrderAsync(bool isStreaming) - { - // Arrange - var executionOrder = new List(); - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var filter1 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter1-Invoking"); - await next(context); - executionOrder.Add("Filter1-Invoked"); - }); - - var filter2 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter2-Invoking"); - await next(context); - executionOrder.Add("Filter2-Invoked"); - }); - - var filter3 = new AutoFunctionInvocationFilter(async (context, next) => - { - executionOrder.Add("Filter3-Invoking"); - await next(context); - executionOrder.Add("Filter3-Invoked"); - }); - - var builder = Kernel.CreateBuilder(); - - builder.Plugins.Add(plugin); - - builder.AddOpenAIChatCompletion( - modelId: "test-model-id", - apiKey: "test-api-key", - httpClient: this._httpClient); - - builder.Services.AddSingleton(filter1); - builder.Services.AddSingleton(filter2); - builder.Services.AddSingleton(filter3); - - var kernel = builder.Build(); - - var arguments = new KernelArguments(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - }); - - // Act - if (isStreaming) - { - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", arguments)) - { } - } - else - { - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - await kernel.InvokePromptAsync("Test prompt", arguments); - } - - // Assert - Assert.Equal("Filter1-Invoking", executionOrder[0]); - Assert.Equal("Filter2-Invoking", executionOrder[1]); - Assert.Equal("Filter3-Invoking", executionOrder[2]); - Assert.Equal("Filter3-Invoked", executionOrder[3]); - Assert.Equal("Filter2-Invoked", executionOrder[4]); - Assert.Equal("Filter1-Invoked", executionOrder[5]); - } - - [Fact] - public async Task FilterCanOverrideArgumentsAsync() - { - // Arrange - const string NewValue = "NewValue"; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - context.Arguments!["parameter"] = NewValue; - await next(context); - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal("NewValue", result.ToString()); - } - - [Fact] - public async Task FilterCanHandleExceptionAsync() - { - // Arrange - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - try - { - await next(context); - } - catch (KernelException exception) - { - Assert.Equal("Exception from Function1", exception.Message); - context.Result = new FunctionResult(context.Result, "Result from filter"); - } - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - var chatCompletion = new OpenAIChatCompletionService(modelId: "test-model-id", apiKey: "test-api-key", httpClient: this._httpClient); - var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - var chatHistory = new ChatHistory(); - - // Act - var result = await chatCompletion.GetChatMessageContentsAsync(chatHistory, executionSettings, kernel); - - var firstFunctionResult = chatHistory[^2].Content; - var secondFunctionResult = chatHistory[^1].Content; - - // Assert - Assert.Equal("Result from filter", firstFunctionResult); - Assert.Equal("Result from Function2", secondFunctionResult); - } - - [Fact] - public async Task FilterCanHandleExceptionOnStreamingAsync() - { - // Arrange - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { throw new KernelException("Exception from Function1"); }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => "Result from Function2", "Function2"); - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - try - { - await next(context); - } - catch (KernelException) - { - context.Result = new FunctionResult(context.Result, "Result from filter"); - } - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var chatCompletion = new OpenAIChatCompletionService(modelId: "test-model-id", apiKey: "test-api-key", httpClient: this._httpClient); - var chatHistory = new ChatHistory(); - var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in chatCompletion.GetStreamingChatMessageContentsAsync(chatHistory, executionSettings, kernel)) - { } - - var firstFunctionResult = chatHistory[^2].Content; - var secondFunctionResult = chatHistory[^1].Content; - - // Assert - Assert.Equal("Result from filter", firstFunctionResult); - Assert.Equal("Result from Function2", secondFunctionResult); - } - - [Fact] - public async Task FiltersCanSkipFunctionExecutionAsync() - { - // Arrange - int filterInvocations = 0; - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Filter delegate is invoked only for second function, the first one should be skipped. - if (context.Function.Name == "Function2") - { - await next(context); - } - - filterInvocations++; - }); - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(2, filterInvocations); - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(1, secondFunctionInvocations); - } - - [Fact] - public async Task PreFilterCanTerminateOperationAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Terminating before first function, so all functions won't be invoked. - context.Terminate = true; - - await next(context); - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - } - - [Fact] - public async Task PreFilterCanTerminateOperationOnStreamingAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - // Terminating before first function, so all functions won't be invoked. - context.Terminate = true; - - await next(context); - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { } - - // Assert - Assert.Equal(0, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - } - - [Fact] - public async Task PostFilterCanTerminateOperationAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - // Terminating after first function, so second function won't be invoked. - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingResponses(); - - // Act - var result = await kernel.InvokePromptAsync("Test prompt", new(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - })); - - // Assert - Assert.Equal(1, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - Assert.Equal([0], requestSequenceNumbers); - Assert.Equal([0], functionSequenceNumbers); - - // Results of function invoked before termination should be returned - var lastMessageContent = result.GetValue(); - Assert.NotNull(lastMessageContent); - - Assert.Equal("function1-value", lastMessageContent.Content); - Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); - } - - [Fact] - public async Task PostFilterCanTerminateOperationOnStreamingAsync() - { - // Arrange - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - List requestSequenceNumbers = []; - List functionSequenceNumbers = []; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => { firstFunctionInvocations++; return parameter; }, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => { secondFunctionInvocations++; return parameter; }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - requestSequenceNumbers.Add(context.RequestSequenceIndex); - functionSequenceNumbers.Add(context.FunctionSequenceIndex); - - await next(context); - - // Terminating after first function, so second function won't be invoked. - context.Terminate = true; - }); - - this._messageHandlerStub.ResponsesToReturn = GetFunctionCallingStreamingResponses(); - - var executionSettings = new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - List streamingContent = []; - - // Act - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", new(executionSettings))) - { - streamingContent.Add(item); - } - - // Assert - Assert.Equal(1, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - Assert.Equal([0], requestSequenceNumbers); - Assert.Equal([0], functionSequenceNumbers); - - // Results of function invoked before termination should be returned - Assert.Equal(3, streamingContent.Count); - - var lastMessageContent = streamingContent[^1] as StreamingChatMessageContent; - Assert.NotNull(lastMessageContent); - - Assert.Equal("function1-value", lastMessageContent.Content); - Assert.Equal(AuthorRole.Tool, lastMessageContent.Role); - } - - [Fact] - public async Task FilterContextHasCancellationTokenAsync() - { - // Arrange - using var cancellationTokenSource = new CancellationTokenSource(); - int firstFunctionInvocations = 0; - int secondFunctionInvocations = 0; - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => - { - cancellationTokenSource.Cancel(); - firstFunctionInvocations++; - return parameter; - }, "Function1"); - - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => - { - secondFunctionInvocations++; - return parameter; - }, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var kernel = this.GetKernelWithFilter(plugin, async (context, next) => - { - Assert.Equal(cancellationTokenSource.Token, context.CancellationToken); - - await next(context); - - context.CancellationToken.ThrowIfCancellationRequested(); - }); - - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - var arguments = new KernelArguments(new OpenAIPromptExecutionSettings { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() - => kernel.InvokePromptAsync("Test prompt", arguments, cancellationToken: cancellationTokenSource.Token)); - - Assert.Equal(1, firstFunctionInvocations); - Assert.Equal(0, secondFunctionInvocations); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public async Task FilterContextHasOperationRelatedInformationAsync(bool isStreaming) - { - // Arrange - List actualToolCallIds = []; - List actualChatMessageContents = []; - - var function = KernelFunctionFactory.CreateFromMethod(() => "Result"); - - var function1 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function1"); - var function2 = KernelFunctionFactory.CreateFromMethod((string parameter) => parameter, "Function2"); - - var plugin = KernelPluginFactory.CreateFromFunctions("MyPlugin", [function1, function2]); - - var filter = new AutoFunctionInvocationFilter(async (context, next) => - { - actualToolCallIds.Add(context.ToolCallId); - actualChatMessageContents.Add(context.ChatMessageContent); - - await next(context); - }); - - var builder = Kernel.CreateBuilder(); - - builder.Plugins.Add(plugin); - - builder.AddOpenAIChatCompletion( - modelId: "test-model-id", - apiKey: "test-api-key", - httpClient: this._httpClient); - - builder.Services.AddSingleton(filter); - - var kernel = builder.Build(); - - var arguments = new KernelArguments(new OpenAIPromptExecutionSettings - { - ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions - }); - - // Act - if (isStreaming) - { - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - await foreach (var item in kernel.InvokePromptStreamingAsync("Test prompt", arguments)) - { } - } - else - { - using var response1 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }; - using var response2 = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) }; - - this._messageHandlerStub.ResponsesToReturn = [response1, response2]; - - await kernel.InvokePromptAsync("Test prompt", arguments); - } - - // Assert - Assert.Equal(["tool-call-id-1", "tool-call-id-2"], actualToolCallIds); - - foreach (var chatMessageContent in actualChatMessageContents) - { - var content = chatMessageContent as OpenAIChatMessageContent; - - Assert.NotNull(content); - - Assert.Equal("test-model-id", content.ModelId); - Assert.Equal(AuthorRole.Assistant, content.Role); - Assert.Equal(2, content.ToolCalls.Count); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - #region private - -#pragma warning disable CA2000 // Dispose objects before losing scope - private static List GetFunctionCallingResponses() - { - return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_multiple_function_calls_test_response.json")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_test_response.json")) } - ]; - } - - private static List GetFunctionCallingStreamingResponses() - { - return [ - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("filters_streaming_multiple_function_calls_test_response.txt")) }, - new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent(OpenAITestHelper.GetTestResponse("chat_completion_streaming_test_response.txt")) } - ]; - } -#pragma warning restore CA2000 - - private Kernel GetKernelWithFilter( - KernelPlugin plugin, - Func, Task>? onAutoFunctionInvocation) - { - var builder = Kernel.CreateBuilder(); - var filter = new AutoFunctionInvocationFilter(onAutoFunctionInvocation); - - builder.Plugins.Add(plugin); - builder.Services.AddSingleton(filter); - - builder.AddOpenAIChatCompletion( - modelId: "test-model-id", - apiKey: "test-api-key", - httpClient: this._httpClient); - - return builder.Build(); - } - - private sealed class AutoFunctionInvocationFilter( - Func, Task>? onAutoFunctionInvocation) : IAutoFunctionInvocationFilter - { - private readonly Func, Task>? _onAutoFunctionInvocation = onAutoFunctionInvocation; - - public Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) => - this._onAutoFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; - } - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs deleted file mode 100644 index b45fc64b60ba..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/KernelFunctionMetadataExtensionsTests.cs +++ /dev/null @@ -1,257 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Linq; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -#pragma warning disable CA1812 // Uninstantiated internal types - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.FunctionCalling; - -public sealed class KernelFunctionMetadataExtensionsTests -{ - [Fact] - public void ItCanConvertToOpenAIFunctionNoParameters() - { - // Arrange - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToOpenAIFunction(); - - // Assert - Assert.Equal(sut.Name, result.FunctionName); - Assert.Equal(sut.PluginName, result.PluginName); - Assert.Equal(sut.Description, result.Description); - Assert.Equal($"{sut.PluginName}-{sut.Name}", result.FullyQualifiedName); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToOpenAIFunctionNoPluginName() - { - // Arrange - var sut = new KernelFunctionMetadata("foo") - { - PluginName = string.Empty, - Description = "baz", - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToOpenAIFunction(); - - // Assert - Assert.Equal(sut.Name, result.FunctionName); - Assert.Equal(sut.PluginName, result.PluginName); - Assert.Equal(sut.Description, result.Description); - Assert.Equal(sut.Name, result.FullyQualifiedName); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public void ItCanConvertToOpenAIFunctionWithParameter(bool withSchema) - { - // Arrange - var param1 = new KernelParameterMetadata("param1") - { - Description = "This is param1", - DefaultValue = "1", - ParameterType = typeof(int), - IsRequired = false, - Schema = withSchema ? KernelJsonSchema.Parse("""{"type":"integer"}""") : null, - }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal("This is param1 (default value: 1)", outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - Assert.NotNull(outputParam.Schema); - Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToOpenAIFunctionWithParameterNoType() - { - // Arrange - var param1 = new KernelParameterMetadata("param1") { Description = "This is param1" }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - ReturnParameter = new KernelReturnParameterMetadata - { - Description = "retDesc", - Schema = KernelJsonSchema.Parse("""{"type": "object" }"""), - } - }; - - // Act - var result = sut.ToOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal(param1.Description, outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - - Assert.NotNull(result.ReturnParameter); - Assert.Equal("retDesc", result.ReturnParameter.Description); - Assert.Equivalent(KernelJsonSchema.Parse("""{"type": "object" }"""), result.ReturnParameter.Schema); - Assert.Null(result.ReturnParameter.ParameterType); - } - - [Fact] - public void ItCanConvertToOpenAIFunctionWithNoReturnParameterType() - { - // Arrange - var param1 = new KernelParameterMetadata("param1") - { - Description = "This is param1", - ParameterType = typeof(int), - }; - - var sut = new KernelFunctionMetadata("foo") - { - PluginName = "bar", - Description = "baz", - Parameters = [param1], - }; - - // Act - var result = sut.ToOpenAIFunction(); - var outputParam = result.Parameters![0]; - - // Assert - Assert.Equal(param1.Name, outputParam.Name); - Assert.Equal(param1.Description, outputParam.Description); - Assert.Equal(param1.IsRequired, outputParam.IsRequired); - Assert.NotNull(outputParam.Schema); - Assert.Equal("integer", outputParam.Schema.RootElement.GetProperty("type").GetString()); - } - - [Fact] - public void ItCanCreateValidOpenAIFunctionManualForPlugin() - { - // Arrange - var kernel = new Kernel(); - kernel.Plugins.AddFromType("MyPlugin"); - - var functionMetadata = kernel.Plugins["MyPlugin"].First().Metadata; - - var sut = functionMetadata.ToOpenAIFunction(); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.NotNull(result); - Assert.Equal( - """{"type":"object","required":["parameter1","parameter2","parameter3"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"type":"string","enum":["Value1","Value2"],"description":"Enum parameter"},"parameter3":{"type":"string","format":"date-time","description":"DateTime parameter"}}}""", - result.Parameters.ToString() - ); - } - - [Fact] - public void ItCanCreateValidOpenAIFunctionManualForPrompt() - { - // Arrange - var promptTemplateConfig = new PromptTemplateConfig("Hello AI") - { - Description = "My sample function." - }; - promptTemplateConfig.InputVariables.Add(new InputVariable - { - Name = "parameter1", - Description = "String parameter", - JsonSchema = """{"type":"string","description":"String parameter"}""" - }); - promptTemplateConfig.InputVariables.Add(new InputVariable - { - Name = "parameter2", - Description = "Enum parameter", - JsonSchema = """{"enum":["Value1","Value2"],"description":"Enum parameter"}""" - }); - var function = KernelFunctionFactory.CreateFromPrompt(promptTemplateConfig); - var functionMetadata = function.Metadata; - var sut = functionMetadata.ToOpenAIFunction(); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.NotNull(result); - Assert.Equal( - """{"type":"object","required":["parameter1","parameter2"],"properties":{"parameter1":{"type":"string","description":"String parameter"},"parameter2":{"enum":["Value1","Value2"],"description":"Enum parameter"}}}""", - result.Parameters.ToString() - ); - } - - private enum MyEnum - { - Value1, - Value2 - } - - private sealed class MyPlugin - { - [KernelFunction, Description("My sample function.")] - public string MyFunction( - [Description("String parameter")] string parameter1, - [Description("Enum parameter")] MyEnum parameter2, - [Description("DateTime parameter")] DateTime parameter3 - ) - { - return "return"; - } - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs deleted file mode 100644 index a9f94d81a673..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/FunctionCalling/OpenAIFunctionTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text.Json; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.FunctionCalling; - -public sealed class OpenAIFunctionTests -{ - [Theory] - [InlineData(null, null, "", "")] - [InlineData("name", "description", "name", "description")] - public void ItInitializesOpenAIFunctionParameterCorrectly(string? name, string? description, string expectedName, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); - var functionParameter = new OpenAIFunctionParameter(name, description, true, typeof(string), schema); - - // Assert - Assert.Equal(expectedName, functionParameter.Name); - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.True(functionParameter.IsRequired); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Theory] - [InlineData(null, "")] - [InlineData("description", "description")] - public void ItInitializesOpenAIFunctionReturnParameterCorrectly(string? description, string expectedDescription) - { - // Arrange & Act - var schema = KernelJsonSchema.Parse("{\"type\": \"object\" }"); - var functionParameter = new OpenAIFunctionReturnParameter(description, typeof(string), schema); - - // Assert - Assert.Equal(expectedDescription, functionParameter.Description); - Assert.Equal(typeof(string), functionParameter.ParameterType); - Assert.Same(schema, functionParameter.Schema); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNoPluginName() - { - // Arrange - OpenAIFunction sut = KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.").Metadata.ToOpenAIFunction(); - - // Act - FunctionDefinition result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal(sut.FunctionName, result.Name); - Assert.Equal(sut.Description, result.Description); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithNullParameters() - { - // Arrange - OpenAIFunction sut = new("plugin", "function", "description", null, null); - - // Act - var result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{}}", result.Parameters.ToString()); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionWithPluginName() - { - // Arrange - OpenAIFunction sut = KernelPluginFactory.CreateFromFunctions("myplugin", new[] - { - KernelFunctionFactory.CreateFromMethod(() => { }, "myfunc", "This is a description of the function.") - }).GetFunctionsMetadata()[0].ToOpenAIFunction(); - - // Act - FunctionDefinition result = sut.ToFunctionDefinition(); - - // Assert - Assert.Equal("myplugin-myfunc", result.Name); - Assert.Equal(sut.Description, result.Description); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndReturnParameterType() - { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => "", - "TestFunction", - "My test function") - }); - - OpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); - - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); - - var exp = JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)); - var act = JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters)); - - Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithParameterTypesAndNoReturnParameterType() - { - string expectedParameterSchema = """{ "type": "object", "required": ["param1", "param2"], "properties": { "param1": { "type": "string", "description": "String param 1" }, "param2": { "type": "integer", "description": "Int param 2" } } } """; - - KernelPlugin plugin = KernelPluginFactory.CreateFromFunctions("Tests", new[] - { - KernelFunctionFactory.CreateFromMethod( - [return: Description("My test Result")] ([Description("String param 1")] string param1, [Description("Int param 2")] int param2) => { }, - "TestFunction", - "My test function") - }); - - OpenAIFunction sut = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); - - FunctionDefinition functionDefinition = sut.ToFunctionDefinition(); - - Assert.NotNull(functionDefinition); - Assert.Equal("Tests-TestFunction", functionDefinition.Name); - Assert.Equal("My test function", functionDefinition.Description); - Assert.Equal(JsonSerializer.Serialize(KernelJsonSchema.Parse(expectedParameterSchema)), JsonSerializer.Serialize(KernelJsonSchema.Parse(functionDefinition.Parameters))); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypes() - { - // Arrange - OpenAIFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: [new KernelParameterMetadata("param1")]).Metadata.ToOpenAIFunction(); - - // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; - - // Assert - Assert.NotNull(pd.properties); - Assert.Single(pd.properties); - Assert.Equal( - JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string" }""")), - JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); - } - - [Fact] - public void ItCanConvertToFunctionDefinitionsWithNoParameterTypesButWithDescriptions() - { - // Arrange - OpenAIFunction f = KernelFunctionFactory.CreateFromMethod( - () => { }, - parameters: [new KernelParameterMetadata("param1") { Description = "something neat" }]).Metadata.ToOpenAIFunction(); - - // Act - FunctionDefinition result = f.ToFunctionDefinition(); - ParametersData pd = JsonSerializer.Deserialize(result.Parameters.ToString())!; - - // Assert - Assert.NotNull(pd.properties); - Assert.Single(pd.properties); - Assert.Equal( - JsonSerializer.Serialize(KernelJsonSchema.Parse("""{ "type":"string", "description":"something neat" }""")), - JsonSerializer.Serialize(pd.properties.First().Value.RootElement)); - } - -#pragma warning disable CA1812 // uninstantiated internal class - private sealed class ParametersData - { - public string? type { get; set; } - public string[]? required { get; set; } - public Dictionary? properties { get; set; } - } -#pragma warning restore CA1812 -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs deleted file mode 100644 index 08bde153aa4a..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIMemoryBuilderExtensionsTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Azure.Core; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Memory; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -/// -/// Unit tests for class. -/// -public sealed class OpenAIMemoryBuilderExtensionsTests -{ - private readonly Mock _mockMemoryStore = new(); - - [Fact] - public void AzureOpenAITextEmbeddingGenerationWithApiKeyWorksCorrectly() - { - // Arrange - var builder = new MemoryBuilder(); - - // Act - var memory = builder - .WithAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key", "model-id") - .WithMemoryStore(this._mockMemoryStore.Object) - .Build(); - - // Assert - Assert.NotNull(memory); - } - - [Fact] - public void AzureOpenAITextEmbeddingGenerationWithTokenCredentialWorksCorrectly() - { - // Arrange - var builder = new MemoryBuilder(); - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - - // Act - var memory = builder - .WithAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials, "model-id") - .WithMemoryStore(this._mockMemoryStore.Object) - .Build(); - - // Assert - Assert.NotNull(memory); - } - - [Fact] - public void OpenAITextEmbeddingGenerationWithApiKeyWorksCorrectly() - { - // Arrange - var builder = new MemoryBuilder(); - - // Act - var memory = builder - .WithOpenAITextEmbeddingGeneration("model-id", "api-key", "organization-id") - .WithMemoryStore(this._mockMemoryStore.Object) - .Build(); - - // Assert - Assert.NotNull(memory); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs deleted file mode 100644 index b64649230d96..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIPromptExecutionSettingsTests.cs +++ /dev/null @@ -1,275 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -/// -/// Unit tests of OpenAIPromptExecutionSettings -/// -public class OpenAIPromptExecutionSettingsTests -{ - [Fact] - public void ItCreatesOpenAIExecutionSettingsWithCorrectDefaults() - { - // Arrange - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(null, 128); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(1, executionSettings.Temperature); - Assert.Equal(1, executionSettings.TopP); - Assert.Equal(0, executionSettings.FrequencyPenalty); - Assert.Equal(0, executionSettings.PresencePenalty); - Assert.Equal(1, executionSettings.ResultsPerPrompt); - Assert.Null(executionSettings.StopSequences); - Assert.Null(executionSettings.TokenSelectionBiases); - Assert.Null(executionSettings.TopLogprobs); - Assert.Null(executionSettings.Logprobs); - Assert.Null(executionSettings.AzureChatExtensionsOptions); - Assert.Equal(128, executionSettings.MaxTokens); - } - - [Fact] - public void ItUsesExistingOpenAIExecutionSettings() - { - // Arrange - OpenAIPromptExecutionSettings actualSettings = new() - { - Temperature = 0.7, - TopP = 0.7, - FrequencyPenalty = 0.7, - PresencePenalty = 0.7, - ResultsPerPrompt = 2, - StopSequences = new string[] { "foo", "bar" }, - ChatSystemPrompt = "chat system prompt", - MaxTokens = 128, - Logprobs = true, - TopLogprobs = 5, - TokenSelectionBiases = new Dictionary() { { 1, 2 }, { 3, 4 } }, - }; - - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(actualSettings, executionSettings); - } - - [Fact] - public void ItCanUseOpenAIExecutionSettings() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() { - { "max_tokens", 1000 }, - { "temperature", 0 } - } - }; - - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - Assert.NotNull(executionSettings); - Assert.Equal(1000, executionSettings.MaxTokens); - Assert.Equal(0, executionSettings.Temperature); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesSnakeCase() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() - { - { "temperature", 0.7 }, - { "top_p", 0.7 }, - { "frequency_penalty", 0.7 }, - { "presence_penalty", 0.7 }, - { "results_per_prompt", 2 }, - { "stop_sequences", new [] { "foo", "bar" } }, - { "chat_system_prompt", "chat system prompt" }, - { "max_tokens", 128 }, - { "token_selection_biases", new Dictionary() { { 1, 2 }, { 3, 4 } } }, - { "seed", 123456 }, - { "logprobs", true }, - { "top_logprobs", 5 }, - } - }; - - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromExtraPropertiesAsStrings() - { - // Arrange - PromptExecutionSettings actualSettings = new() - { - ExtensionData = new Dictionary() - { - { "temperature", "0.7" }, - { "top_p", "0.7" }, - { "frequency_penalty", "0.7" }, - { "presence_penalty", "0.7" }, - { "results_per_prompt", "2" }, - { "stop_sequences", new [] { "foo", "bar" } }, - { "chat_system_prompt", "chat system prompt" }, - { "max_tokens", "128" }, - { "token_selection_biases", new Dictionary() { { "1", "2" }, { "3", "4" } } }, - { "seed", 123456 }, - { "logprobs", true }, - { "top_logprobs", 5 } - } - }; - - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings, null); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Fact] - public void ItCreatesOpenAIExecutionSettingsFromJsonSnakeCase() - { - // Arrange - var json = """ - { - "temperature": 0.7, - "top_p": 0.7, - "frequency_penalty": 0.7, - "presence_penalty": 0.7, - "results_per_prompt": 2, - "stop_sequences": [ "foo", "bar" ], - "chat_system_prompt": "chat system prompt", - "token_selection_biases": { "1": 2, "3": 4 }, - "max_tokens": 128, - "seed": 123456, - "logprobs": true, - "top_logprobs": 5 - } - """; - var actualSettings = JsonSerializer.Deserialize(json); - - // Act - OpenAIPromptExecutionSettings executionSettings = OpenAIPromptExecutionSettings.FromExecutionSettings(actualSettings); - - // Assert - AssertExecutionSettings(executionSettings); - } - - [Theory] - [InlineData("", "")] - [InlineData("System prompt", "System prompt")] - public void ItUsesCorrectChatSystemPrompt(string chatSystemPrompt, string expectedChatSystemPrompt) - { - // Arrange & Act - var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = chatSystemPrompt }; - - // Assert - Assert.Equal(expectedChatSystemPrompt, settings.ChatSystemPrompt); - } - - [Fact] - public void PromptExecutionSettingsCloneWorksAsExpected() - { - // Arrange - string configPayload = """ - { - "max_tokens": 60, - "temperature": 0.5, - "top_p": 0.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0 - } - """; - var executionSettings = JsonSerializer.Deserialize(configPayload); - - // Act - var clone = executionSettings!.Clone(); - - // Assert - Assert.NotNull(clone); - Assert.Equal(executionSettings.ModelId, clone.ModelId); - Assert.Equivalent(executionSettings.ExtensionData, clone.ExtensionData); - } - - [Fact] - public void PromptExecutionSettingsFreezeWorksAsExpected() - { - // Arrange - string configPayload = """ - { - "max_tokens": 60, - "temperature": 0.5, - "top_p": 0.0, - "presence_penalty": 0.0, - "frequency_penalty": 0.0, - "stop_sequences": [ "DONE" ], - "token_selection_biases": { "1": 2, "3": 4 } - } - """; - var executionSettings = JsonSerializer.Deserialize(configPayload); - - // Act - executionSettings!.Freeze(); - - // Assert - Assert.True(executionSettings.IsFrozen); - Assert.Throws(() => executionSettings.ModelId = "gpt-4"); - Assert.Throws(() => executionSettings.ResultsPerPrompt = 2); - Assert.Throws(() => executionSettings.Temperature = 1); - Assert.Throws(() => executionSettings.TopP = 1); - Assert.Throws(() => executionSettings.StopSequences?.Add("STOP")); - Assert.Throws(() => executionSettings.TokenSelectionBiases?.Add(5, 6)); - - executionSettings!.Freeze(); // idempotent - Assert.True(executionSettings.IsFrozen); - } - - [Fact] - public void FromExecutionSettingsWithDataDoesNotIncludeEmptyStopSequences() - { - // Arrange - var executionSettings = new OpenAIPromptExecutionSettings { StopSequences = [] }; - - // Act -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - var executionSettingsWithData = OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(executionSettings); -#pragma warning restore CS0618 - // Assert - Assert.Null(executionSettingsWithData.StopSequences); - } - - private static void AssertExecutionSettings(OpenAIPromptExecutionSettings executionSettings) - { - Assert.NotNull(executionSettings); - Assert.Equal(0.7, executionSettings.Temperature); - Assert.Equal(0.7, executionSettings.TopP); - Assert.Equal(0.7, executionSettings.FrequencyPenalty); - Assert.Equal(0.7, executionSettings.PresencePenalty); - Assert.Equal(2, executionSettings.ResultsPerPrompt); - Assert.Equal(new string[] { "foo", "bar" }, executionSettings.StopSequences); - Assert.Equal("chat system prompt", executionSettings.ChatSystemPrompt); - Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, executionSettings.TokenSelectionBiases); - Assert.Equal(128, executionSettings.MaxTokens); - Assert.Equal(123456, executionSettings.Seed); - Assert.Equal(true, executionSettings.Logprobs); - Assert.Equal(5, executionSettings.TopLogprobs); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs deleted file mode 100644 index 5cc41c3c881e..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAIServiceCollectionExtensionsTests.cs +++ /dev/null @@ -1,746 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Embeddings; -using Microsoft.SemanticKernel.TextGeneration; -using Microsoft.SemanticKernel.TextToAudio; -using Microsoft.SemanticKernel.TextToImage; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -#pragma warning disable CS0618 // AzureOpenAIChatCompletionWithData is deprecated in favor of OpenAIPromptExecutionSettings.AzureChatExtensionsOptions - -/// -/// Unit tests for class. -/// -public sealed class OpenAIServiceCollectionExtensionsTests : IDisposable -{ - private readonly HttpClient _httpClient; - - public OpenAIServiceCollectionExtensionsTests() - { - this._httpClient = new HttpClient(); - } - - #region Text generation - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddAzureOpenAITextGenerationAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextGeneration("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextGeneration("deployment-name"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddAzureOpenAITextGenerationAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextGeneration("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAITextGeneration("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAITextGeneration("deployment-name"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddOpenAITextGenerationAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddOpenAITextGeneration("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.AddOpenAITextGeneration("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAITextGeneration("model-id"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddOpenAITextGenerationAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddOpenAITextGeneration("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.Services.AddOpenAITextGeneration("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAITextGeneration("model-id"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextGenerationService); - } - - #endregion - - #region Text embeddings - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextEmbeddingGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddAzureOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAITextEmbeddingGeneration("deployment-name"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextEmbeddingGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddOpenAITextEmbeddingGeneration("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.AddOpenAITextEmbeddingGeneration("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAITextEmbeddingGeneration("model-id"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextEmbeddingGenerationService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddOpenAITextEmbeddingGenerationAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddOpenAITextEmbeddingGeneration("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.Services.AddOpenAITextEmbeddingGeneration("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAITextEmbeddingGeneration("model-id"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextEmbeddingGenerationService); - } - - #endregion - - #region Chat completion - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - [InlineData(InitializationType.ChatCompletionWithData)] - public void KernelBuilderAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var config = this.GetCompletionWithDataConfig(); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAIChatCompletion("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAIChatCompletion("deployment-name"), - InitializationType.ChatCompletionWithData => builder.AddAzureOpenAIChatCompletion(config), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - - if (type == InitializationType.ChatCompletionWithData) - { - Assert.True(service is AzureOpenAIChatCompletionWithDataService); - } - else - { - Assert.True(service is AzureOpenAIChatCompletionService); - } - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - [InlineData(InitializationType.ChatCompletionWithData)] - public void ServiceCollectionAddAzureOpenAIChatCompletionAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var config = this.GetCompletionWithDataConfig(); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAIChatCompletion("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAIChatCompletion("deployment-name"), - InitializationType.ChatCompletionWithData => builder.Services.AddAzureOpenAIChatCompletion(config), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - - if (type == InitializationType.ChatCompletionWithData) - { - Assert.True(service is AzureOpenAIChatCompletionWithDataService); - } - else - { - Assert.True(service is AzureOpenAIChatCompletionService); - } - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientEndpoint)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddOpenAIChatCompletionAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddOpenAIChatCompletion("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.AddOpenAIChatCompletion("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAIChatCompletion("model-id"), - InitializationType.OpenAIClientEndpoint => builder.AddOpenAIChatCompletion("model-id", new Uri("http://localhost:12345"), "apikey"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAIChatCompletionService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientEndpoint)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddOpenAIChatCompletionAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddOpenAIChatCompletion("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.Services.AddOpenAIChatCompletion("model-id", client), - InitializationType.OpenAIClientEndpoint => builder.Services.AddOpenAIChatCompletion("model-id", new Uri("http://localhost:12345"), "apikey"), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAIChatCompletion("model-id"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAIChatCompletionService); - } - - #endregion - - #region Text to image - - [Fact] - public void KernelBuilderAddAzureOpenAITextToImageAddsValidServiceWithTokenCredentials() - { - // Arrange - var builder = Kernel.CreateBuilder(); - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - - // Act - builder = builder.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", credentials); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToImageService); - } - - [Fact] - public void ServiceCollectionAddAzureOpenAITextToImageAddsValidServiceTokenCredentials() - { - // Arrange - var builder = Kernel.CreateBuilder(); - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - - // Act - builder.Services.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", credentials); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToImageService); - } - - [Fact] - public void KernelBuilderAddAzureOpenAITextToImageAddsValidServiceWithApiKey() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder = builder.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToImageService); - } - - [Fact] - public void ServiceCollectionAddAzureOpenAITextToImageAddsValidServiceWithApiKey() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder.Services.AddAzureOpenAITextToImage("deployment-name", "https://endpoint", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToImageService); - } - - [Fact] - public void KernelBuilderAddOpenAITextToImageAddsValidServiceWithApiKey() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder = builder.AddOpenAITextToImage("model-id", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextToImageService); - } - - [Fact] - public void ServiceCollectionAddOpenAITextToImageAddsValidServiceWithApiKey() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder.Services.AddOpenAITextToImage("model-id", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextToImageService); - } - - #endregion - - #region Text to audio - - [Fact] - public void KernelBuilderAddAzureOpenAITextToAudioAddsValidService() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder = builder.AddAzureOpenAITextToAudio("deployment-name", "https://endpoint", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToAudioService); - } - - [Fact] - public void ServiceCollectionAddAzureOpenAITextToAudioAddsValidService() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder.Services.AddAzureOpenAITextToAudio("deployment-name", "https://endpoint", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAITextToAudioService); - } - - [Fact] - public void KernelBuilderAddOpenAITextToAudioAddsValidService() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder = builder.AddOpenAITextToAudio("model-id", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextToAudioService); - } - - [Fact] - public void ServiceCollectionAddOpenAITextToAudioAddsValidService() - { - // Arrange - var builder = Kernel.CreateBuilder(); - - // Act - builder.Services.AddOpenAITextToAudio("model-id", "api-key"); - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAITextToAudioService); - } - - #endregion - - #region Audio to text - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddAzureOpenAIAudioToTextAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.AddAzureOpenAIAudioToText("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddAzureOpenAIAudioToText("deployment-name"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAIAudioToTextService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.TokenCredential)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddAzureOpenAIAudioToTextAddsValidService(InitializationType type) - { - // Arrange - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", "api-key"), - InitializationType.TokenCredential => builder.Services.AddAzureOpenAIAudioToText("deployment-name", "https://endpoint", credentials), - InitializationType.OpenAIClientInline => builder.Services.AddAzureOpenAIAudioToText("deployment-name", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddAzureOpenAIAudioToText("deployment-name"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is AzureOpenAIAudioToTextService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void KernelBuilderAddOpenAIAudioToTextAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - builder = type switch - { - InitializationType.ApiKey => builder.AddOpenAIAudioToText("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.AddOpenAIAudioToText("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.AddOpenAIAudioToText("model-id"), - _ => builder - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAIAudioToTextService); - } - - [Theory] - [InlineData(InitializationType.ApiKey)] - [InlineData(InitializationType.OpenAIClientInline)] - [InlineData(InitializationType.OpenAIClientInServiceProvider)] - public void ServiceCollectionAddOpenAIAudioToTextAddsValidService(InitializationType type) - { - // Arrange - var client = new OpenAIClient("key"); - var builder = Kernel.CreateBuilder(); - - builder.Services.AddSingleton(client); - - // Act - IServiceCollection collection = type switch - { - InitializationType.ApiKey => builder.Services.AddOpenAIAudioToText("model-id", "api-key"), - InitializationType.OpenAIClientInline => builder.Services.AddOpenAIAudioToText("model-id", client), - InitializationType.OpenAIClientInServiceProvider => builder.Services.AddOpenAIAudioToText("model-id"), - _ => builder.Services - }; - - // Assert - var service = builder.Build().GetRequiredService(); - - Assert.NotNull(service); - Assert.True(service is OpenAIAudioToTextService); - } - - #endregion - - public void Dispose() - { - this._httpClient.Dispose(); - } - - public enum InitializationType - { - ApiKey, - TokenCredential, - OpenAIClientInline, - OpenAIClientInServiceProvider, - OpenAIClientEndpoint, - ChatCompletionWithData - } - - private AzureOpenAIChatCompletionWithDataConfig GetCompletionWithDataConfig() - { - return new() - { - CompletionApiKey = "completion-api-key", - CompletionApiVersion = "completion-v1", - CompletionEndpoint = "https://completion-endpoint", - CompletionModelId = "completion-model-id", - DataSourceApiKey = "data-source-api-key", - DataSourceEndpoint = "https://data-source-endpoint", - DataSourceIndex = "data-source-index" - }; - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAITestHelper.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAITestHelper.cs deleted file mode 100644 index f6ee6bb93a11..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/OpenAITestHelper.cs +++ /dev/null @@ -1,20 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.IO; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -/// -/// Helper for OpenAI test purposes. -/// -internal static class OpenAITestHelper -{ - /// - /// Reads test response from file for mocking purposes. - /// - /// Name of the file with test response. - internal static string GetTestResponse(string fileName) - { - return File.ReadAllText($"./OpenAI/TestData/{fileName}"); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json deleted file mode 100644 index 737b972309ba..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_multiple_function_calls_test_response.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "id": "response-id", - "object": "chat.completion", - "created": 1699896916, - "model": "gpt-3.5-turbo-0613", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "tool_calls": [ - { - "id": "1", - "type": "function", - "function": { - "name": "MyPlugin-GetCurrentWeather", - "arguments": "{\n\"location\": \"Boston, MA\"\n}" - } - }, - { - "id": "2", - "type": "function", - "function": { - "name": "MyPlugin-FunctionWithException", - "arguments": "{\n\"argument\": \"value\"\n}" - } - }, - { - "id": "3", - "type": "function", - "function": { - "name": "MyPlugin-NonExistentFunction", - "arguments": "{\n\"argument\": \"value\"\n}" - } - }, - { - "id": "4", - "type": "function", - "function": { - "name": "MyPlugin-InvalidArguments", - "arguments": "invalid_arguments_format" - } - }, - { - "id": "5", - "type": "function", - "function": { - "name": "MyPlugin-IntArguments", - "arguments": "{\n\"age\": 36\n}" - } - } - ] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 82, - "completion_tokens": 17, - "total_tokens": 99 - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json deleted file mode 100644 index 6c93e434f259..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_single_function_call_test_response.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "id": "response-id", - "object": "chat.completion", - "created": 1699896916, - "model": "gpt-3.5-turbo-0613", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "tool_calls": [ - { - "id": "1", - "type": "function", - "function": { - "name": "MyPlugin-GetCurrentWeather", - "arguments": "{\n\"location\": \"Boston, MA\"\n}" - } - } - ] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 82, - "completion_tokens": 17, - "total_tokens": 99 - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt deleted file mode 100644 index ceb8f3e8b44b..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt +++ /dev/null @@ -1,9 +0,0 @@ -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"2","type":"function","function":{"name":"MyPlugin-FunctionWithException","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":2,"id":"3","type":"function","function":{"name":"MyPlugin-NonExistentFunction","arguments":"{\n\"argument\": \"value\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":3,"id":"4","type":"function","function":{"name":"MyPlugin-InvalidArguments","arguments":"invalid_arguments_format"}}]},"finish_reason":"tool_calls"}]} - -data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt deleted file mode 100644 index 6835039941ce..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_single_function_call_test_response.txt +++ /dev/null @@ -1,3 +0,0 @@ -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"1","type":"function","function":{"name":"MyPlugin-GetCurrentWeather","arguments":"{\n\"location\": \"Boston, MA\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt deleted file mode 100644 index e5e8d1b19afd..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_streaming_test_response.txt +++ /dev/null @@ -1,5 +0,0 @@ -data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{"content":"Test chat streaming response"},"logprobs":null,"finish_reason":null}]} - -data: {"id":"chatcmpl-96fqQVHGjG9Yzs4ZMB1K6nfy2oEoo","object":"chat.completion.chunk","created":1711377846,"model":"gpt-4-0125-preview","system_fingerprint":"fp_a7daf7c51e","choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]} - -data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json deleted file mode 100644 index b601bac8b55b..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_test_response.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "id": "response-id", - "object": "chat.completion", - "created": 1704208954, - "model": "gpt-4", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": "Test chat response" - }, - "finish_reason": "stop" - } - ], - "usage": { - "prompt_tokens": 55, - "completion_tokens": 100, - "total_tokens": 155 - }, - "system_fingerprint": null -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt deleted file mode 100644 index 5e17403da9fc..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_streaming_test_response.txt +++ /dev/null @@ -1 +0,0 @@ -data: {"id":"response-id","model":"","created":1684304924,"object":"chat.completion","choices":[{"index":0,"messages":[{"delta":{"role":"assistant","content":"Test chat with data streaming response"},"end_turn":false}]}]} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json deleted file mode 100644 index 40d769dac8a7..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/chat_completion_with_data_test_response.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "id": "response-id", - "model": "", - "created": 1684304924, - "object": "chat.completion", - "choices": [ - { - "index": 0, - "messages": [ - { - "role": "tool", - "content": "{\"citations\": [{\"content\": \"\\nAzure AI services are cloud-based artificial intelligence (AI) services...\", \"id\": null, \"title\": \"What is Azure AI services\", \"filepath\": null, \"url\": null, \"metadata\": {\"chunking\": \"original document size=250. Scores=0.4314117431640625 and 1.72564697265625.Org Highlight count=4.\"}, \"chunk_id\": \"0\"}], \"intent\": \"[\\\"Learn about Azure AI services.\\\"]\"}", - "end_turn": false - }, - { - "role": "assistant", - "content": "Test chat with data response", - "end_turn": true - } - ] - } - ], - "usage": { - "prompt_tokens": 55, - "completion_tokens": 100, - "total_tokens": 155 - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_multiple_function_calls_test_response.json deleted file mode 100644 index eb695f292c96..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_multiple_function_calls_test_response.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "id": "response-id", - "object": "chat.completion", - "created": 1699896916, - "model": "gpt-3.5-turbo-0613", - "choices": [ - { - "index": 0, - "message": { - "role": "assistant", - "content": null, - "tool_calls": [ - { - "id": "tool-call-id-1", - "type": "function", - "function": { - "name": "MyPlugin-Function1", - "arguments": "{\n\"parameter\": \"function1-value\"\n}" - } - }, - { - "id": "tool-call-id-2", - "type": "function", - "function": { - "name": "MyPlugin-Function2", - "arguments": "{\n\"parameter\": \"function2-value\"\n}" - } - } - ] - }, - "logprobs": null, - "finish_reason": "tool_calls" - } - ], - "usage": { - "prompt_tokens": 82, - "completion_tokens": 17, - "total_tokens": 99 - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_streaming_multiple_function_calls_test_response.txt deleted file mode 100644 index 0e26da41d32b..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/filters_streaming_multiple_function_calls_test_response.txt +++ /dev/null @@ -1,5 +0,0 @@ -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":0,"id":"tool-call-id-1","type":"function","function":{"name":"MyPlugin-Function1","arguments":"{\n\"parameter\": \"function1-value\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: {"id":"response-id","object":"chat.completion.chunk","created":1704212243,"model":"gpt-4","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"Test chat streaming response","tool_calls":[{"index":1,"id":"tool-call-id-2","type":"function","function":{"name":"MyPlugin-Function2","arguments":"{\n\"parameter\": \"function2-value\"\n}"}}]},"finish_reason":"tool_calls"}]} - -data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt deleted file mode 100644 index a511ea446236..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_streaming_test_response.txt +++ /dev/null @@ -1,3 +0,0 @@ -data: {"id":"response-id","object":"text_completion","created":1646932609,"model":"ada","choices":[{"text":"Test chat streaming response","index":0,"logprobs":null,"finish_reason":"length"}],"usage":{"prompt_tokens":55,"completion_tokens":100,"total_tokens":155}} - -data: [DONE] diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json deleted file mode 100644 index 540229437440..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TestData/text_completion_test_response.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "id": "response-id", - "object": "text_completion", - "created": 1646932609, - "model": "ada", - "choices": [ - { - "text": "Test chat response", - "index": 0, - "logprobs": null, - "finish_reason": "length" - } - ], - "usage": { - "prompt_tokens": 55, - "completion_tokens": 100, - "total_tokens": 155 - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs deleted file mode 100644 index 640280830ba2..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/AzureOpenAITextEmbeddingGenerationServiceTests.cs +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextEmbedding; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAITextEmbeddingGenerationServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAITextEmbeddingGenerationServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAITextEmbeddingGenerationService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextEmbeddingGenerationService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new AzureOpenAITextEmbeddingGenerationService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextEmbeddingGenerationService("deployment", client, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GenerateEmbeddingsForEmptyDataReturnsEmptyResultAsync() - { - // Arrange - var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - - // Act - var result = await service.GenerateEmbeddingsAsync([]); - - // Assert - Assert.Empty(result); - } - - [Fact] - public async Task GenerateEmbeddingsWithEmptyResponseThrowsExceptionAsync() - { - // Arrange - var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "object": "list", - "data": [], - "model": "model-id" - } - """, Encoding.UTF8, "application/json") - }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GenerateEmbeddingsAsync(["test"])); - Assert.Equal("Expected 1 text embedding(s), but received 0", exception.Message); - } - - [Fact] - public async Task GenerateEmbeddingsByDefaultWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAITextEmbeddingGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = this.SuccessfulResponse; - - // Act - var result = await service.GenerateEmbeddingsAsync(["test"]); - - // Assert - Assert.Single(result); - - var memory = result[0]; - - Assert.Equal(0.018990106880664825, memory.Span[0]); - Assert.Equal(-0.0073809814639389515, memory.Span[1]); - } - - [Fact] - public async Task GenerateEmbeddingsWithDimensionsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAITextEmbeddingGenerationService( - "deployment-name", - "https://endpoint", - "api-key", - "model-id", - this._httpClient, - dimensions: 256); - - this._messageHandlerStub.ResponseToReturn = this.SuccessfulResponse; - - // Act - await service.GenerateEmbeddingsAsync(["test"]); - - var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - var optionsJson = JsonSerializer.Deserialize(requestContent); - - // Assert - Assert.Equal(256, optionsJson.GetProperty("dimensions").GetInt32()); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - #region private - - private HttpResponseMessage SuccessfulResponse - => new(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "object": "list", - "data": [ - { - "object": "embedding", - "embedding": [ - 0.018990106880664825, - -0.0073809814639389515 - ], - "index": 0 - } - ], - "model": "model-id" - } - """, Encoding.UTF8, "application/json") - }; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs deleted file mode 100644 index 76638ae9cc9f..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextEmbedding/OpenAITextEmbeddingGenerationServiceTests.cs +++ /dev/null @@ -1,164 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextEmbedding; - -/// -/// Unit tests for class. -/// -public sealed class OpenAITextEmbeddingGenerationServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAITextEmbeddingGenerationServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new OpenAITextEmbeddingGenerationService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextEmbeddingGenerationService("model-id", client); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GenerateEmbeddingsForEmptyDataReturnsEmptyResultAsync() - { - // Arrange - var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); - - // Act - var result = await service.GenerateEmbeddingsAsync([]); - - // Assert - Assert.Empty(result); - } - - [Fact] - public async Task GenerateEmbeddingsWithEmptyResponseThrowsExceptionAsync() - { - // Arrange - var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "object": "list", - "data": [], - "model": "model-id" - } - """, Encoding.UTF8, "application/json") - }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GenerateEmbeddingsAsync(["test"])); - Assert.Equal("Expected 1 text embedding(s), but received 0", exception.Message); - } - - [Fact] - public async Task GenerateEmbeddingsByDefaultWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient); - this._messageHandlerStub.ResponseToReturn = this.SuccessfulResponse; - - // Act - var result = await service.GenerateEmbeddingsAsync(["test"]); - - // Assert - Assert.Single(result); - - var memory = result[0]; - - Assert.Equal(0.018990106880664825, memory.Span[0]); - Assert.Equal(-0.0073809814639389515, memory.Span[1]); - } - - [Fact] - public async Task GenerateEmbeddingsWithDimensionsWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAITextEmbeddingGenerationService("model-id", "api-key", "organization", this._httpClient, dimensions: 256); - this._messageHandlerStub.ResponseToReturn = this.SuccessfulResponse; - - // Act - await service.GenerateEmbeddingsAsync(["test"]); - - var requestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); - var optionsJson = JsonSerializer.Deserialize(requestContent); - - // Assert - Assert.Equal(256, optionsJson.GetProperty("dimensions").GetInt32()); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - #region private - - private HttpResponseMessage SuccessfulResponse - => new(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "object": "list", - "data": [ - { - "object": "embedding", - "embedding": [ - 0.018990106880664825, - -0.0073809814639389515 - ], - "index": 0 - } - ], - "model": "model-id" - } - """, Encoding.UTF8, "application/json") - }; - - #endregion -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs deleted file mode 100644 index d20bb502e23d..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/AzureOpenAITextGenerationServiceTests.cs +++ /dev/null @@ -1,210 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextGeneration; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAITextGenerationServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAITextGenerationServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAITextGenerationService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextGenerationService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new AzureOpenAITextGenerationService("deployment", client, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextGenerationService("deployment", client, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GetTextContentsWithEmptyChoicesThrowsExceptionAsync() - { - // Arrange - var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent("{\"id\":\"response-id\",\"object\":\"text_completion\",\"created\":1646932609,\"model\":\"ada\",\"choices\":[]}") - }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetTextContentsAsync("Prompt")); - - Assert.Equal("Text completions not found", exception.Message); - } - - [Theory] - [InlineData(0)] - [InlineData(129)] - public async Task GetTextContentsWithInvalidResultsPerPromptValueThrowsExceptionAsync(int resultsPerPrompt) - { - // Arrange - var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings { ResultsPerPrompt = resultsPerPrompt }; - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => service.GetTextContentsAsync("Prompt", settings)); - - Assert.Contains("The value must be in range between", exception.Message, StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task GetTextContentsHandlesSettingsCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - var settings = new OpenAIPromptExecutionSettings - { - MaxTokens = 123, - Temperature = 0.6, - TopP = 0.5, - FrequencyPenalty = 1.6, - PresencePenalty = 1.2, - ResultsPerPrompt = 5, - TokenSelectionBiases = new Dictionary { { 2, 3 } }, - StopSequences = ["stop_sequence"], - TopLogprobs = 5 - }; - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) - }; - - // Act - var result = await service.GetTextContentsAsync("Prompt", settings); - - // Assert - var requestContent = this._messageHandlerStub.RequestContent; - - Assert.NotNull(requestContent); - - var content = JsonSerializer.Deserialize(Encoding.UTF8.GetString(requestContent)); - - Assert.Equal("Prompt", content.GetProperty("prompt")[0].GetString()); - Assert.Equal(123, content.GetProperty("max_tokens").GetInt32()); - Assert.Equal(0.6, content.GetProperty("temperature").GetDouble()); - Assert.Equal(0.5, content.GetProperty("top_p").GetDouble()); - Assert.Equal(1.6, content.GetProperty("frequency_penalty").GetDouble()); - Assert.Equal(1.2, content.GetProperty("presence_penalty").GetDouble()); - Assert.Equal(5, content.GetProperty("n").GetInt32()); - Assert.Equal(5, content.GetProperty("best_of").GetInt32()); - Assert.Equal(3, content.GetProperty("logit_bias").GetProperty("2").GetInt32()); - Assert.Equal("stop_sequence", content.GetProperty("stop")[0].GetString()); - Assert.Equal(5, content.GetProperty("logprobs").GetInt32()); - } - - [Fact] - public async Task GetTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) - }; - - // Act - var result = await service.GetTextContentsAsync("Prompt"); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Text); - - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - } - - [Fact] - public async Task GetStreamingTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new AzureOpenAITextGenerationService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("text_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) - { - Assert.Equal("Test chat streaming response", chunk.Text); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs deleted file mode 100644 index b8d804c21b5d..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextGeneration/OpenAITextGenerationServiceTests.cs +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextGeneration; - -/// -/// Unit tests for class. -/// -public sealed class OpenAITextGenerationServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAITextGenerationServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAITextGenerationService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextGenerationService("model-id", "api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithOpenAIClientWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var client = new OpenAIClient("key"); - var service = includeLoggerFactory ? - new OpenAITextGenerationService("model-id", client, loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextGenerationService("model-id", client); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Fact] - public async Task GetTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAITextGenerationService("model-id", "api-key", "organization", this._httpClient); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StringContent(OpenAITestHelper.GetTestResponse("text_completion_test_response.json")) - }; - - // Act - var result = await service.GetTextContentsAsync("Prompt"); - - // Assert - Assert.True(result.Count > 0); - Assert.Equal("Test chat response", result[0].Text); - - var usage = result[0].Metadata?["Usage"] as CompletionsUsage; - - Assert.NotNull(usage); - Assert.Equal(55, usage.PromptTokens); - Assert.Equal(100, usage.CompletionTokens); - Assert.Equal(155, usage.TotalTokens); - } - - [Fact] - public async Task GetStreamingTextContentsWorksCorrectlyAsync() - { - // Arrange - var service = new OpenAITextGenerationService("model-id", "api-key", "organization", this._httpClient); - using var stream = new MemoryStream(Encoding.UTF8.GetBytes(OpenAITestHelper.GetTestResponse("text_completion_streaming_test_response.txt"))); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act & Assert - await foreach (var chunk in service.GetStreamingTextContentsAsync("Prompt")) - { - Assert.Equal("Test chat streaming response", chunk.Text); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/AzureOpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/AzureOpenAITextToAudioServiceTests.cs deleted file mode 100644 index baa11a265f0a..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/AzureOpenAITextToAudioServiceTests.cs +++ /dev/null @@ -1,130 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToAudio; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAITextToAudioServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAITextToAudioServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - Assert.Equal("deployment-name", service.Attributes["DeploymentName"]); - } - - [Theory] - [MemberData(nameof(ExecutionSettings))] - public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType) - { - // Arrange - var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - await using var stream = new MemoryStream(new byte[] { 0x00, 0x00, 0xFF, 0x7F }); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var exception = await Record.ExceptionAsync(() => service.GetAudioContentsAsync("Some text", settings)); - - // Assert - Assert.NotNull(exception); - Assert.IsType(expectedExceptionType, exception); - } - - [Fact] - public async Task GetAudioContentByDefaultWorksCorrectlyAsync() - { - // Arrange - var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; - - var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice")); - - // Assert - var audioData = result[0].Data!.Value; - Assert.False(audioData.IsEmpty); - Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); - } - - [Theory] - [InlineData(true, "http://local-endpoint")] - [InlineData(false, "https://endpoint")] - public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAddress, string expectedBaseAddress) - { - // Arrange - var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; - - if (useHttpClientBaseAddress) - { - this._httpClient.BaseAddress = new Uri("http://local-endpoint"); - } - - var service = new AzureOpenAITextToAudioService("deployment-name", "https://endpoint", "api-key", "model-id", this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice")); - - // Assert - Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - public static TheoryData ExecutionSettings => new() - { - { new OpenAITextToAudioExecutionSettings(""), typeof(ArgumentException) }, - }; -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioExecutionSettingsTests.cs deleted file mode 100644 index ea1b1adafae5..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioExecutionSettingsTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Text.Json; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToAudio; - -/// -/// Unit tests for class. -/// -public sealed class OpenAITextToAudioExecutionSettingsTests -{ - [Fact] - public void ItReturnsDefaultSettingsWhenSettingsAreNull() - { - Assert.NotNull(OpenAITextToAudioExecutionSettings.FromExecutionSettings(null)); - } - - [Fact] - public void ItReturnsValidOpenAITextToAudioExecutionSettings() - { - // Arrange - var textToAudioSettings = new OpenAITextToAudioExecutionSettings("voice") - { - ModelId = "model_id", - ResponseFormat = "mp3", - Speed = 1.0f - }; - - // Act - var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(textToAudioSettings); - - // Assert - Assert.Same(textToAudioSettings, settings); - } - - [Fact] - public void ItCreatesOpenAIAudioToTextExecutionSettingsFromJson() - { - // Arrange - var json = """ - { - "model_id": "model_id", - "voice": "voice", - "response_format": "mp3", - "speed": 1.2 - } - """; - - var executionSettings = JsonSerializer.Deserialize(json); - - // Act - var settings = OpenAITextToAudioExecutionSettings.FromExecutionSettings(executionSettings); - - // Assert - Assert.NotNull(settings); - Assert.Equal("model_id", settings.ModelId); - Assert.Equal("voice", settings.Voice); - Assert.Equal("mp3", settings.ResponseFormat); - Assert.Equal(1.2f, settings.Speed); - } - - [Fact] - public void ItClonesAllProperties() - { - var textToAudioSettings = new OpenAITextToAudioExecutionSettings() - { - ModelId = "some_model", - ResponseFormat = "some_format", - Speed = 3.14f, - Voice = "something" - }; - - var clone = (OpenAITextToAudioExecutionSettings)textToAudioSettings.Clone(); - Assert.NotSame(textToAudioSettings, clone); - - Assert.Equal("some_model", clone.ModelId); - Assert.Equal("some_format", clone.ResponseFormat); - Assert.Equal(3.14f, clone.Speed); - Assert.Equal("something", clone.Voice); - } - - [Fact] - public void ItFreezesAndPreventsMutation() - { - var textToAudioSettings = new OpenAITextToAudioExecutionSettings() - { - ModelId = "some_model", - ResponseFormat = "some_format", - Speed = 3.14f, - Voice = "something" - }; - - textToAudioSettings.Freeze(); - Assert.True(textToAudioSettings.IsFrozen); - - Assert.Throws(() => textToAudioSettings.ModelId = "new_model"); - Assert.Throws(() => textToAudioSettings.ResponseFormat = "some_format"); - Assert.Throws(() => textToAudioSettings.Speed = 3.14f); - Assert.Throws(() => textToAudioSettings.Voice = "something"); - - textToAudioSettings.Freeze(); // idempotent - Assert.True(textToAudioSettings.IsFrozen); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioServiceTests.cs deleted file mode 100644 index 588616f54348..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToAudio/OpenAITextToAudioServiceTests.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToAudio; - -/// -/// Unit tests for class. -/// -public sealed class OpenAITextToAudioServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAITextToAudioServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAITextToAudioService("model-id", "api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextToAudioService("model-id", "api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [MemberData(nameof(ExecutionSettings))] - public async Task GetAudioContentWithInvalidSettingsThrowsExceptionAsync(OpenAITextToAudioExecutionSettings? settings, Type expectedExceptionType) - { - // Arrange - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); - await using var stream = new MemoryStream(new byte[] { 0x00, 0x00, 0xFF, 0x7F }); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var exception = await Record.ExceptionAsync(() => service.GetAudioContentsAsync("Some text", settings)); - - // Assert - Assert.NotNull(exception); - Assert.IsType(expectedExceptionType, exception); - } - - [Fact] - public async Task GetAudioContentByDefaultWorksCorrectlyAsync() - { - // Arrange - var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; - - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice")); - - // Assert - var audioData = result[0].Data!.Value; - Assert.False(audioData.IsEmpty); - Assert.True(audioData.Span.SequenceEqual(expectedByteArray)); - } - - [Theory] - [InlineData(true, "http://local-endpoint")] - [InlineData(false, "https://api.openai.com")] - public async Task GetAudioContentUsesValidBaseUrlAsync(bool useHttpClientBaseAddress, string expectedBaseAddress) - { - // Arrange - var expectedByteArray = new byte[] { 0x00, 0x00, 0xFF, 0x7F }; - - if (useHttpClientBaseAddress) - { - this._httpClient.BaseAddress = new Uri("http://local-endpoint"); - } - - var service = new OpenAITextToAudioService("model-id", "api-key", "organization", this._httpClient); - await using var stream = new MemoryStream(expectedByteArray); - - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(HttpStatusCode.OK) - { - Content = new StreamContent(stream) - }; - - // Act - var result = await service.GetAudioContentsAsync("Some text", new OpenAITextToAudioExecutionSettings("voice")); - - // Assert - Assert.StartsWith(expectedBaseAddress, this._messageHandlerStub.RequestUri!.AbsoluteUri, StringComparison.InvariantCulture); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } - - public static TheoryData ExecutionSettings => new() - { - { new OpenAITextToAudioExecutionSettings(""), typeof(ArgumentException) }, - }; -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/AzureOpenAITextToImageTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/AzureOpenAITextToImageTests.cs deleted file mode 100644 index 084fa923b2ce..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/AzureOpenAITextToImageTests.cs +++ /dev/null @@ -1,174 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Azure.Core; -using Azure.Core.Pipeline; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Services; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToImage; - -/// -/// Unit tests for class. -/// -public sealed class AzureOpenAITextToImageServiceTests : IDisposable -{ - private readonly MultipleHttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public AzureOpenAITextToImageServiceTests() - { - this._messageHandlerStub = new MultipleHttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - - var mockLogger = new Mock(); - - mockLogger.Setup(l => l.IsEnabled(It.IsAny())).Returns(true); - - this._mockLoggerFactory.Setup(l => l.CreateLogger(It.IsAny())).Returns(mockLogger.Object); - } - - [Fact] - public async Task ItSupportsOpenAIClientInjectionAsync() - { - // Arrange - using var messageHandlerStub = new HttpMessageHandlerStub(); - using var httpClient = new HttpClient(messageHandlerStub, false); - messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "created": 1702575371, - "data": [ - { - "revised_prompt": "A photo capturing the diversity of the Earth's landscapes.", - "url": "https://dalleprodsec.blob.core.windows.net/private/images/0f20c621-7eb0-449d-87fd-8dd2a3a15fbe/generated_00.png?se=2023-12-15T17%3A36%3A25Z&sig=jd2%2Fa8jOM9NmclrUbOLdRgAxcFDFPezOpG%2BSF82d7zM%3D&ske=2023-12-20T10%3A10%3A28Z&skoid=e52d5ed7-0657-4f62-bc12-7e5dbb260a96&sks=b&skt=2023-12-13T10%3A10%3A28Z&sktid=33e01921-4d64-4f8c-a055-5bdaffd5e33d&skv=2020-10-02&sp=r&spr=https&sr=b&sv=2020-10-02" - } - ] - } - """, Encoding.UTF8, "application/json") - }; - var clientOptions = new OpenAIClientOptions - { - Transport = new HttpClientTransport(httpClient), - }; - var openAIClient = new OpenAIClient(new Uri("https://az.com"), new Azure.AzureKeyCredential("NOKEY"), clientOptions); - - var textToImageCompletion = new AzureOpenAITextToImageService(deploymentName: "gpt-35-turbo", openAIClient, modelId: "gpt-3.5-turbo"); - - // Act - var result = await textToImageCompletion.GenerateImageAsync("anything", 1024, 1024); - - // Assert - Assert.NotNull(result); - } - - [Theory] - [InlineData(1024, 1024, null)] - [InlineData(1792, 1024, null)] - [InlineData(1024, 1792, null)] - [InlineData(512, 512, typeof(NotSupportedException))] - [InlineData(256, 256, typeof(NotSupportedException))] - [InlineData(123, 456, typeof(NotSupportedException))] - public async Task ItValidatesTheModelIdAsync(int width, int height, Type? expectedExceptionType) - { - // Arrange - using var messageHandlerStub = new HttpMessageHandlerStub(); - using var httpClient = new HttpClient(messageHandlerStub, false); - messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "created": 1702575371, - "data": [ - { - "revised_prompt": "A photo capturing the diversity of the Earth's landscapes.", - "url": "https://dalleprodsec.blob.core.windows.net/private/images/0f20c621-7eb0-449d-87fd-8dd2a3a15fbe/generated_00.png?se=2023-12-15T17%3A36%3A25Z&sig=jd2%2Fa8jOM9NmclrUbOLdRgAxcFDFPezOpG%2BSF82d7zM%3D&ske=2023-12-20T10%3A10%3A28Z&skoid=e52d5ed7-0657-4f62-bc12-7e5dbb260a96&sks=b&skt=2023-12-13T10%3A10%3A28Z&sktid=33e01921-4d64-4f8c-a055-5bdaffd5e33d&skv=2020-10-02&sp=r&spr=https&sr=b&sv=2020-10-02" - } - ] - } - """, Encoding.UTF8, "application/json") - }; - - var textToImageCompletion = new AzureOpenAITextToImageService(deploymentName: "gpt-35-turbo", modelId: "gpt-3.5-turbo", endpoint: "https://az.com", apiKey: "NOKEY", httpClient: httpClient); - - if (expectedExceptionType is not null) - { - await Assert.ThrowsAsync(expectedExceptionType, () => textToImageCompletion.GenerateImageAsync("anything", width, height)); - } - else - { - // Act - var result = await textToImageCompletion.GenerateImageAsync("anything", width, height); - - // Assert - Assert.NotNull(result); - } - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithApiKeyWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAITextToImageService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextToImageService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWithTokenCredentialWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var credentials = DelegatedTokenCredential.Create((_, _) => new AccessToken()); - var service = includeLoggerFactory ? - new AzureOpenAITextToImageService("deployment", "https://endpoint", credentials, "model-id", loggerFactory: this._mockLoggerFactory.Object) : - new AzureOpenAITextToImageService("deployment", "https://endpoint", credentials, "model-id"); - - // Assert - Assert.NotNull(service); - Assert.Equal("model-id", service.Attributes["ModelId"]); - } - - [Theory] - [InlineData("gpt-35-turbo", "gpt-3.5-turbo")] - [InlineData("gpt-35-turbo", null)] - [InlineData("gpt-4-turbo", "gpt-4")] - public void ItHasPropertiesAsDefined(string deploymentName, string? modelId) - { - var service = new AzureOpenAITextToImageService(deploymentName, "https://az.com", "NOKEY", modelId); - Assert.Contains(AzureOpenAITextToImageService.DeploymentNameKey, service.Attributes); - Assert.Equal(deploymentName, service.Attributes[AzureOpenAITextToImageService.DeploymentNameKey]); - - if (modelId is null) - { - return; - } - - Assert.Contains(AIServiceExtensions.ModelIdKey, service.Attributes); - Assert.Equal(modelId, service.Attributes[AIServiceExtensions.ModelIdKey]); - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs deleted file mode 100644 index 1f31ec076edd..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/TextToImage/OpenAITextToImageServiceTests.cs +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net.Http; -using System.Text; -using System.Threading.Tasks; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI.TextToImage; - -/// -/// Unit tests for class. -/// -public sealed class OpenAITextToImageServiceTests : IDisposable -{ - private readonly HttpMessageHandlerStub _messageHandlerStub; - private readonly HttpClient _httpClient; - private readonly Mock _mockLoggerFactory; - - public OpenAITextToImageServiceTests() - { - this._messageHandlerStub = new HttpMessageHandlerStub(); - this._httpClient = new HttpClient(this._messageHandlerStub, false); - this._mockLoggerFactory = new Mock(); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void ConstructorWorksCorrectly(bool includeLoggerFactory) - { - // Arrange & Act - var service = includeLoggerFactory ? - new OpenAITextToImageService("api-key", "organization", loggerFactory: this._mockLoggerFactory.Object) : - new OpenAITextToImageService("api-key", "organization"); - - // Assert - Assert.NotNull(service); - Assert.Equal("organization", service.Attributes["Organization"]); - Assert.False(service.Attributes.ContainsKey("ModelId")); - } - - [Theory] - [InlineData(123, 456, true)] - [InlineData(256, 512, true)] - [InlineData(256, 256, false)] - [InlineData(512, 512, false)] - [InlineData(1024, 1024, false)] - public async Task GenerateImageWorksCorrectlyAsync(int width, int height, bool expectedException) - { - // Arrange - var service = new OpenAITextToImageService("api-key", "organization", "dall-e-3", this._httpClient); - Assert.Equal("dall-e-3", service.Attributes["ModelId"]); - this._messageHandlerStub.ResponseToReturn = new HttpResponseMessage(System.Net.HttpStatusCode.OK) - { - Content = new StringContent(""" - { - "created": 1702575371, - "data": [ - { - "url": "https://image-url" - } - ] - } - """, Encoding.UTF8, "application/json") - }; - - // Act & Assert - if (expectedException) - { - await Assert.ThrowsAsync(() => service.GenerateImageAsync("description", width, height)); - } - else - { - var result = await service.GenerateImageAsync("description", width, height); - - Assert.Equal("https://image-url", result); - } - } - - public void Dispose() - { - this._httpClient.Dispose(); - this._messageHandlerStub.Dispose(); - } -} diff --git a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs deleted file mode 100644 index d39480ebfe8d..000000000000 --- a/dotnet/src/Connectors/Connectors.UnitTests/OpenAI/ToolCallBehaviorTests.cs +++ /dev/null @@ -1,249 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Collections.Generic; -using System.Linq; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Xunit; -using static Microsoft.SemanticKernel.Connectors.OpenAI.ToolCallBehavior; - -namespace SemanticKernel.Connectors.UnitTests.OpenAI; - -/// -/// Unit tests for -/// -public sealed class ToolCallBehaviorTests -{ - [Fact] - public void EnableKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - var behavior = ToolCallBehavior.EnableKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(0, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void AutoInvokeKernelFunctionsReturnsCorrectKernelFunctionsInstance() - { - // Arrange & Act - const int DefaultMaximumAutoInvokeAttempts = 128; - var behavior = ToolCallBehavior.AutoInvokeKernelFunctions; - - // Assert - Assert.IsType(behavior); - Assert.Equal(DefaultMaximumAutoInvokeAttempts, behavior.MaximumAutoInvokeAttempts); - } - - [Fact] - public void EnableFunctionsReturnsEnabledFunctionsInstance() - { - // Arrange & Act - List functions = [new("Plugin", "Function", "description", [], null)]; - var behavior = ToolCallBehavior.EnableFunctions(functions); - - // Assert - Assert.IsType(behavior); - } - - [Fact] - public void RequireFunctionReturnsRequiredFunctionInstance() - { - // Arrange & Act - var behavior = ToolCallBehavior.RequireFunction(new("Plugin", "Function", "description", [], null)); - - // Assert - Assert.IsType(behavior); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithNullKernelDoesNotAddTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); - - // Act - kernelFunctions.ConfigureOptions(null, chatCompletionsOptions); - - // Assert - Assert.Empty(chatCompletionsOptions.Tools); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var kernel = Kernel.CreateBuilder().Build(); - - // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); - - // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); - } - - [Fact] - public void KernelFunctionsConfigureOptionsWithFunctionsAddsTools() - { - // Arrange - var kernelFunctions = new KernelFunctions(autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var kernel = Kernel.CreateBuilder().Build(); - - var plugin = this.GetTestPlugin(); - - kernel.Plugins.Add(plugin); - - // Act - kernelFunctions.ConfigureOptions(kernel, chatCompletionsOptions); - - // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); - - this.AssertTools(chatCompletionsOptions); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithoutFunctionsDoesNotAddTools() - { - // Arrange - var enabledFunctions = new EnabledFunctions([], autoInvoke: false); - var chatCompletionsOptions = new ChatCompletionsOptions(); - - // Act - enabledFunctions.ConfigureOptions(null, chatCompletionsOptions); - - // Assert - Assert.Null(chatCompletionsOptions.ToolChoice); - Assert.Empty(chatCompletionsOptions.Tools); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(null, chatCompletionsOptions)); - Assert.Equal($"Auto-invocation with {nameof(EnabledFunctions)} is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void EnabledFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var functions = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions)); - Assert.Equal($"The specified {nameof(EnabledFunctions)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); - } - - [Theory] - [InlineData(true)] - [InlineData(false)] - public void EnabledFunctionsConfigureOptionsWithKernelAndPluginsAddsTools(bool autoInvoke) - { - // Arrange - var plugin = this.GetTestPlugin(); - var functions = plugin.GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()); - var enabledFunctions = new EnabledFunctions(functions, autoInvoke); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var kernel = Kernel.CreateBuilder().Build(); - - kernel.Plugins.Add(plugin); - - // Act - enabledFunctions.ConfigureOptions(kernel, chatCompletionsOptions); - - // Assert - Assert.Equal(ChatCompletionsToolChoice.Auto, chatCompletionsOptions.ToolChoice); - - this.AssertTools(chatCompletionsOptions); - } - - [Fact] - public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndNullKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); - - // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(null, chatCompletionsOptions)); - Assert.Equal($"Auto-invocation with {nameof(RequiredFunction)} is not supported when no kernel is provided.", exception.Message); - } - - [Fact] - public void RequiredFunctionsConfigureOptionsWithAutoInvokeAndEmptyKernelThrowsException() - { - // Arrange - var function = this.GetTestPlugin().GetFunctionsMetadata().Select(function => function.ToOpenAIFunction()).First(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var kernel = Kernel.CreateBuilder().Build(); - - // Act & Assert - var exception = Assert.Throws(() => requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions)); - Assert.Equal($"The specified {nameof(RequiredFunction)} function MyPlugin-MyFunction is not available in the kernel.", exception.Message); - } - - [Fact] - public void RequiredFunctionConfigureOptionsAddsTools() - { - // Arrange - var plugin = this.GetTestPlugin(); - var function = plugin.GetFunctionsMetadata()[0].ToOpenAIFunction(); - var chatCompletionsOptions = new ChatCompletionsOptions(); - var requiredFunction = new RequiredFunction(function, autoInvoke: true); - var kernel = new Kernel(); - kernel.Plugins.Add(plugin); - - // Act - requiredFunction.ConfigureOptions(kernel, chatCompletionsOptions); - - // Assert - Assert.NotNull(chatCompletionsOptions.ToolChoice); - - this.AssertTools(chatCompletionsOptions); - } - - private KernelPlugin GetTestPlugin() - { - var function = KernelFunctionFactory.CreateFromMethod( - (string parameter1, string parameter2) => "Result1", - "MyFunction", - "Test Function", - [new KernelParameterMetadata("parameter1"), new KernelParameterMetadata("parameter2")], - new KernelReturnParameterMetadata { ParameterType = typeof(string), Description = "Function Result" }); - - return KernelPluginFactory.CreateFromFunctions("MyPlugin", [function]); - } - - private void AssertTools(ChatCompletionsOptions chatCompletionsOptions) - { - Assert.Single(chatCompletionsOptions.Tools); - - var tool = chatCompletionsOptions.Tools[0] as ChatCompletionsFunctionToolDefinition; - - Assert.NotNull(tool); - - Assert.Equal("MyPlugin-MyFunction", tool.Name); - Assert.Equal("Test Function", tool.Description); - Assert.Equal("{\"type\":\"object\",\"required\":[],\"properties\":{\"parameter1\":{\"type\":\"string\"},\"parameter2\":{\"type\":\"string\"}}}", tool.Parameters.ToString()); - } -} diff --git a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj index a3f5a93a7013..6fdfb01ffa75 100644 --- a/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj +++ b/dotnet/src/Experimental/Orchestration.Flow.IntegrationTests/Experimental.Orchestration.Flow.IntegrationTests.csproj @@ -28,7 +28,7 @@ - + diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj b/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj index b730d1c27025..750e678395f2 100644 --- a/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj +++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj @@ -27,8 +27,8 @@ + - diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTest.cs b/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTest.cs index 308f87d40464..cec3b63c0fd9 100644 --- a/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTest.cs +++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/PromptyTest.cs @@ -60,8 +60,8 @@ public void ChatPromptyShouldSupportCreatingOpenAIExecutionSettings() // Assert Assert.NotNull(executionSettings); Assert.Equal("gpt-35-turbo", executionSettings.ModelId); - Assert.Equal(1.0, executionSettings.Temperature); - Assert.Equal(1.0, executionSettings.TopP); + Assert.Null(executionSettings.Temperature); + Assert.Null(executionSettings.TopP); Assert.Null(executionSettings.StopSequences); Assert.Null(executionSettings.ResponseFormat); Assert.Null(executionSettings.TokenSelectionBiases); diff --git a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj index 50f58e947499..9564032ae126 100644 --- a/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj +++ b/dotnet/src/Functions/Functions.UnitTests/Functions.UnitTests.csproj @@ -52,7 +52,7 @@ - + diff --git a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs index c9f082b329a3..ea83585baa50 100644 --- a/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs +++ b/dotnet/src/Functions/Functions.UnitTests/OpenApi/RestApiOperationTests.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Plugins.OpenApi; using Microsoft.SemanticKernel.TextGeneration; @@ -568,7 +569,7 @@ public void ItBuildsServicesIntoKernel() { var builder = Kernel.CreateBuilder() .AddOpenAIChatCompletion(modelId: "abcd", apiKey: "efg", serviceId: "openai") - .AddAzureOpenAITextGeneration(deploymentName: "hijk", modelId: "qrs", endpoint: "https://lmnop", apiKey: "tuv", serviceId: "azureopenai"); + .AddAzureOpenAIChatCompletion(deploymentName: "hijk", modelId: "qrs", endpoint: "https://lmnop", apiKey: "tuv", serviceId: "azureopenai"); builder.Services.AddSingleton(CultureInfo.InvariantCulture); builder.Services.AddSingleton(CultureInfo.CurrentCulture); @@ -577,10 +578,10 @@ public void ItBuildsServicesIntoKernel() Kernel kernel = builder.Build(); Assert.IsType(kernel.GetRequiredService("openai")); - Assert.IsType(kernel.GetRequiredService("azureopenai")); + Assert.IsType(kernel.GetRequiredService("azureopenai")); Assert.Equal(2, kernel.GetAllServices().Count()); - Assert.Single(kernel.GetAllServices()); + Assert.Equal(2, kernel.GetAllServices().Count()); Assert.Equal(3, kernel.GetAllServices().Count()); } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/AIServiceType.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/AIServiceType.cs deleted file mode 100644 index b09a7a5ef635..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/AIServiceType.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -/// -/// Enumeration to run integration tests for different AI services -/// -public enum AIServiceType -{ - /// - /// Open AI service - /// - OpenAI = 0, - - /// - /// Azure Open AI service - /// - AzureOpenAI = 1 -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs deleted file mode 100644 index bf102a517e52..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/ChatHistoryTests.cs +++ /dev/null @@ -1,149 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.ComponentModel; -using System.Linq; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class ChatHistoryTests(ITestOutputHelper output) : IDisposable -{ - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); - private readonly XunitLogger _logger = new(output); - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - private static readonly JsonSerializerOptions s_jsonOptionsCache = new() { WriteIndented = true }; - - [Fact] - public async Task ItSerializesAndDeserializesChatHistoryAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - builder.Plugins.AddFromType(); - var kernel = builder.Build(); - - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - ChatHistory history = []; - - // Act - history.AddUserMessage("Make me a special poem"); - var historyBeforeJson = JsonSerializer.Serialize(history.ToList(), s_jsonOptionsCache); - var service = kernel.GetRequiredService(); - ChatMessageContent result = await service.GetChatMessageContentAsync(history, settings, kernel); - history.AddUserMessage("Ok thank you"); - - ChatMessageContent resultOriginalWorking = await service.GetChatMessageContentAsync(history, settings, kernel); - var historyJson = JsonSerializer.Serialize(history, s_jsonOptionsCache); - var historyAfterSerialization = JsonSerializer.Deserialize(historyJson); - var exception = await Record.ExceptionAsync(() => service.GetChatMessageContentAsync(historyAfterSerialization!, settings, kernel)); - - // Assert - Assert.Null(exception); - } - - [Fact] - public async Task ItUsesChatSystemPromptFromSettingsAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - builder.Plugins.AddFromType(); - var kernel = builder.Build(); - - string systemPrompt = "You are batman. If asked who you are, say 'I am Batman!'"; - - OpenAIPromptExecutionSettings settings = new() { ChatSystemPrompt = systemPrompt }; - ChatHistory history = []; - - // Act - history.AddUserMessage("Who are you?"); - var service = kernel.GetRequiredService(); - ChatMessageContent result = await service.GetChatMessageContentAsync(history, settings, kernel); - - // Assert - Assert.Contains("Batman", result.ToString(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task ItUsesChatSystemPromptFromChatHistoryAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - builder.Plugins.AddFromType(); - var kernel = builder.Build(); - - string systemPrompt = "You are batman. If asked who you are, say 'I am Batman!'"; - - OpenAIPromptExecutionSettings settings = new(); - ChatHistory history = new(systemPrompt); - - // Act - history.AddUserMessage("Who are you?"); - var service = kernel.GetRequiredService(); - ChatMessageContent result = await service.GetChatMessageContentAsync(history, settings, kernel); - - // Assert - Assert.Contains("Batman", result.ToString(), StringComparison.OrdinalIgnoreCase); - } - - private void ConfigureAzureOpenAIChatAsText(IKernelBuilder kernelBuilder) - { - var azureOpenAIConfiguration = this._configuration.GetSection("Planners:AzureOpenAI").Get(); - - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); - Assert.NotNull(azureOpenAIConfiguration.ApiKey); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - Assert.NotNull(azureOpenAIConfiguration.ServiceId); - - kernelBuilder.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName, - modelId: azureOpenAIConfiguration.ChatModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey, - serviceId: azureOpenAIConfiguration.ServiceId); - } - - public class FakePlugin - { - [KernelFunction, Description("creates a special poem")] - public string CreateSpecialPoem() - { - return "ABCDE"; - } - } - - public void Dispose() - { - this.Dispose(true); - GC.SuppressFinalize(this); - } - - private void Dispose(bool disposing) - { - if (disposing) - { - this._logger.Dispose(); - } - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs deleted file mode 100644 index dd4a55f6cc2c..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.IO; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.AudioToText; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class OpenAIAudioToTextTests() -{ - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Fact(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - public async Task OpenAIAudioToTextTestAsync() - { - // Arrange - const string Filename = "test_audio.wav"; - - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIAudioToText").Get(); - Assert.NotNull(openAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddOpenAIAudioToText(openAIConfiguration.ModelId, openAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - await using Stream audio = File.OpenRead($"./TestData/{Filename}"); - var audioData = await BinaryData.FromStreamAsync(audio); - - // Act - var result = await service.GetTextContentAsync(new AudioContent(audioData, mimeType: "audio/wav"), new OpenAIAudioToTextExecutionSettings(Filename)); - - // Assert - Assert.Contains("The sun rises in the east and sets in the west.", result.Text, StringComparison.OrdinalIgnoreCase); - } - - [Fact(Skip = "Re-enable when Azure OpenAPI service is available.")] - public async Task AzureOpenAIAudioToTextTestAsync() - { - // Arrange - const string Filename = "test_audio.wav"; - - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAIAudioToText").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAIAudioToText( - azureOpenAIConfiguration.DeploymentName, - azureOpenAIConfiguration.Endpoint, - azureOpenAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - await using Stream audio = File.OpenRead($"./TestData/{Filename}"); - var audioData = await BinaryData.FromStreamAsync(audio); - - // Act - var result = await service.GetTextContentAsync(new AudioContent(audioData, mimeType: "audio/wav"), new OpenAIAudioToTextExecutionSettings(Filename)); - - // Assert - Assert.Contains("The sun rises in the east and sets in the west.", result.Text, StringComparison.OrdinalIgnoreCase); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs deleted file mode 100644 index 675661b76d83..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAICompletionTests.cs +++ /dev/null @@ -1,668 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Net.Http; -using System.Text; -using System.Text.Json; -using System.Threading; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience; -using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. - -public sealed class OpenAICompletionTests(ITestOutputHelper output) : IDisposable -{ - private const string InputParameterName = "input"; - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place Market")] - public async Task OpenAITestAsync(string prompt, string expectedAnswerContains) - { - // Arrange - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - this._kernelBuilder.Services.AddSingleton(this._logger); - Kernel target = this._kernelBuilder - .AddOpenAITextGeneration( - serviceId: openAIConfiguration.ServiceId, - modelId: openAIConfiguration.ModelId, - apiKey: openAIConfiguration.ApiKey) - .Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "ChatPlugin"); - - // Act - FunctionResult actual = await target.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); - - // Assert - Assert.Contains(expectedAnswerContains, actual.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place Market")] - public async Task OpenAIChatAsTextTestAsync(string prompt, string expectedAnswerContains) - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - IKernelBuilder builder = this._kernelBuilder; - - this.ConfigureChatOpenAI(builder); - - Kernel target = builder.Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "ChatPlugin"); - - // Act - FunctionResult actual = await target.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); - - // Assert - Assert.Contains(expectedAnswerContains, actual.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Fact(Skip = "Skipping while we investigate issue with GitHub actions.")] - public async Task CanUseOpenAiChatForTextGenerationAsync() - { - // Note: we use OpenAI Chat Completion and GPT 3.5 Turbo - this._kernelBuilder.Services.AddSingleton(this._logger); - IKernelBuilder builder = this._kernelBuilder; - this.ConfigureChatOpenAI(builder); - - Kernel target = builder.Build(); - - var func = target.CreateFunctionFromPrompt( - "List the two planets after '{{$input}}', excluding moons, using bullet points.", - new OpenAIPromptExecutionSettings()); - - var result = await func.InvokeAsync(target, new() { [InputParameterName] = "Jupiter" }); - - Assert.NotNull(result); - Assert.Contains("Saturn", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); - Assert.Contains("Uranus", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); - } - - [Theory] - [InlineData(false, "Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place")] - [InlineData(true, "Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place")] - public async Task AzureOpenAIStreamingTestAsync(bool useChatModel, string prompt, string expectedAnswerContains) - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - - if (useChatModel) - { - this.ConfigureAzureOpenAIChatAsText(builder); - } - else - { - this.ConfigureAzureOpenAI(builder); - } - - Kernel target = builder.Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "ChatPlugin"); - - StringBuilder fullResult = new(); - // Act - await foreach (var content in target.InvokeStreamingAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt })) - { - if (content is StreamingChatMessageContent messageContent) - { - Assert.NotNull(messageContent.Role); - } - - fullResult.Append(content); - } - - // Assert - Assert.Contains(expectedAnswerContains, fullResult.ToString(), StringComparison.OrdinalIgnoreCase); - } - - [Theory] - [InlineData(false, "Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place")] - [InlineData(true, "Where is the most famous fish market in Seattle, Washington, USA?", "Pike Place")] - public async Task AzureOpenAITestAsync(bool useChatModel, string prompt, string expectedAnswerContains) - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - - if (useChatModel) - { - this.ConfigureAzureOpenAIChatAsText(builder); - } - else - { - this.ConfigureAzureOpenAI(builder); - } - - Kernel target = builder.Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "ChatPlugin"); - - // Act - FunctionResult actual = await target.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); - - // Assert - Assert.Contains(expectedAnswerContains, actual.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - // If the test fails, please note that SK retry logic may not be fully integrated into the underlying code using Azure SDK - [Theory] - [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Resilience event occurred")] - public async Task OpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput) - { - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - this._kernelBuilder.Services.AddSingleton(this._testOutputHelper); - this._kernelBuilder - .AddOpenAITextGeneration( - serviceId: openAIConfiguration.ServiceId, - modelId: openAIConfiguration.ModelId, - apiKey: "INVALID_KEY"); // Use an invalid API key to force a 401 Unauthorized response - this._kernelBuilder.Services.ConfigureHttpClientDefaults(c => - { - // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example - c.AddStandardResilienceHandler().Configure(o => - { - o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.Unauthorized); - }); - }); - Kernel target = this._kernelBuilder.Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); - - // Act - await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = prompt })); - - // Assert - Assert.Contains(expectedOutput, this._testOutputHelper.GetLogs(), StringComparison.OrdinalIgnoreCase); - } - - // If the test fails, please note that SK retry logic may not be fully integrated into the underlying code using Azure SDK - [Theory] - [InlineData("Where is the most famous fish market in Seattle, Washington, USA?", "Resilience event occurred")] - public async Task AzureOpenAIHttpRetryPolicyTestAsync(string prompt, string expectedOutput) - { - this._kernelBuilder.Services.AddSingleton(this._testOutputHelper); - IKernelBuilder builder = this._kernelBuilder; - - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - // Use an invalid API key to force a 401 Unauthorized response - builder.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: "INVALID_KEY"); - - builder.Services.ConfigureHttpClientDefaults(c => - { - // Use a standard resiliency policy, augmented to retry on 401 Unauthorized for this example - c.AddStandardResilienceHandler().Configure(o => - { - o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.Unauthorized); - }); - }); - - Kernel target = builder.Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); - - // Act - await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = prompt })); - - // Assert - Assert.Contains(expectedOutput, this._testOutputHelper.GetLogs(), StringComparison.OrdinalIgnoreCase); - } - - [Theory] - [InlineData(false)] - [InlineData(true)] - public async Task AzureOpenAIShouldReturnMetadataAsync(bool useChatModel) - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - - if (useChatModel) - { - this.ConfigureAzureOpenAIChatAsText(this._kernelBuilder); - } - else - { - this.ConfigureAzureOpenAI(this._kernelBuilder); - } - - var kernel = this._kernelBuilder.Build(); - - var plugin = TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); - - // Act - var result = await kernel.InvokeAsync(plugin["FunPlugin"]["Limerick"]); - - // Assert - Assert.NotNull(result.Metadata); - - // Usage - Assert.True(result.Metadata.TryGetValue("Usage", out object? usageObject)); - Assert.NotNull(usageObject); - - var jsonObject = JsonSerializer.SerializeToElement(usageObject); - Assert.True(jsonObject.TryGetProperty("PromptTokens", out JsonElement promptTokensJson)); - Assert.True(promptTokensJson.TryGetInt32(out int promptTokens)); - Assert.NotEqual(0, promptTokens); - - Assert.True(jsonObject.TryGetProperty("CompletionTokens", out JsonElement completionTokensJson)); - Assert.True(completionTokensJson.TryGetInt32(out int completionTokens)); - Assert.NotEqual(0, completionTokens); - - // ContentFilterResults - Assert.True(result.Metadata.ContainsKey("ContentFilterResults")); - } - - [Fact] - public async Task OpenAIHttpInvalidKeyShouldReturnErrorDetailAsync() - { - // Arrange - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - // Use an invalid API key to force a 401 Unauthorized response - this._kernelBuilder.Services.AddSingleton(this._logger); - Kernel target = this._kernelBuilder - .AddOpenAITextGeneration( - modelId: openAIConfiguration.ModelId, - apiKey: "INVALID_KEY", - serviceId: openAIConfiguration.ServiceId) - .Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); - - // Act and Assert - var ex = await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = "Any" })); - - Assert.Equal(HttpStatusCode.Unauthorized, ((HttpOperationException)ex).StatusCode); - } - - [Fact] - public async Task AzureOpenAIHttpInvalidKeyShouldReturnErrorDetailAsync() - { - // Arrange - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - this._kernelBuilder.Services.AddSingleton(this._testOutputHelper); - Kernel target = this._kernelBuilder - .AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: "INVALID_KEY", - serviceId: azureOpenAIConfiguration.ServiceId) - .Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); - - // Act and Assert - var ex = await Assert.ThrowsAsync(() => target.InvokeAsync(plugins["SummarizePlugin"]["Summarize"], new() { [InputParameterName] = "Any" })); - - Assert.Equal(HttpStatusCode.Unauthorized, ((HttpOperationException)ex).StatusCode); - } - - [Fact] - public async Task AzureOpenAIHttpExceededMaxTokensShouldReturnErrorDetailAsync() - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - // Arrange - this._kernelBuilder.Services.AddSingleton(this._testOutputHelper); - Kernel target = this._kernelBuilder - .AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey, - serviceId: azureOpenAIConfiguration.ServiceId) - .Build(); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "SummarizePlugin"); - - // Act - // Assert - await Assert.ThrowsAsync(() => plugins["SummarizePlugin"]["Summarize"].InvokeAsync(target, new() { [InputParameterName] = string.Join('.', Enumerable.Range(1, 40000)) })); - } - - [Theory(Skip = "This test is for manual verification.")] - [InlineData("\n", AIServiceType.OpenAI)] - [InlineData("\r\n", AIServiceType.OpenAI)] - [InlineData("\n", AIServiceType.AzureOpenAI)] - [InlineData("\r\n", AIServiceType.AzureOpenAI)] - public async Task CompletionWithDifferentLineEndingsAsync(string lineEnding, AIServiceType service) - { - // Arrange - var prompt = - "Given a json input and a request. Apply the request on the json input and return the result. " + - $"Put the result in between tags{lineEnding}" + - $$"""Input:{{lineEnding}}{"name": "John", "age": 30}{{lineEnding}}{{lineEnding}}Request:{{lineEnding}}name"""; - - const string ExpectedAnswerContains = "John"; - - this._kernelBuilder.Services.AddSingleton(this._logger); - Kernel target = this._kernelBuilder.Build(); - - this._serviceConfiguration[service](target); - - IReadOnlyKernelPluginCollection plugins = TestHelpers.ImportSamplePlugins(target, "ChatPlugin"); - - // Act - FunctionResult actual = await target.InvokeAsync(plugins["ChatPlugin"]["Chat"], new() { [InputParameterName] = prompt }); - - // Assert - Assert.Contains(ExpectedAnswerContains, actual.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task AzureOpenAIInvokePromptTestAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAI(builder); - Kernel target = builder.Build(); - - var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; - - // Act - FunctionResult actual = await target.InvokePromptAsync(prompt, new(new OpenAIPromptExecutionSettings() { MaxTokens = 150 })); - - // Assert - Assert.Contains("Pike Place", actual.GetValue(), StringComparison.OrdinalIgnoreCase); - Assert.NotNull(actual.Metadata); - } - - [Fact] - public async Task AzureOpenAIInvokePromptWithMultipleResultsTestAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - Kernel target = builder.Build(); - - var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; - - var executionSettings = new OpenAIPromptExecutionSettings() { MaxTokens = 150, ResultsPerPrompt = 3 }; - - // Act - FunctionResult actual = await target.InvokePromptAsync(prompt, new(executionSettings)); - - // Assert - Assert.Null(actual.Metadata); - - var chatMessageContents = actual.GetValue>(); - - Assert.NotNull(chatMessageContents); - Assert.Equal(executionSettings.ResultsPerPrompt, chatMessageContents.Count); - - foreach (var chatMessageContent in chatMessageContents) - { - Assert.NotNull(chatMessageContent.Metadata); - Assert.Contains("Pike Place", chatMessageContent.Content, StringComparison.OrdinalIgnoreCase); - } - } - - [Fact] - public async Task AzureOpenAIDefaultValueTestAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAI(builder); - Kernel target = builder.Build(); - - IReadOnlyKernelPluginCollection plugin = TestHelpers.ImportSamplePlugins(target, "FunPlugin"); - - // Act - FunctionResult actual = await target.InvokeAsync(plugin["FunPlugin"]["Limerick"]); - - // Assert - Assert.Contains("Bob", actual.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task MultipleServiceLoadPromptConfigTestAsync() - { - // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAI(builder); - this.ConfigureInvalidAzureOpenAI(builder); - - Kernel target = builder.Build(); - - var prompt = "Where is the most famous fish market in Seattle, Washington, USA?"; - var defaultPromptModel = new PromptTemplateConfig(prompt) { Name = "FishMarket1" }; - var azurePromptModel = PromptTemplateConfig.FromJson(""" - { - "name": "FishMarket2", - "execution_settings": { - "azure-gpt-35-turbo-instruct": { - "max_tokens": 256 - } - } - } - """); - azurePromptModel.Template = prompt; - - var defaultFunc = target.CreateFunctionFromPrompt(defaultPromptModel); - var azureFunc = target.CreateFunctionFromPrompt(azurePromptModel); - - // Act - await Assert.ThrowsAsync(() => target.InvokeAsync(defaultFunc)); - - FunctionResult azureResult = await target.InvokeAsync(azureFunc); - - // Assert - Assert.Contains("Pike Place", azureResult.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task ChatSystemPromptIsNotIgnoredAsync() - { - // Arrange - var settings = new OpenAIPromptExecutionSettings { ChatSystemPrompt = "Reply \"I don't know\" to every question." }; - - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - Kernel target = builder.Build(); - - // Act - var result = await target.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?", new(settings)); - - // Assert - Assert.Contains("I don't know", result.ToString(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task SemanticKernelVersionHeaderIsSentAsync() - { - // Arrange - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); - Assert.NotNull(azureOpenAIConfiguration.ApiKey); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - Assert.NotNull(azureOpenAIConfiguration.ServiceId); - - using var defaultHandler = new HttpClientHandler(); - using var httpHeaderHandler = new HttpHeaderHandler(defaultHandler); - using var httpClient = new HttpClient(httpHeaderHandler); - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - builder.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName, - modelId: azureOpenAIConfiguration.ChatModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey, - serviceId: azureOpenAIConfiguration.ServiceId, - httpClient: httpClient); - Kernel target = builder.Build(); - - // Act - var result = await target.InvokePromptAsync("Where is the most famous fish market in Seattle, Washington, USA?"); - - // Assert - Assert.NotNull(httpHeaderHandler.RequestHeaders); - Assert.True(httpHeaderHandler.RequestHeaders.TryGetValues("Semantic-Kernel-Version", out var values)); - } - - [Theory(Skip = "This test is for manual verification.")] - [InlineData(null, null)] - [InlineData(false, null)] - [InlineData(true, 2)] - [InlineData(true, 5)] - public async Task LogProbsDataIsReturnedWhenRequestedAsync(bool? logprobs, int? topLogprobs) - { - // Arrange - var settings = new OpenAIPromptExecutionSettings { Logprobs = logprobs, TopLogprobs = topLogprobs }; - - this._kernelBuilder.Services.AddSingleton(this._logger); - var builder = this._kernelBuilder; - this.ConfigureAzureOpenAIChatAsText(builder); - Kernel target = builder.Build(); - - // Act - var result = await target.InvokePromptAsync("Hi, can you help me today?", new(settings)); - - var logProbabilityInfo = result.Metadata?["LogProbabilityInfo"] as ChatChoiceLogProbabilityInfo; - - // Assert - if (logprobs is true) - { - Assert.NotNull(logProbabilityInfo); - Assert.Equal(topLogprobs, logProbabilityInfo.TokenLogProbabilityResults[0].TopLogProbabilityEntries.Count); - } - else - { - Assert.Null(logProbabilityInfo); - } - } - - #region internals - - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - private readonly Dictionary> _serviceConfiguration = []; - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - - private void ConfigureChatOpenAI(IKernelBuilder kernelBuilder) - { - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - - Assert.NotNull(openAIConfiguration); - Assert.NotNull(openAIConfiguration.ChatModelId); - Assert.NotNull(openAIConfiguration.ApiKey); - Assert.NotNull(openAIConfiguration.ServiceId); - - kernelBuilder.AddOpenAIChatCompletion( - modelId: openAIConfiguration.ChatModelId, - apiKey: openAIConfiguration.ApiKey, - serviceId: openAIConfiguration.ServiceId); - } - - private void ConfigureAzureOpenAI(IKernelBuilder kernelBuilder) - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.DeploymentName); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - Assert.NotNull(azureOpenAIConfiguration.ApiKey); - Assert.NotNull(azureOpenAIConfiguration.ServiceId); - - kernelBuilder.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey, - serviceId: azureOpenAIConfiguration.ServiceId); - } - private void ConfigureInvalidAzureOpenAI(IKernelBuilder kernelBuilder) - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.DeploymentName); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - - kernelBuilder.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: "invalid-api-key", - serviceId: $"invalid-{azureOpenAIConfiguration.ServiceId}"); - } - - private void ConfigureAzureOpenAIChatAsText(IKernelBuilder kernelBuilder) - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); - Assert.NotNull(azureOpenAIConfiguration.ApiKey); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - Assert.NotNull(azureOpenAIConfiguration.ServiceId); - - kernelBuilder.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName, - modelId: azureOpenAIConfiguration.ChatModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey, - serviceId: azureOpenAIConfiguration.ServiceId); - } - - private sealed class HttpHeaderHandler(HttpMessageHandler innerHandler) : DelegatingHandler(innerHandler) - { - public System.Net.Http.Headers.HttpRequestHeaders? RequestHeaders { get; private set; } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - this.RequestHeaders = request.Headers; - return await base.SendAsync(request, cancellationToken); - } - } - - #endregion -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs deleted file mode 100644 index 30b0c3d1115b..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; -using Xunit.Abstractions; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -#pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. - -public sealed class OpenAIFileServiceTests(ITestOutputHelper output) : IDisposable -{ - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData("test_image_001.jpg", "image/jpeg")] - [InlineData("test_content.txt", "text/plain")] - public async Task OpenAIFileServiceLifecycleAsync(string fileName, string mimeType) - { - // Arrange - OpenAIFileService fileService = this.CreateOpenAIFileService(); - - // Act & Assert - await this.VerifyFileServiceLifecycleAsync(fileService, fileName, mimeType); - } - - [Theory] - [InlineData("test_image_001.jpg", "image/jpeg")] - [InlineData("test_content.txt", "text/plain")] - public async Task AzureOpenAIFileServiceLifecycleAsync(string fileName, string mimeType) - { - // Arrange - OpenAIFileService fileService = this.CreateOpenAIFileService(); - - // Act & Assert - await this.VerifyFileServiceLifecycleAsync(fileService, fileName, mimeType); - } - - private async Task VerifyFileServiceLifecycleAsync(OpenAIFileService fileService, string fileName, string mimeType) - { - // Setup file content - await using FileStream fileStream = File.OpenRead($"./TestData/{fileName}"); - BinaryData sourceData = await BinaryData.FromStreamAsync(fileStream); - BinaryContent sourceContent = new(sourceData.ToArray(), mimeType); - - // Upload file with unsupported purpose (failure case) - await Assert.ThrowsAsync(() => fileService.UploadContentAsync(sourceContent, new(fileName, OpenAIFilePurpose.AssistantsOutput))); - - // Upload file with wacky purpose (failure case) - await Assert.ThrowsAsync(() => fileService.UploadContentAsync(sourceContent, new(fileName, new OpenAIFilePurpose("pretend")))); - - // Upload file - OpenAIFileReference fileReference = await fileService.UploadContentAsync(sourceContent, new(fileName, OpenAIFilePurpose.FineTune)); - try - { - AssertFileReferenceEquals(fileReference, fileName, sourceData.Length, OpenAIFilePurpose.FineTune); - - // Retrieve files by different purpose - Dictionary fileMap = await GetFilesAsync(fileService, OpenAIFilePurpose.Assistants); - Assert.DoesNotContain(fileReference.Id, fileMap.Keys); - - // Retrieve files by wacky purpose (failure case) - await Assert.ThrowsAsync(() => GetFilesAsync(fileService, new OpenAIFilePurpose("pretend"))); - - // Retrieve files by expected purpose - fileMap = await GetFilesAsync(fileService, OpenAIFilePurpose.FineTune); - Assert.Contains(fileReference.Id, fileMap.Keys); - AssertFileReferenceEquals(fileMap[fileReference.Id], fileName, sourceData.Length, OpenAIFilePurpose.FineTune); - - // Retrieve files by no specific purpose - fileMap = await GetFilesAsync(fileService); - Assert.Contains(fileReference.Id, fileMap.Keys); - AssertFileReferenceEquals(fileMap[fileReference.Id], fileName, sourceData.Length, OpenAIFilePurpose.FineTune); - - // Retrieve file by id - OpenAIFileReference file = await fileService.GetFileAsync(fileReference.Id); - AssertFileReferenceEquals(file, fileName, sourceData.Length, OpenAIFilePurpose.FineTune); - - // Retrieve file content - BinaryContent retrievedContent = await fileService.GetFileContentAsync(fileReference.Id); - Assert.NotNull(retrievedContent.Data); - Assert.NotNull(retrievedContent.Uri); - Assert.NotNull(retrievedContent.Metadata); - Assert.Equal(fileReference.Id, retrievedContent.Metadata["id"]); - Assert.Equal(sourceContent.Data!.Value.Length, retrievedContent.Data.Value.Length); - } - finally - { - // Delete file - await fileService.DeleteFileAsync(fileReference.Id); - } - } - - private static void AssertFileReferenceEquals(OpenAIFileReference fileReference, string expectedFileName, int expectedSize, OpenAIFilePurpose expectedPurpose) - { - Assert.Equal(expectedFileName, fileReference.FileName); - Assert.Equal(expectedPurpose, fileReference.Purpose); - Assert.Equal(expectedSize, fileReference.SizeInBytes); - } - - private static async Task> GetFilesAsync(OpenAIFileService fileService, OpenAIFilePurpose? purpose = null) - { - IEnumerable files = await fileService.GetFilesAsync(purpose); - Dictionary fileIds = files.DistinctBy(f => f.Id).ToDictionary(f => f.Id); - return fileIds; - } - - #region internals - - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - - private OpenAIFileService CreateOpenAIFileService() - { - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - - Assert.NotNull(openAIConfiguration); - Assert.NotNull(openAIConfiguration.ApiKey); - Assert.NotNull(openAIConfiguration.ServiceId); - - return new(openAIConfiguration.ApiKey, openAIConfiguration.ServiceId, loggerFactory: this._logger); - } - - private OpenAIFileService CreateAzureOpenAIFileService() - { - var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); - - Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.Endpoint); - Assert.NotNull(azureOpenAIConfiguration.ApiKey); - Assert.NotNull(azureOpenAIConfiguration.ServiceId); - - return new(new Uri(azureOpenAIConfiguration.Endpoint), azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.ServiceId, loggerFactory: this._logger); - } - - #endregion -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs deleted file mode 100644 index 74f63fa3fabd..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Microsoft.SemanticKernel.Embeddings; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class OpenAITextEmbeddingTests -{ - private const int AdaVectorLength = 1536; - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData("test sentence")] - public async Task OpenAITestAsync(string testInputString) - { - // Arrange - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get(); - Assert.NotNull(openAIConfiguration); - - var embeddingGenerator = new OpenAITextEmbeddingGenerationService(openAIConfiguration.ModelId, openAIConfiguration.ApiKey); - - // Act - var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString); - var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]); - - // Assert - Assert.Equal(AdaVectorLength, singleResult.Length); - Assert.Equal(3, batchResult.Count); - } - - [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - [InlineData(null, 3072)] - [InlineData(1024, 1024)] - public async Task OpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength) - { - // Arrange - const string TestInputString = "test sentence"; - - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAIEmbeddings").Get(); - Assert.NotNull(openAIConfiguration); - - var embeddingGenerator = new OpenAITextEmbeddingGenerationService( - "text-embedding-3-large", - openAIConfiguration.ApiKey, - dimensions: dimensions); - - // Act - var result = await embeddingGenerator.GenerateEmbeddingAsync(TestInputString); - - // Assert - Assert.Equal(expectedVectorLength, result.Length); - } - - [Theory] - [InlineData("test sentence")] - public async Task AzureOpenAITestAsync(string testInputString) - { - // Arrange - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - var embeddingGenerator = new AzureOpenAITextEmbeddingGenerationService(azureOpenAIConfiguration.DeploymentName, - azureOpenAIConfiguration.Endpoint, - azureOpenAIConfiguration.ApiKey); - - // Act - var singleResult = await embeddingGenerator.GenerateEmbeddingAsync(testInputString); - var batchResult = await embeddingGenerator.GenerateEmbeddingsAsync([testInputString, testInputString, testInputString]); - - // Assert - Assert.Equal(AdaVectorLength, singleResult.Length); - Assert.Equal(3, batchResult.Count); - } - - [Theory] - [InlineData(null, 3072)] - [InlineData(1024, 1024)] - public async Task AzureOpenAIWithDimensionsAsync(int? dimensions, int expectedVectorLength) - { - // Arrange - const string TestInputString = "test sentence"; - - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAIEmbeddings").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - var embeddingGenerator = new AzureOpenAITextEmbeddingGenerationService( - "text-embedding-3-large", - azureOpenAIConfiguration.Endpoint, - azureOpenAIConfiguration.ApiKey, - dimensions: dimensions); - - // Act - var result = await embeddingGenerator.GenerateEmbeddingAsync(TestInputString); - - // Assert - Assert.Equal(expectedVectorLength, result.Length); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs deleted file mode 100644 index e35c357cf375..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.TextToAudio; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class OpenAITextToAudioTests -{ - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Fact(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] - public async Task OpenAITextToAudioTestAsync() - { - // Arrange - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToAudio").Get(); - Assert.NotNull(openAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddOpenAITextToAudio(openAIConfiguration.ModelId, openAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - // Act - var result = await service.GetAudioContentAsync("The sun rises in the east and sets in the west."); - - // Assert - var audioData = result.Data!.Value; - Assert.False(audioData.IsEmpty); - } - - [Fact] - public async Task AzureOpenAITextToAudioTestAsync() - { - // Arrange - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAITextToAudio").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAITextToAudio( - azureOpenAIConfiguration.DeploymentName, - azureOpenAIConfiguration.Endpoint, - azureOpenAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - // Act - var result = await service.GetAudioContentAsync("The sun rises in the east and sets in the west."); - - // Assert - var audioData = result.Data!.Value; - Assert.False(audioData.IsEmpty); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs deleted file mode 100644 index e133f91ee547..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.TextToImage; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; -public sealed class OpenAITextToImageTests -{ - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - [Fact(Skip = "This test is for manual verification.")] - public async Task OpenAITextToImageTestAsync() - { - // Arrange - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToImage").Get(); - Assert.NotNull(openAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddOpenAITextToImage(apiKey: openAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - // Act - var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", 512, 512); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - } - - [Fact(Skip = "This test is for manual verification.")] - public async Task OpenAITextToImageByModelTestAsync() - { - // Arrange - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("OpenAITextToImage").Get(); - Assert.NotNull(openAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddOpenAITextToImage(apiKey: openAIConfiguration.ApiKey, modelId: openAIConfiguration.ModelId) - .Build(); - - var service = kernel.GetRequiredService(); - - // Act - var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", 1024, 1024); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - } - - [Fact(Skip = "This test is for manual verification.")] - public async Task AzureOpenAITextToImageTestAsync() - { - // Arrange - AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAITextToImage").Get(); - Assert.NotNull(azureOpenAIConfiguration); - - var kernel = Kernel.CreateBuilder() - .AddAzureOpenAITextToImage( - azureOpenAIConfiguration.DeploymentName, - azureOpenAIConfiguration.Endpoint, - azureOpenAIConfiguration.ApiKey) - .Build(); - - var service = kernel.GetRequiredService(); - - // Act - var result = await service.GenerateImageAsync("The sun rises in the east and sets in the west.", 1024, 1024); - - // Assert - Assert.NotNull(result); - Assert.NotEmpty(result); - } -} diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs deleted file mode 100644 index 243526fdfc82..000000000000 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIToolsTests.cs +++ /dev/null @@ -1,852 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Linq; -using System.Text; -using System.Text.Json; -using System.Threading.Tasks; -using Azure.AI.OpenAI; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Time.Testing; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using SemanticKernel.IntegrationTests.TestSettings; -using Xunit; - -namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; - -public sealed class OpenAIToolsTests : BaseIntegrationTest -{ - [Fact(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - public async Task CanAutoInvokeKernelFunctionsAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - var filter = new FakeFunctionFilter(async (context, next) => - { - invokedFunctions.Add(context.Function.Name); - await next(context); - }); - - kernel.FunctionInvocationFilters.Add(filter); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var result = await kernel.InvokePromptAsync("How many days until Christmas? Explain your thinking.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("GetCurrentUtcTime", invokedFunctions); - } - - [Fact(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - public async Task CanAutoInvokeKernelFunctionsStreamingAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - var invokedFunctions = new List(); - - var filter = new FakeFunctionFilter(async (context, next) => - { - invokedFunctions.Add($"{context.Function.Name}({string.Join(", ", context.Arguments)})"); - await next(context); - }); - - kernel.FunctionInvocationFilters.Add(filter); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - string result = ""; - await foreach (string c in kernel.InvokePromptStreamingAsync( - $"How much older is John than Jim? Compute that value and pass it to the {nameof(TimeInformation)}.{nameof(TimeInformation.InterpretValue)} function, then respond only with its result.", - new(settings))) - { - result += c; - } - - // Assert - Assert.Contains("6", result, StringComparison.InvariantCulture); - Assert.Contains("GetAge([personName, John])", invokedFunctions); - Assert.Contains("GetAge([personName, Jim])", invokedFunctions); - Assert.Contains("InterpretValue([value, 3])", invokedFunctions); - } - - [Fact(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - public async Task CanAutoInvokeKernelFunctionsWithComplexTypeParametersAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var result = await kernel.InvokePromptAsync("What is the current temperature in Dublin, Ireland, in Fahrenheit?", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("42.8", result.GetValue(), StringComparison.InvariantCulture); // The WeatherPlugin always returns 42.8 for Dublin, Ireland. - } - - [Fact(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - public async Task CanAutoInvokeKernelFunctionsWithPrimitiveTypeParametersAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - kernel.ImportPluginFromType(); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var result = await kernel.InvokePromptAsync("Convert 50 degrees Fahrenheit to Celsius.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("10", result.GetValue(), StringComparison.InvariantCulture); - } - - [Fact(Skip = "OpenAI is throttling requests. Switch this test to use Azure OpenAI.")] - public async Task CanAutoInvokeKernelFunctionsWithEnumTypeParametersAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - var timeProvider = new FakeTimeProvider(); - timeProvider.SetUtcNow(new DateTimeOffset(new DateTime(2024, 4, 24))); // Wednesday - var timePlugin = new TimePlugin(timeProvider); - kernel.ImportPluginFromObject(timePlugin, nameof(TimePlugin)); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var result = await kernel.InvokePromptAsync( - "When was last friday? Show the date in format DD.MM.YYYY for example: 15.07.2019", - new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("19.04.2024", result.GetValue(), StringComparison.OrdinalIgnoreCase); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionFromPromptAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var promptFunction = KernelFunctionFactory.CreateFromPrompt( - "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", - functionName: "FindLatestNews", - description: "Searches for the latest news."); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( - "NewsProvider", - "Delivers up-to-date news content.", - [promptFunction])); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var result = await kernel.InvokePromptAsync("Show me the latest news as they are.", new(settings)); - - // Assert - Assert.NotNull(result); - Assert.Contains("Transportation", result.GetValue(), StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task CanAutoInvokeKernelFunctionFromPromptStreamingAsync() - { - // Arrange - Kernel kernel = this.InitializeKernel(); - - var promptFunction = KernelFunctionFactory.CreateFromPrompt( - "Your role is always to return this text - 'A Game-Changer for the Transportation Industry'. Don't ask for more details or context.", - functionName: "FindLatestNews", - description: "Searches for the latest news."); - - kernel.Plugins.Add(KernelPluginFactory.CreateFromFunctions( - "NewsProvider", - "Delivers up-to-date news content.", - [promptFunction])); - - // Act - OpenAIPromptExecutionSettings settings = new() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - var streamingResult = kernel.InvokePromptStreamingAsync("Show me the latest news as they are.", new(settings)); - - var builder = new StringBuilder(); - - await foreach (var update in streamingResult) - { - builder.Append(update.ToString()); - } - - var result = builder.ToString(); - - // Assert - Assert.NotNull(result); - Assert.Contains("Transportation", result, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorSpecificChatMessageContentClassesCanBeUsedForManualFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - // Act - var result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Current way of handling function calls manually using connector specific chat message content class. - var toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); - - while (toolCalls.Count > 0) - { - // Adding LLM function call request to chat history - chatHistory.Add(result); - - // Iterating over the requested function calls and invoking them - foreach (var toolCall in toolCalls) - { - string content = kernel.Plugins.TryGetFunctionAndArguments(toolCall, out KernelFunction? function, out KernelArguments? arguments) ? - JsonSerializer.Serialize((await function.InvokeAsync(kernel, arguments)).GetValue()) : - "Unable to find function. Please try again!"; - - // Adding the result of the function call to the chat history - chatHistory.Add(new ChatMessageContent( - AuthorRole.Tool, - content, - metadata: new Dictionary(1) { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } })); - } - - // Sending the functions invocation results back to the LLM to get the final response - result = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - toolCalls = ((OpenAIChatMessageContent)result).ToolCalls.OfType().ToList(); - } - - // Assert - Assert.Contains("rain", result.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - // Act - var messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length != 0) - { - // Adding function call from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - var result = await functionCall.InvokeAsync(kernel); - - chatHistory.Add(result.ToChatMessage()); - } - - // Sending the functions invocation results to the LLM to get the final response - messageContent = await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.Contains("rain", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact(Skip = "The test is temporarily disabled until a more stable solution is found.")] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var completionService = kernel.GetRequiredService(); - - // Act - var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length != 0) - { - // Adding function call from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - // Simulating an exception - var exception = new OperationCanceledException("The operation was canceled due to timeout."); - - chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); - } - - // Sending the functions execution results back to the LLM to get the final response - messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.NotNull(messageContent.Content); - - Assert.Contains("error", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var completionService = kernel.GetRequiredService(); - - // Act - var messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - var functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - - while (functionCalls.Length > 0) - { - // Adding function call from LLM to chat history - chatHistory.Add(messageContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - var result = await functionCall.InvokeAsync(kernel); - - chatHistory.AddMessage(AuthorRole.Tool, [result]); - } - - // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); - messageContent.Items.Add(simulatedFunctionCall); - - // Adding a simulated function result to chat history - var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); - - // Sending the functions invocation results back to the LLM to get the final response - messageContent = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - functionCalls = FunctionCallContent.GetFunctionCalls(messageContent).ToArray(); - } - - // Assert - Assert.Contains("tornado", messageContent.Content, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ItFailsIfNoFunctionResultProvidedAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var completionService = kernel.GetRequiredService(); - - // Act - var result = await completionService.GetChatMessageContentAsync(chatHistory, settings, kernel); - - chatHistory.Add(result); - - var exception = await Assert.ThrowsAsync(() => completionService.GetChatMessageContentAsync(chatHistory, settings, kernel)); - - // Assert - Assert.Contains("'tool_calls' must be followed by tool", exception.Message, StringComparison.InvariantCulture); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - // Act - await sut.GetChatMessageContentAsync(chatHistory, settings, kernel); - - // Assert - Assert.Equal(5, chatHistory.Count); - - var userMessage = chatHistory[0]; - Assert.Equal(AuthorRole.User, userMessage.Role); - - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); - - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); - Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; - Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); - Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); - Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); - Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; - Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); - Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); - Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); - Assert.NotNull(getWeatherForCityFunctionCallResult.Result); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForManualFunctionCallingForStreamingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - string? result = null; - - // Act - while (true) - { - AuthorRole? authorRole = null; - var fccBuilder = new FunctionCallContentBuilder(); - var textContent = new StringBuilder(); - - await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) - { - textContent.Append(streamingContent.Content); - authorRole ??= streamingContent.Role; - fccBuilder.Append(streamingContent); - } - - var functionCalls = fccBuilder.Build(); - if (functionCalls.Any()) - { - var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); - chatHistory.Add(fcContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - fcContent.Items.Add(functionCall); - - var functionResult = await functionCall.InvokeAsync(kernel); - - chatHistory.Add(functionResult.ToChatMessage()); - } - - continue; - } - - result = textContent.ToString(); - break; - } - - // Assert - Assert.Contains("rain", result, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanBeUsedForAutoFunctionCallingForStreamingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var chatHistory = new ChatHistory(); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - var result = new StringBuilder(); - - // Act - await foreach (var contentUpdate in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) - { - result.Append(contentUpdate.Content); - } - - // Assert - Assert.Equal(5, chatHistory.Count); - - var userMessage = chatHistory[0]; - Assert.Equal(AuthorRole.User, userMessage.Role); - - // LLM requested the current time. - var getCurrentTimeFunctionCallRequestMessage = chatHistory[1]; - Assert.Equal(AuthorRole.Assistant, getCurrentTimeFunctionCallRequestMessage.Role); - - var getCurrentTimeFunctionCallRequest = getCurrentTimeFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallRequest.PluginName); - Assert.NotNull(getCurrentTimeFunctionCallRequest.Id); - - // Connector invoked the GetCurrentUtcTime function and added result to chat history. - var getCurrentTimeFunctionCallResultMessage = chatHistory[2]; - Assert.Equal(AuthorRole.Tool, getCurrentTimeFunctionCallResultMessage.Role); - Assert.Single(getCurrentTimeFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getCurrentTimeFunctionCallResult = getCurrentTimeFunctionCallResultMessage.Items.OfType().Single(); - Assert.Equal("GetCurrentUtcTime", getCurrentTimeFunctionCallResult.FunctionName); - Assert.Equal("HelperFunctions", getCurrentTimeFunctionCallResult.PluginName); - Assert.Equal(getCurrentTimeFunctionCallRequest.Id, getCurrentTimeFunctionCallResult.CallId); - Assert.NotNull(getCurrentTimeFunctionCallResult.Result); - - // LLM requested the weather for Boston. - var getWeatherForCityFunctionCallRequestMessage = chatHistory[3]; - Assert.Equal(AuthorRole.Assistant, getWeatherForCityFunctionCallRequestMessage.Role); - - var getWeatherForCityFunctionCallRequest = getWeatherForCityFunctionCallRequestMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallRequest.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallRequest.PluginName); - Assert.NotNull(getWeatherForCityFunctionCallRequest.Id); - - // Connector invoked the Get_Weather_For_City function and added result to chat history. - var getWeatherForCityFunctionCallResultMessage = chatHistory[4]; - Assert.Equal(AuthorRole.Tool, getWeatherForCityFunctionCallResultMessage.Role); - Assert.Single(getWeatherForCityFunctionCallResultMessage.Items.OfType()); // Current function calling model adds TextContent item representing the result of the function call. - - var getWeatherForCityFunctionCallResult = getWeatherForCityFunctionCallResultMessage.Items.OfType().Single(); - Assert.Equal("Get_Weather_For_City", getWeatherForCityFunctionCallResult.FunctionName); - Assert.Equal("HelperFunctions", getWeatherForCityFunctionCallResult.PluginName); - Assert.Equal(getWeatherForCityFunctionCallRequest.Id, getWeatherForCityFunctionCallResult.CallId); - Assert.NotNull(getWeatherForCityFunctionCallResult.Result); - } - - [Fact(Skip = "The test is temporarily disabled until a more stable solution is found.")] - public async Task ConnectorAgnosticFunctionCallingModelClassesCanPassFunctionExceptionToConnectorForStreamingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("If you are unable to answer the question for whatever reason, please add the 'error' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - string? result = null; - - // Act - while (true) - { - AuthorRole? authorRole = null; - var fccBuilder = new FunctionCallContentBuilder(); - var textContent = new StringBuilder(); - - await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) - { - textContent.Append(streamingContent.Content); - authorRole ??= streamingContent.Role; - fccBuilder.Append(streamingContent); - } - - var functionCalls = fccBuilder.Build(); - if (functionCalls.Any()) - { - var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); - chatHistory.Add(fcContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - fcContent.Items.Add(functionCall); - - // Simulating an exception - var exception = new OperationCanceledException("The operation was canceled due to timeout."); - - chatHistory.Add(new FunctionResultContent(functionCall, exception).ToChatMessage()); - } - - continue; - } - - result = textContent.ToString(); - break; - } - - // Assert - Assert.Contains("error", result, StringComparison.InvariantCultureIgnoreCase); - } - - [Fact] - public async Task ConnectorAgnosticFunctionCallingModelClassesSupportSimulatedFunctionCallsForStreamingAsync() - { - // Arrange - var kernel = this.InitializeKernel(importHelperPlugin: true); - - var settings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.EnableKernelFunctions }; - - var sut = kernel.GetRequiredService(); - - var chatHistory = new ChatHistory(); - chatHistory.AddSystemMessage("if there's a tornado warning, please add the 'tornado' keyword to the response."); - chatHistory.AddUserMessage("Given the current time of day and weather, what is the likely color of the sky in Boston?"); - - string? result = null; - - // Act - while (true) - { - AuthorRole? authorRole = null; - var fccBuilder = new FunctionCallContentBuilder(); - var textContent = new StringBuilder(); - - await foreach (var streamingContent in sut.GetStreamingChatMessageContentsAsync(chatHistory, settings, kernel)) - { - textContent.Append(streamingContent.Content); - authorRole ??= streamingContent.Role; - fccBuilder.Append(streamingContent); - } - - var functionCalls = fccBuilder.Build(); - if (functionCalls.Any()) - { - var fcContent = new ChatMessageContent(role: authorRole ?? default, content: null); - chatHistory.Add(fcContent); - - // Iterating over the requested function calls and invoking them - foreach (var functionCall in functionCalls) - { - fcContent.Items.Add(functionCall); - - var functionResult = await functionCall.InvokeAsync(kernel); - - chatHistory.Add(functionResult.ToChatMessage()); - } - - // Adding a simulated function call to the connector response message - var simulatedFunctionCall = new FunctionCallContent("weather-alert", id: "call_123"); - fcContent.Items.Add(simulatedFunctionCall); - - // Adding a simulated function result to chat history - var simulatedFunctionResult = "A Tornado Watch has been issued, with potential for severe thunderstorms causing unusual sky colors like green, yellow, or dark gray. Stay informed and follow safety instructions from authorities."; - chatHistory.Add(new FunctionResultContent(simulatedFunctionCall, simulatedFunctionResult).ToChatMessage()); - - continue; - } - - result = textContent.ToString(); - break; - } - - // Assert - Assert.Contains("tornado", result, StringComparison.InvariantCultureIgnoreCase); - } - - private Kernel InitializeKernel(bool importHelperPlugin = false) - { - OpenAIConfiguration? openAIConfiguration = this._configuration.GetSection("Planners:OpenAI").Get(); - Assert.NotNull(openAIConfiguration); - - IKernelBuilder builder = this.CreateKernelBuilder() - .AddOpenAIChatCompletion( - modelId: openAIConfiguration.ModelId, - apiKey: openAIConfiguration.ApiKey); - - var kernel = builder.Build(); - - if (importHelperPlugin) - { - kernel.ImportPluginFromFunctions("HelperFunctions", - [ - kernel.CreateFunctionFromMethod(() => DateTime.UtcNow.ToString("R"), "GetCurrentUtcTime", "Retrieves the current time in UTC."), - kernel.CreateFunctionFromMethod((string cityName) => - cityName switch - { - "Boston" => "61 and rainy", - _ => "31 and snowing", - }, "Get_Weather_For_City", "Gets the current weather for the specified city"), - ]); - } - - return kernel; - } - - private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() - .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) - .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) - .AddEnvironmentVariables() - .AddUserSecrets() - .Build(); - - /// - /// A plugin that returns the current time. - /// - public class TimeInformation - { - [KernelFunction] - [Description("Retrieves the current time in UTC.")] - public string GetCurrentUtcTime() => DateTime.UtcNow.ToString("R"); - - [KernelFunction] - [Description("Gets the age of the specified person.")] - public int GetAge(string personName) - { - if ("John".Equals(personName, StringComparison.OrdinalIgnoreCase)) - { - return 33; - } - - if ("Jim".Equals(personName, StringComparison.OrdinalIgnoreCase)) - { - return 30; - } - - return -1; - } - - [KernelFunction] - public int InterpretValue(int value) => value * 2; - } - - public class WeatherPlugin - { - [KernelFunction, Description("Get current temperature.")] - public Task GetCurrentTemperatureAsync(WeatherParameters parameters) - { - if (parameters.City.Name == "Dublin" && (parameters.City.Country == "Ireland" || parameters.City.Country == "IE")) - { - return Task.FromResult(42.8); // 42.8 Fahrenheit. - } - - throw new NotSupportedException($"Weather in {parameters.City.Name} ({parameters.City.Country}) is not supported."); - } - - [KernelFunction, Description("Convert temperature from Fahrenheit to Celsius.")] - public Task ConvertTemperatureAsync(double temperatureInFahrenheit) - { - double temperatureInCelsius = (temperatureInFahrenheit - 32) * 5 / 9; - return Task.FromResult(temperatureInCelsius); - } - } - - public record WeatherParameters(City City); - - public class City - { - public string Name { get; set; } = string.Empty; - public string Country { get; set; } = string.Empty; - } - - #region private - - private sealed class FakeFunctionFilter : IFunctionInvocationFilter - { - private readonly Func, Task>? _onFunctionInvocation; - - public FakeFunctionFilter( - Func, Task>? onFunctionInvocation = null) - { - this._onFunctionInvocation = onFunctionInvocation; - } - - public Task OnFunctionInvocationAsync(FunctionInvocationContext context, Func next) => - this._onFunctionInvocation?.Invoke(context, next) ?? Task.CompletedTask; - } - - #endregion - - public sealed class TimePlugin - { - private readonly TimeProvider _timeProvider; - - public TimePlugin(TimeProvider timeProvider) - { - this._timeProvider = timeProvider; - } - - [KernelFunction] - [Description("Get the date of the last day matching the supplied week day name in English. Example: Che giorno era 'Martedi' scorso -> dateMatchingLastDayName 'Tuesday' => Tuesday, 16 May, 2023")] - public string DateMatchingLastDayName( - [Description("The day name to match")] DayOfWeek input, - IFormatProvider? formatProvider = null) - { - DateTimeOffset dateTime = this._timeProvider.GetUtcNow(); - - // Walk backwards from the previous day for up to a week to find the matching day - for (int i = 1; i <= 7; ++i) - { - dateTime = dateTime.AddDays(-1); - if (dateTime.DayOfWeek == input) - { - break; - } - } - - return dateTime.ToString("D", formatProvider); - } - } -} diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index 6d741d390c2e..6abbb8eb3020 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -60,12 +60,12 @@ + - diff --git a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs index e87bbc8d4813..5ed6d6364d6d 100644 --- a/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs +++ b/dotnet/src/IntegrationTests/Planners/Handlebars/HandlebarsPlannerTests.cs @@ -17,12 +17,12 @@ namespace SemanticKernel.IntegrationTests.Planners.Handlebars; public sealed class HandlebarsPlannerTests { [Theory] - [InlineData(true, "Write a joke and send it in an e-mail to Kai.", "SendEmail", "test")] - public async Task CreatePlanFunctionFlowAsync(bool useChatModel, string goal, string expectedFunction, string expectedPlugin) + [InlineData("Write a joke and send it in an e-mail to Kai.", "SendEmail", "test")] + public async Task CreatePlanFunctionFlowAsync(string goal, string expectedFunction, string expectedPlugin) { // Arrange bool useEmbeddings = false; - var kernel = this.InitializeKernel(useEmbeddings, useChatModel); + var kernel = this.InitializeKernel(useEmbeddings); kernel.ImportPluginFromType(expectedPlugin); TestHelpers.ImportSamplePlugins(kernel, "FunPlugin"); @@ -57,7 +57,7 @@ public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFuncti } [Theory] - [InlineData(true, "List each property of the default Qux object.", "## Complex types", """ + [InlineData("List each property of the default Qux object.", "## Complex types", """ ### Qux: { "type": "Object", @@ -71,11 +71,11 @@ public async Task CreatePlanWithDefaultsAsync(string goal, string expectedFuncti } } """, "GetDefaultQux", "Foo")] - public async Task CreatePlanWithComplexTypesDefinitionsAsync(bool useChatModel, string goal, string expectedSectionHeader, string expectedTypeHeader, string expectedFunction, string expectedPlugin) + public async Task CreatePlanWithComplexTypesDefinitionsAsync(string goal, string expectedSectionHeader, string expectedTypeHeader, string expectedFunction, string expectedPlugin) { // Arrange bool useEmbeddings = false; - var kernel = this.InitializeKernel(useEmbeddings, useChatModel); + var kernel = this.InitializeKernel(useEmbeddings); kernel.ImportPluginFromObject(new Foo()); // Act @@ -103,7 +103,7 @@ public async Task CreatePlanWithComplexTypesDefinitionsAsync(bool useChatModel, ); } - private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = true) + private Kernel InitializeKernel(bool useEmbeddings = false) { AzureOpenAIConfiguration? azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(azureOpenAIConfiguration); @@ -113,22 +113,11 @@ private Kernel InitializeKernel(bool useEmbeddings = false, bool useChatModel = IKernelBuilder builder = Kernel.CreateBuilder(); - if (useChatModel) - { - builder.Services.AddAzureOpenAIChatCompletion( - deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, - modelId: azureOpenAIConfiguration.ChatModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey); - } - else - { - builder.Services.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, - endpoint: azureOpenAIConfiguration.Endpoint, - apiKey: azureOpenAIConfiguration.ApiKey); - } + builder.Services.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName!, + modelId: azureOpenAIConfiguration.ChatModelId, + endpoint: azureOpenAIConfiguration.Endpoint, + apiKey: azureOpenAIConfiguration.ApiKey); if (useEmbeddings) { diff --git a/dotnet/src/IntegrationTests/PromptTests.cs b/dotnet/src/IntegrationTests/PromptTests.cs index 7b252713d24c..4649b7b47fcd 100644 --- a/dotnet/src/IntegrationTests/PromptTests.cs +++ b/dotnet/src/IntegrationTests/PromptTests.cs @@ -9,7 +9,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.PromptTemplates.Handlebars; -using SemanticKernel.IntegrationTests.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; using Xunit.Abstractions; @@ -27,7 +26,7 @@ public PromptTests(ITestOutputHelper output) .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) .AddEnvironmentVariables() - .AddUserSecrets() + .AddUserSecrets() .Build(); this._kernelBuilder = Kernel.CreateBuilder(); @@ -76,14 +75,13 @@ private void ConfigureAzureOpenAI(IKernelBuilder kernelBuilder) var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); Assert.NotNull(azureOpenAIConfiguration); - Assert.NotNull(azureOpenAIConfiguration.DeploymentName); + Assert.NotNull(azureOpenAIConfiguration.ChatDeploymentName); Assert.NotNull(azureOpenAIConfiguration.Endpoint); Assert.NotNull(azureOpenAIConfiguration.ApiKey); Assert.NotNull(azureOpenAIConfiguration.ServiceId); - kernelBuilder.AddAzureOpenAITextGeneration( - deploymentName: azureOpenAIConfiguration.DeploymentName, - modelId: azureOpenAIConfiguration.ModelId, + kernelBuilder.AddAzureOpenAIChatCompletion( + deploymentName: azureOpenAIConfiguration.ChatDeploymentName, endpoint: azureOpenAIConfiguration.Endpoint, apiKey: azureOpenAIConfiguration.ApiKey, serviceId: azureOpenAIConfiguration.ServiceId); diff --git a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj index cd5be49a67cb..7ac522bca663 100644 --- a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj +++ b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj @@ -13,6 +13,6 @@ Empowers app owners to integrate cutting-edge LLM technology quickly and easily - + \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelBuilderTests.cs b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelBuilderTests.cs index dc9db68b5836..31ceeac6015a 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Functions/KernelBuilderTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Functions/KernelBuilderTests.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.TextGeneration; using Xunit; @@ -109,7 +110,7 @@ public void ItBuildsServicesIntoKernel() { var builder = Kernel.CreateBuilder() .AddOpenAIChatCompletion(modelId: "abcd", apiKey: "efg", serviceId: "openai") - .AddAzureOpenAITextGeneration(deploymentName: "hijk", modelId: "qrs", endpoint: "https://lmnop", apiKey: "tuv", serviceId: "azureopenai"); + .AddAzureOpenAIChatCompletion(deploymentName: "hijk", modelId: "qrs", endpoint: "https://lmnop", apiKey: "tuv", serviceId: "azureopenai"); builder.Services.AddSingleton(CultureInfo.InvariantCulture); builder.Services.AddSingleton(CultureInfo.CurrentCulture); @@ -118,10 +119,10 @@ public void ItBuildsServicesIntoKernel() Kernel kernel = builder.Build(); Assert.IsType(kernel.GetRequiredService("openai")); - Assert.IsType(kernel.GetRequiredService("azureopenai")); + Assert.IsType(kernel.GetRequiredService("azureopenai")); Assert.Equal(2, kernel.GetAllServices().Count()); - Assert.Single(kernel.GetAllServices()); + Assert.Equal(2, kernel.GetAllServices().Count()); Assert.Equal(3, kernel.GetAllServices().Count()); } diff --git a/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj b/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj index 3cbaf6b60797..af4542f55a2b 100644 --- a/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj +++ b/dotnet/src/SemanticKernel.UnitTests/SemanticKernel.UnitTests.csproj @@ -32,7 +32,7 @@ - + From ef92c71dcbf5b5e72144847ad9626270f0319f9a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 09:13:27 -0700 Subject: [PATCH 120/226] Update based on merge and PR input --- .../ChatCompletion_FunctionTermination.cs | 5 ++- .../Concepts/Agents/MixedChat_Agents.cs | 2 +- .../Agents/OpenAIAssistant_ChartMaker.cs | 2 +- .../Agents/OpenAIAssistant_CodeInterpreter.cs | 2 +- .../OpenAIAssistant_FileManipulation.cs | 2 +- .../Agents/OpenAIAssistant_FileSearch.cs | 2 +- .../GettingStartedWithAgents/Step2_Plugins.cs | 3 +- .../Step8_OpenAIAssistant.cs | 2 +- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 1 - .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 +-- .../OpenAI/OpenAIAssistantDefinition.cs | 2 +- .../Agents/UnitTests/Agents.UnitTests.csproj | 1 + .../OpenAI/OpenAIAssistantAgentTests.cs | 32 +++++++++---------- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 6 ++-- .../Agents/ChatCompletionAgentTests.cs | 2 +- .../Agents/OpenAIAssistantAgentTests.cs | 4 +-- .../IntegrationTests/IntegrationTests.csproj | 4 +-- 17 files changed, 37 insertions(+), 39 deletions(-) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Agents/ChatCompletionAgentTests.cs (98%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Agents/OpenAIAssistantAgentTests.cs (97%) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs index f90f38587131..f344dae432b9 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -3,7 +3,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Agents; @@ -23,7 +22,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() { Instructions = "Answer questions about the menu.", Kernel = CreateKernelWithChatCompletion(), - ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; KernelPlugin plugin = KernelPluginFactory.CreateFromType(); @@ -76,7 +75,7 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync() { Instructions = "Answer questions about the menu.", Kernel = CreateKernelWithChatCompletion(), - ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; KernelPlugin plugin = KernelPluginFactory.CreateFromType(); diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index 20769fa030b7..91add34e8693 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -52,7 +52,7 @@ await OpenAIAssistantAgent.CreateAsync( { Instructions = CopyWriterInstructions, Name = CopyWriterName, - ModelName = this.Model, + ModelId = this.Model, }); // Create a chat for agent interaction. diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index ce1f05a8b08b..531e47b8ec0b 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -33,7 +33,7 @@ await OpenAIAssistantAgent.CreateAsync( Instructions = AgentInstructions, Name = AgentName, EnableCodeInterpreter = true, - ModelName = this.Model, + ModelId = this.Model, }); // Create a chat for agent interaction. diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs index b3090007e0e4..eb5169f40b3f 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs @@ -24,7 +24,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { EnableCodeInterpreter = true, // Enable code-interpreter - ModelName = this.Model, + ModelId = this.Model, }); // Create a chat for agent interaction. diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index ff4f26a92ba6..3de5b2d4f3ff 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -41,7 +41,7 @@ await OpenAIAssistantAgent.CreateAsync( { CodeInterpterFileIds = [uploadFile.Id], EnableCodeInterpreter = true, // Enable code-interpreter - ModelName = this.Model, + ModelId = this.Model, }); // Create a chat for agent interaction. diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 9fe775e0b50e..550615c6bf3e 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -47,7 +47,7 @@ await OpenAIAssistantAgent.CreateAsync( config, new() { - ModelName = this.Model, + ModelId = this.Model, VectorStoreId = vectorStore.Id, }); diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs index eb5ed86a77e3..38741bbb2e7c 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs @@ -3,7 +3,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; namespace GettingStarted; @@ -27,7 +26,7 @@ public async Task UseChatCompletionWithPluginAgentAsync() Instructions = HostInstructions, Name = HostName, Kernel = this.CreateKernelWithChatCompletion(), - ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 964e9671eda2..5e5aa604a77a 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -31,7 +31,7 @@ await OpenAIAssistantAgent.CreateAsync( { Instructions = HostInstructions, Name = HostName, - ModelName = this.Model, + ModelId = this.Model, }); // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 94c914c88eba..fa73edc77cde 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -29,7 +29,6 @@ - diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 164eceb5aff5..17ce8d4e1685 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -68,7 +68,7 @@ public static async Task CreateAsync( // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); - Assistant model = await client.CreateAssistantAsync(definition.ModelName, assistantCreationOptions, cancellationToken).ConfigureAwait(false); + Assistant model = await client.CreateAssistantAsync(definition.ModelId, assistantCreationOptions, cancellationToken).ConfigureAwait(false); // Instantiate the agent return @@ -311,7 +311,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod CodeInterpterFileIds = fileIds, EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), Metadata = model.Metadata, - ModelName = model.Model, + ModelId = model.Model, EnableJsonResponse = enableJsonResponse, TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index ae66aab1502b..d16ff4dbb091 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -11,7 +11,7 @@ public sealed class OpenAIAssistantDefinition /// /// Identifies the AI model targeted by the agent. /// - public string ModelName { get; init; } = string.Empty; + public string ModelId { get; init; } = string.Empty; /// /// The description of the assistant. diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 96938cff3129..6b9fea49fde2 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -32,6 +32,7 @@ + diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index c61cc99093e4..2bf7a8e2dd1c 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -35,7 +35,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", }; await this.VerifyAgentCreationAsync(definition); @@ -51,7 +51,7 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", Name = "testname", Description = "testdescription", Instructions = "testinstructions", @@ -70,7 +70,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", EnableCodeInterpreter = true, }; @@ -87,7 +87,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", EnableCodeInterpreter = true, CodeInterpterFileIds = ["file1", "file2"], }; @@ -105,7 +105,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", VectorStoreId = "#vs1", }; @@ -122,7 +122,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", Metadata = new Dictionary() { { "a", "1" }, @@ -143,7 +143,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", EnableJsonResponse = true, }; @@ -160,7 +160,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", Temperature = 2.0F, }; @@ -177,7 +177,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", TopP = 2.0F, }; @@ -194,7 +194,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsy OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", ExecutionOptions = new(), }; @@ -211,7 +211,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", ExecutionOptions = new() { @@ -233,7 +233,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAnd OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", ExecutionOptions = new(), Metadata = new Dictionary() { @@ -254,7 +254,7 @@ public async Task VerifyOpenAIAssistantAgentRetrievalAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); @@ -535,7 +535,7 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs Assert.NotNull(agent.Id); Assert.False(agent.IsDeleted); Assert.NotNull(agent.Definition); - Assert.Equal(sourceDefinition.ModelName, agent.Definition.ModelName); + Assert.Equal(sourceDefinition.ModelId, agent.Definition.ModelId); // Verify core properties Assert.Equal(sourceDefinition.Instructions ?? string.Empty, agent.Instructions); @@ -604,7 +604,7 @@ private Task CreateAgentAsync() OpenAIAssistantDefinition definition = new() { - ModelName = "testmodel", + ModelId = "testmodel", }; this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); @@ -663,7 +663,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) builder.AppendLine(@$" ""name"": ""{definition.Name}"","); builder.AppendLine(@$" ""description"": ""{definition.Description}"","); builder.AppendLine(@$" ""instructions"": ""{definition.Instructions}"","); - builder.AppendLine(@$" ""model"": ""{definition.ModelName}"","); + builder.AppendLine(@$" ""model"": ""{definition.ModelId}"","); bool hasCodeInterpreter = definition.EnableCodeInterpreter; bool hasCodeInterpreterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index bf38207c3026..32bab0ac0609 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -19,7 +19,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() OpenAIAssistantDefinition definition = new(); Assert.Equal(string.Empty, definition.Id); - Assert.Equal(string.Empty, definition.ModelName); + Assert.Equal(string.Empty, definition.ModelId); Assert.Null(definition.Name); Assert.Null(definition.Instructions); Assert.Null(definition.Description); @@ -44,7 +44,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() { Id = "testid", Name = "testname", - ModelName = "testmodel", + ModelId = "testmodel", Instructions = "testinstructions", Description = "testdescription", VectorStoreId = "#vs", @@ -66,7 +66,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("testid", definition.Id); Assert.Equal("testname", definition.Name); - Assert.Equal("testmodel", definition.ModelName); + Assert.Equal("testmodel", definition.ModelId); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); Assert.Equal("#vs", definition.VectorStoreId); diff --git a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs similarity index 98% rename from dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs rename to dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs index e6f06b766053..4d5302f5ae03 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -13,7 +13,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTests.Agents.OpenAI; +namespace SemanticKernel.IntegrationTests.Agents; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs similarity index 97% rename from dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs rename to dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index 7e34c4ebe65e..f7dee91db903 100644 --- a/dotnet/src/IntegrationTestsV2/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -11,7 +11,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTests.Agents.OpenAI; +namespace SemanticKernel.IntegrationTests.Agents; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. @@ -79,7 +79,7 @@ await OpenAIAssistantAgent.CreateAsync( new() { Instructions = "Answer questions about the menu.", - ModelName = modelName, + ModelId = modelName, }); AgentGroupChat chat = new(); diff --git a/dotnet/src/IntegrationTests/IntegrationTests.csproj b/dotnet/src/IntegrationTests/IntegrationTests.csproj index 3adeaaffd2c1..6abbb8eb3020 100644 --- a/dotnet/src/IntegrationTests/IntegrationTests.csproj +++ b/dotnet/src/IntegrationTests/IntegrationTests.csproj @@ -62,8 +62,8 @@ - + + From 21a905f37d59ac32f1d818dde3e8f30acf43836b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 09:20:58 -0700 Subject: [PATCH 121/226] Merge new agent samples --- dotnet/samples/Concepts/Agents/MixedChat_Files.cs | 2 ++ dotnet/samples/Concepts/Agents/MixedChat_Images.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index 5d96de68da72..b95c6efca36d 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -25,6 +25,7 @@ public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeFileAndGenerateReportAsync() { +#pragma warning disable CS0618 // Type or member is obsolete OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); OpenAIFileReference uploadFile = @@ -95,5 +96,6 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) } } } +#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 385577573ac6..36b96fc4be54 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -27,6 +27,7 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeDataAndGenerateChartAsync() { +#pragma warning disable CS0618 // Type or member is obsolete OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); // Define the agents @@ -108,5 +109,6 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) } } } +#pragma warning restore CS0618 // Type or member is obsolete } } From afda06119bdc8aa9adf9f8d46b892e0e4f2c8f85 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 09:30:41 -0700 Subject: [PATCH 122/226] Migrate new samples --- .../Concepts/Agents/MixedChat_Files.cs | 34 +++++++++++-------- .../Concepts/Agents/MixedChat_Images.cs | 21 +++++++----- 2 files changed, 33 insertions(+), 22 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index b95c6efca36d..52b8b1920afa 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -4,7 +4,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -25,13 +25,15 @@ public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeFileAndGenerateReportAsync() { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIServiceConfiguration config = GetOpenAIConfiguration(); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("30-user-context.txt"), mimeType: "text/plain"), - new OpenAIFileUploadExecutionSettings("30-user-context.txt", OpenAIFilePurpose.Assistants)); + FileClient fileClient = config.CreateFileClient(); + + OpenAIFileInfo uploadFile = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("30-user-context.txt")), + "30-user-context.txt", + FileUploadPurpose.Assistants); Console.WriteLine(this.ApiKey); @@ -39,12 +41,12 @@ await fileService.UploadContentAsync( OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config, new() { EnableCodeInterpreter = true, // Enable code-interpreter ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file with assistant + CodeInterpterFileIds = [uploadFile.Id] // Associate uploaded file with assistant }); ChatCompletionAgent summaryAgent = @@ -71,7 +73,7 @@ Create a tab delimited file report of the ordered (descending) frequency distrib finally { await analystAgent.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); + await fileClient.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -90,12 +92,16 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) foreach (AnnotationContent annotation in content.Items.OfType()) { Console.WriteLine($"\t* '{annotation.Quote}' => {annotation.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - Console.WriteLine($"\n{Encoding.Default.GetString(byteContent)}"); + BinaryData fileContent = await fileClient.DownloadFileAsync(annotation.FileId!); + Console.WriteLine($"\n{Encoding.Default.GetString(fileContent.ToArray())}"); } } } -#pragma warning restore CS0618 // Type or member is obsolete } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 36b96fc4be54..cfbcd97c8260 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -3,7 +3,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Files; namespace Agents; @@ -27,14 +27,15 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeDataAndGenerateChartAsync() { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + + FileClient fileClient = config.CreateFileClient(); // Define the agents OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), + config, new() { Instructions = AnalystInstructions, @@ -101,14 +102,18 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) foreach (FileReferenceContent fileReference in message.Items.OfType()) { Console.WriteLine($"\t* Generated image - @{fileReference.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(fileReference.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; + BinaryData fileContent = await fileClient.DownloadFileAsync(fileReference.FileId!); string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png"); - await File.WriteAllBytesAsync($"{filePath}.png", byteContent); + await File.WriteAllBytesAsync($"{filePath}.png", fileContent.ToArray()); Console.WriteLine($"\t* Local path - {filePath}"); } } } -#pragma warning restore CS0618 // Type or member is obsolete } + + private OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } From ea1fa9270311597904926364daf7ab85c116f229 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 10:02:32 -0700 Subject: [PATCH 123/226] Legacy fix --- .../Experimental/Agents/Internal/ChatRun.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs index 1928f219c903..218ef3e3ddfc 100644 --- a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs +++ b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs @@ -163,13 +163,12 @@ private IEnumerable> ExecuteStep(ThreadRunStepModel step, private async Task ProcessFunctionStepAsync(string callId, ThreadRunStepModel.FunctionDetailsModel functionDetails, CancellationToken cancellationToken) { var result = await InvokeFunctionCallAsync().ConfigureAwait(false); - var toolResult = result as string ?? JsonSerializer.Serialize(result); return new ToolResultModel { CallId = callId, - Output = toolResult!, + Output = ParseFunctionResult(result), }; async Task InvokeFunctionCallAsync() @@ -191,4 +190,19 @@ async Task InvokeFunctionCallAsync() return result.GetValue() ?? string.Empty; } } + + private static string ParseFunctionResult(object result) + { + if (result is string stringResult) + { + return stringResult; + } + + if (result is ChatMessageContent messageResult) + { + return messageResult.Content ?? JsonSerializer.Serialize(messageResult); + } + + return JsonSerializer.Serialize(result); + } } From 259b769d92a5c789099aa518459695a87a3b13ee Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 10:23:03 -0700 Subject: [PATCH 124/226] Exception message --- dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs index 4474da62115f..97c3cc978f68 100644 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs @@ -42,7 +42,7 @@ public static OpenAIClient CreateClient(OpenAIServiceConfiguration config) return new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); } - throw new KernelException($"Unsupported configuration type: {config.Type}"); + throw new KernelException($"Unsupported configuration state: {config.Type}. No api-key or credential present."); } case OpenAIServiceConfiguration.OpenAIServiceType.OpenAI: { @@ -50,7 +50,7 @@ public static OpenAIClient CreateClient(OpenAIServiceConfiguration config) return new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); } default: - throw new KernelException($"Unsupported configuration state: {config.Type}"); + throw new KernelException($"Unsupported configuration type: {config.Type}"); } } From 6c6bc5ce06439d91366c9a2e447034b3795f9756 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:02:38 +0100 Subject: [PATCH 125/226] .Net: OpenAI V2 IntegrationTests Merge - Phase 02 (#7453) ### Motivation and Context Merge IntegrationTests for OpenAI and AzureOpenAI back in the main IntegrationTests project. --- dotnet/SK-dotnet.sln | 9 --- .../AzureOpenAIAudioToTextTests.cs | 5 +- .../AzureOpenAIChatCompletionTests.cs | 2 +- ...enAIChatCompletion_FunctionCallingTests.cs | 2 +- ...eOpenAIChatCompletion_NonStreamingTests.cs | 2 +- ...zureOpenAIChatCompletion_StreamingTests.cs | 2 +- .../AzureOpenAITextEmbeddingTests.cs | 2 +- .../AzureOpenAITextToAudioTests.cs | 2 +- .../AzureOpenAITextToImageTests.cs | 2 +- .../OpenAI/OpenAIAudioToTextTests.cs | 3 +- .../OpenAI/OpenAIChatCompletionTests.cs | 2 +- ...enAIChatCompletion_FunctionCallingTests.cs | 2 +- .../OpenAIChatCompletion_NonStreamingTests.cs | 2 +- .../OpenAIChatCompletion_StreamingTests.cs | 2 +- .../OpenAI/OpenAIFileServiceTests.cs | 2 +- .../OpenAI/OpenAITextEmbeddingTests.cs | 0 .../OpenAI/OpenAITextToAudioTests.cs | 0 .../OpenAI/OpenAITextToImageTests.cs | 0 .../serializedChatHistoryV1_15_1.json | 0 dotnet/src/IntegrationTests/TestHelpers.cs | 10 +++ dotnet/src/IntegrationTestsV2/.editorconfig | 6 -- .../IntegrationTestsV2/BaseIntegrationTest.cs | 37 ---------- .../IntegrationTestsV2.csproj | 69 ------------------ .../TestData/test_audio.wav | Bin 222798 -> 0 bytes .../TestData/test_content.txt | 9 --- .../TestData/test_image_001.jpg | Bin 61082 -> 0 bytes dotnet/src/IntegrationTestsV2/TestHelpers.cs | 65 ----------------- .../TestSettings/AzureOpenAIConfiguration.cs | 19 ----- .../TestSettings/OpenAIConfiguration.cs | 15 ---- 29 files changed, 27 insertions(+), 244 deletions(-) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs (95%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs (98%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs (97%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs (95%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs (95%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIAudioToTextTests.cs (93%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIChatCompletionTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs (98%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAIFileServiceTests.cs (99%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAITextEmbeddingTests.cs (100%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAITextToAudioTests.cs (100%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/Connectors/OpenAI/OpenAITextToImageTests.cs (100%) rename dotnet/src/{IntegrationTestsV2 => IntegrationTests}/TestData/serializedChatHistoryV1_15_1.json (100%) delete mode 100644 dotnet/src/IntegrationTestsV2/.editorconfig delete mode 100644 dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs delete mode 100644 dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj delete mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_audio.wav delete mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_content.txt delete mode 100644 dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg delete mode 100644 dotnet/src/IntegrationTestsV2/TestHelpers.cs delete mode 100644 dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs delete mode 100644 dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 3805151b3a33..e3c792ee957c 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -317,8 +317,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2", "src\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2.UnitTests", "src\Connectors\Connectors.OpenAIV2.UnitTests\Connectors.OpenAIV2.UnitTests.csproj", "{A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "IntegrationTestsV2", "src\IntegrationTestsV2\IntegrationTestsV2.csproj", "{FDEB4884-89B9-4656-80A0-57C7464490F7}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI.UnitTests", "src\Connectors\Connectors.AzureOpenAI.UnitTests\Connectors.AzureOpenAI.UnitTests.csproj", "{DB219924-208B-4CDD-8796-EE424689901E}" @@ -815,12 +813,6 @@ Global {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Publish|Any CPU.Build.0 = Debug|Any CPU {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.ActiveCfg = Release|Any CPU {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}.Release|Any CPU.Build.0 = Release|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Debug|Any CPU.Build.0 = Debug|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Publish|Any CPU.Build.0 = Debug|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.ActiveCfg = Release|Any CPU - {FDEB4884-89B9-4656-80A0-57C7464490F7}.Release|Any CPU.Build.0 = Release|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Debug|Any CPU.Build.0 = Debug|Any CPU {6744272E-8326-48CE-9A3F-6BE227A5E777}.Publish|Any CPU.ActiveCfg = Publish|Any CPU @@ -967,7 +959,6 @@ Global {B0B3901E-AF56-432B-8FAA-858468E5D0DF} = {24503383-A8C4-4255-9998-28D70FE8E99A} {8AC4D976-BBBA-44C7-9CFD-567F0B4751D8} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} - {FDEB4884-89B9-4656-80A0-57C7464490F7} = {831DDCA2-7D2C-4C31-80DB-6BDB3E1F7AE0} {6744272E-8326-48CE-9A3F-6BE227A5E777} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {DB219924-208B-4CDD-8796-EE424689901E} = {1B4CBDE0-10C2-4E7D-9CD0-FE7586C96ED1} {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} = {4D3DAE63-41C6-4E1C-A35A-E77BDFC40675} diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs similarity index 95% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs index 3319b4f055e8..e155f6159c9a 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIAudioToTextTests.cs @@ -8,9 +8,10 @@ using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; +using xRetry; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; public sealed class AzureOpenAIAudioToTextTests() { @@ -21,7 +22,7 @@ public sealed class AzureOpenAIAudioToTextTests() .AddUserSecrets() .Build(); - [Fact] + [RetryFact] public async Task AzureOpenAIAudioToTextTestAsync() { // Arrange diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs index 4dcb9d12ebe4..5728632e2886 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletionTests.cs @@ -18,7 +18,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs index 24ba6f2cad4d..aec7320867d2 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_FunctionCallingTests.cs @@ -16,7 +16,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; public sealed class AzureOpenAIChatCompletionFunctionCallingTests : BaseIntegrationTest { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs index 84b1fe1d7ad2..b16a77bf882a 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_NonStreamingTests.cs @@ -13,7 +13,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs similarity index 98% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs index f340064b2ee3..0707f835ad7b 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAIChatCompletion_StreamingTests.cs @@ -12,7 +12,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs similarity index 97% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs index 1fc5678ed564..20f9851a5ad7 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextEmbeddingTests.cs @@ -7,7 +7,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; public sealed class AzureOpenAITextEmbeddingTests { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs similarity index 95% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs index 372364ff21ed..c50ce2478001 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToAudioTests.cs @@ -7,7 +7,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; public sealed class AzureOpenAITextToAudioTests { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs similarity index 95% rename from dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs rename to dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs index 08e2599fd51e..1374ed860f2f 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/AzureOpenAI/AzureOpenAITextToImageTests.cs @@ -7,7 +7,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.AzureOpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.AzureOpenAI; public sealed class AzureOpenAITextToImageTests { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs similarity index 93% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs index f1ead5f9b9c5..90375307c533 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIAudioToTextTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIAudioToTextTests.cs @@ -8,6 +8,7 @@ using Microsoft.SemanticKernel.AudioToText; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; +using xRetry; using Xunit; namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; @@ -21,7 +22,7 @@ public sealed class OpenAIAudioToTextTests() .AddUserSecrets() .Build(); - [Fact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [RetryFact]//(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] public async Task OpenAIAudioToTextTestAsync() { // Arrange diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletionTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletionTests.cs index cb4fce766456..d3941f7d3515 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletionTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletionTests.cs @@ -18,7 +18,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs index 4a3746dbca99..5f22dd019ca8 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_FunctionCallingTests.cs @@ -15,7 +15,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; public sealed class OpenAIChatCompletionFunctionCallingTests : BaseIntegrationTest { diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs index 54be93609b8d..4d8f3ac7914d 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs @@ -13,7 +13,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs similarity index 98% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs index 5a3145b5881f..342c6ed6f93f 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_StreamingTests.cs @@ -12,7 +12,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs similarity index 99% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs index 5e1f01055080..b0dc71c09eb7 100644 --- a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAIFileServiceTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIFileServiceTests.cs @@ -11,7 +11,7 @@ using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -namespace SemanticKernel.IntegrationTestsV2.Connectors.OpenAI; +namespace SemanticKernel.IntegrationTests.Connectors.OpenAI; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs similarity index 100% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextEmbeddingTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextEmbeddingTests.cs diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs similarity index 100% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToAudioTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToAudioTests.cs diff --git a/dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs similarity index 100% rename from dotnet/src/IntegrationTestsV2/Connectors/OpenAI/OpenAITextToImageTests.cs rename to dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAITextToImageTests.cs diff --git a/dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json b/dotnet/src/IntegrationTests/TestData/serializedChatHistoryV1_15_1.json similarity index 100% rename from dotnet/src/IntegrationTestsV2/TestData/serializedChatHistoryV1_15_1.json rename to dotnet/src/IntegrationTests/TestData/serializedChatHistoryV1_15_1.json diff --git a/dotnet/src/IntegrationTests/TestHelpers.cs b/dotnet/src/IntegrationTests/TestHelpers.cs index e790aa1ca26b..5b42d2884377 100644 --- a/dotnet/src/IntegrationTests/TestHelpers.cs +++ b/dotnet/src/IntegrationTests/TestHelpers.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Reflection; using Microsoft.SemanticKernel; +using Xunit; namespace SemanticKernel.IntegrationTests; @@ -52,4 +53,13 @@ internal static IReadOnlyKernelPluginCollection ImportSamplePromptFunctions(Kern from pluginName in pluginNames select kernel.ImportPluginFromPromptDirectory(Path.Combine(parentDirectory, pluginName))); } + + internal static void AssertChatErrorExcuseMessage(string content) + { + string[] errors = ["error", "difficult", "unable"]; + + var matchesAny = errors.Any(e => content.Contains(e, StringComparison.InvariantCultureIgnoreCase)); + + Assert.True(matchesAny); + } } diff --git a/dotnet/src/IntegrationTestsV2/.editorconfig b/dotnet/src/IntegrationTestsV2/.editorconfig deleted file mode 100644 index 394eef685f21..000000000000 --- a/dotnet/src/IntegrationTestsV2/.editorconfig +++ /dev/null @@ -1,6 +0,0 @@ -# Suppressing errors for Test projects under dotnet folder -[*.cs] -dotnet_diagnostic.CA2007.severity = none # Do not directly await a Task -dotnet_diagnostic.VSTHRD111.severity = none # Use .ConfigureAwait(bool) is hidden by default, set to none to prevent IDE from changing on autosave -dotnet_diagnostic.CS1591.severity = none # Missing XML comment for publicly visible type or member -dotnet_diagnostic.IDE1006.severity = warning # Naming rule violations diff --git a/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs b/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs deleted file mode 100644 index a86274d4f8ce..000000000000 --- a/dotnet/src/IntegrationTestsV2/BaseIntegrationTest.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Net; -using System.Threading.Tasks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Http.Resilience; -using Microsoft.SemanticKernel; - -namespace SemanticKernel.IntegrationTestsV2; - -public class BaseIntegrationTest -{ - protected IKernelBuilder CreateKernelBuilder() - { - var builder = Kernel.CreateBuilder(); - - builder.Services.ConfigureHttpClientDefaults(c => - { - c.AddStandardResilienceHandler().Configure(o => - { - o.Retry.ShouldRetryAfterHeader = true; - o.Retry.ShouldHandle = args => ValueTask.FromResult(args.Outcome.Result?.StatusCode is HttpStatusCode.TooManyRequests); - o.CircuitBreaker = new HttpCircuitBreakerStrategyOptions - { - SamplingDuration = TimeSpan.FromSeconds(40.0), // The duration should be least double of an attempt timeout - }; - o.AttemptTimeout = new HttpTimeoutStrategyOptions - { - Timeout = TimeSpan.FromSeconds(20.0) // Doubling the default 10s timeout - }; - }); - }); - - return builder; - } -} diff --git a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj b/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj deleted file mode 100644 index 3d564cd8aad2..000000000000 --- a/dotnet/src/IntegrationTestsV2/IntegrationTestsV2.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - IntegrationTests - SemanticKernel.IntegrationTestsV2 - net8.0 - true - false - $(NoWarn);CA2007,CA1861,VSTHRD111,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0110 - b7762d10-e29b-4bb1-8b74-b6d69a667dd4 - - - - - - - - - - - - - - - - - - - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - - - - - Always - - - Always - - - Always - - - - - - Always - - - - \ No newline at end of file diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav b/dotnet/src/IntegrationTestsV2/TestData/test_audio.wav deleted file mode 100644 index c6d0edd9a93178162afd3446a32be7cccb822743..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 222798 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%0AVO=q3r{Tu-u_WnIi}k?YCb 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 z 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

w(#uq=ZnfnEQq-^Yei1L6g9(J~XOt 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 diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_content.txt b/dotnet/src/IntegrationTestsV2/TestData/test_content.txt deleted file mode 100644 index 447ce0649e56..000000000000 --- a/dotnet/src/IntegrationTestsV2/TestData/test_content.txt +++ /dev/null @@ -1,9 +0,0 @@ -Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Amet dictum sit amet justo donec enim diam vulputate ut. Nibh ipsum consequat nisl vel pretium lectus. Urna nec tincidunt praesent semper feugiat. Tristique nulla aliquet enim tortor. Ut morbi tincidunt augue interdum velit euismod in pellentesque massa. Ullamcorper morbi tincidunt ornare massa eget egestas purus viverra. Commodo ullamcorper a lacus vestibulum sed arcu non. Volutpat ac tincidunt vitae semper quis lectus nulla. Sem nulla pharetra diam sit amet nisl. Viverra aliquet eget sit amet tellus cras adipiscing enim eu. - -Morbi blandit cursus risus at ultrices mi tempus. Sagittis orci a scelerisque purus. Iaculis nunc sed augue lacus viverra. Accumsan sit amet nulla facilisi morbi tempus iaculis. Nisl rhoncus mattis rhoncus urna neque. Commodo odio aenean sed adipiscing diam donec adipiscing tristique. Tristique senectus et netus et malesuada fames. Nascetur ridiculus mus mauris vitae ultricies leo integer. Ut sem viverra aliquet eget. Sed egestas egestas fringilla phasellus faucibus scelerisque. - -In tellus integer feugiat scelerisque varius morbi. Vitae proin sagittis nisl rhoncus mattis rhoncus urna neque. Cum sociis natoque penatibus et magnis dis. Iaculis at erat pellentesque adipiscing commodo elit at imperdiet dui. Praesent semper feugiat nibh sed pulvinar proin gravida hendrerit lectus. Consectetur a erat nam at lectus urna. Hac habitasse platea dictumst vestibulum rhoncus est pellentesque elit. Aliquam vestibulum morbi blandit cursus risus at ultrices. Eu non diam phasellus vestibulum lorem sed. Risus pretium quam vulputate dignissim suspendisse in est. Elit scelerisque mauris pellentesque pulvinar pellentesque habitant morbi. At varius vel pharetra vel turpis nunc eget. Aliquam malesuada bibendum arcu vitae. At consectetur lorem donec massa. Mi sit amet mauris commodo. Maecenas volutpat blandit aliquam etiam erat velit. Nullam ac tortor vitae purus faucibus ornare suspendisse. - -Facilisi nullam vehicula ipsum a arcu cursus vitae. Commodo sed egestas egestas fringilla phasellus. Lacus luctus accumsan tortor posuere ac ut consequat. Adipiscing commodo elit at imperdiet dui accumsan sit. Non tellus orci ac auctor augue. Viverra aliquet eget sit amet tellus. Luctus venenatis lectus magna fringilla urna porttitor rhoncus dolor. Mattis enim ut tellus elementum. Nunc sed id semper risus. At augue eget arcu dictum. - -Ullamcorper a lacus vestibulum sed arcu non. Vitae tortor condimentum lacinia quis vel. Dui faucibus in ornare quam viverra. Vel pharetra vel turpis nunc eget. In egestas erat imperdiet sed euismod nisi porta lorem mollis. Lacus vestibulum sed arcu non odio euismod lacinia at quis. Augue mauris augue neque gravida in. Ornare quam viverra orci sagittis. Lacus suspendisse faucibus interdum posuere lorem ipsum. Arcu vitae elementum curabitur vitae nunc sed velit dignissim. Diam quam nulla porttitor massa id neque. Gravida dictum fusce ut placerat orci nulla pellentesque. Mus mauris vitae ultricies leo integer malesuada nunc vel risus. Donec pretium vulputate sapien nec sagittis aliquam. Velit egestas dui id ornare. Sed elementum tempus egestas sed sed risus pretium quam vulputate. \ No newline at end of file diff --git a/dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg b/dotnet/src/IntegrationTestsV2/TestData/test_image_001.jpg deleted file mode 100644 index 4a132825f9d641659e036ad6ebc9ac64abe7b192..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 61082 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 u=Z_`vzx?BYvS5Pa-52tZ+F+r7=vTm~>w#yfi content.Contains(e, StringComparison.InvariantCultureIgnoreCase)); - - Assert.True(matchesAny); - } -} diff --git a/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs b/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs deleted file mode 100644 index 6a15a4c89dd7..000000000000 --- a/dotnet/src/IntegrationTestsV2/TestSettings/AzureOpenAIConfiguration.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace SemanticKernel.IntegrationTests.TestSettings; - -[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", - Justification = "Configuration classes are instantiated through IConfiguration.")] -internal sealed class AzureOpenAIConfiguration(string serviceId, string deploymentName, string endpoint, string apiKey, string? chatDeploymentName = null, string? modelId = null, string? chatModelId = null, string? embeddingModelId = null) -{ - public string ServiceId { get; set; } = serviceId; - public string DeploymentName { get; set; } = deploymentName; - public string ApiKey { get; set; } = apiKey; - public string? ChatDeploymentName { get; set; } = chatDeploymentName ?? deploymentName; - public string ModelId { get; set; } = modelId ?? deploymentName; - public string ChatModelId { get; set; } = chatModelId ?? deploymentName; - public string EmbeddingModelId { get; set; } = embeddingModelId ?? "text-embedding-ada-002"; - public string Endpoint { get; set; } = endpoint; -} diff --git a/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs b/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs deleted file mode 100644 index cb3884e3bdfc..000000000000 --- a/dotnet/src/IntegrationTestsV2/TestSettings/OpenAIConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.Diagnostics.CodeAnalysis; - -namespace SemanticKernel.IntegrationTests.TestSettings; - -[SuppressMessage("Performance", "CA1812:Internal class that is apparently never instantiated", - Justification = "Configuration classes are instantiated through IConfiguration.")] -internal sealed class OpenAIConfiguration(string serviceId, string modelId, string apiKey, string? chatModelId = null) -{ - public string ServiceId { get; set; } = serviceId; - public string ModelId { get; set; } = modelId; - public string? ChatModelId { get; set; } = chatModelId; - public string ApiKey { get; set; } = apiKey; -} From 20c70571380a97855ff75156c07ed8bbbce1bc5e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 25 Jul 2024 13:06:27 -0700 Subject: [PATCH 126/226] Clean-up --- .../src/IntegrationTestsV2/testsettings.json | 97 ------------------- 1 file changed, 97 deletions(-) delete mode 100644 dotnet/src/IntegrationTestsV2/testsettings.json diff --git a/dotnet/src/IntegrationTestsV2/testsettings.json b/dotnet/src/IntegrationTestsV2/testsettings.json deleted file mode 100644 index 66df73f8b7a5..000000000000 --- a/dotnet/src/IntegrationTestsV2/testsettings.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "OpenAI": { - "ServiceId": "gpt-3.5-turbo-instruct", - "ModelId": "gpt-3.5-turbo-instruct", - "ApiKey": "" - }, - "AzureOpenAI": { - "ServiceId": "azure-gpt-35-turbo-instruct", - "DeploymentName": "gpt-35-turbo-instruct", - "ChatDeploymentName": "gpt-4", - "Endpoint": "", - "ApiKey": "" - }, - "OpenAIEmbeddings": { - "ServiceId": "text-embedding-ada-002", - "ModelId": "text-embedding-ada-002", - "ApiKey": "" - }, - "AzureOpenAIEmbeddings": { - "ServiceId": "azure-text-embedding-ada-002", - "DeploymentName": "ada-002", - "Endpoint": "", - "ApiKey": "" - }, - "OpenAITextToAudio": { - "ServiceId": "tts-1", - "ModelId": "tts-1", - "ApiKey": "" - }, - "AzureOpenAITextToAudio": { - "ServiceId": "azure-tts", - "DeploymentName": "tts", - "Endpoint": "", - "ApiKey": "" - }, - "OpenAIAudioToText": { - "ServiceId": "whisper-1", - "ModelId": "whisper-1", - "ApiKey": "" - }, - "AzureOpenAIAudioToText": { - "ServiceId": "azure-whisper", - "DeploymentName": "whisper", - "Endpoint": "", - "ApiKey": "" - }, - "HuggingFace": { - "ApiKey": "" - }, - "GoogleAI": { - "EmbeddingModelId": "embedding-001", - "ApiKey": "", - "Gemini": { - "ModelId": "gemini-1.5-flash", - "VisionModelId": "gemini-1.5-flash" - } - }, - "VertexAI": { - "EmbeddingModelId": "textembedding-gecko@003", - "BearerKey": "", - "Location": "us-central1", - "ProjectId": "", - "Gemini": { - "ModelId": "gemini-1.5-flash", - "VisionModelId": "gemini-1.5-flash" - } - }, - "Bing": { - "ApiKey": "" - }, - "Postgres": { - "ConnectionString": "" - }, - "MongoDB": { - "ConnectionString": "", - "VectorSearchCollection": "dotnetMSKNearestTest.nearestSearch" - }, - "AzureCosmosDB": { - "ConnectionString": "" - }, - "SqlServer": { - "ConnectionString": "" - }, - "Planners": { - "AzureOpenAI": { - "ServiceId": "azure-gpt-35-turbo", - "DeploymentName": "gpt-35-turbo", - "Endpoint": "", - "ApiKey": "" - }, - "OpenAI": { - "ServiceId": "openai-gpt-4", - "ModelId": "gpt-4", - "ApiKey": "" - } - } -} \ No newline at end of file From 718505f3fcedbeaa2a8be6cd5d2b267395965a9a Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 26 Jul 2024 13:42:22 +0100 Subject: [PATCH 127/226] .Net: OpenAI V2 -> OpenAI Renaming - Phase 03 (#7454) This PR brings back the OpenAIV2 to its original name for final adjustments Resolves #6870 --- dotnet/SK-dotnet.sln | 6 +++--- dotnet/samples/Concepts/Concepts.csproj | 2 +- .../CodeInterpreterPlugin/CodeInterpreterPlugin.csproj | 2 +- dotnet/samples/Demos/ContentSafety/ContentSafety.csproj | 2 +- dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj | 2 +- .../StepwisePlannerMigration.csproj | 2 +- dotnet/samples/Demos/TimePlugin/TimePlugin.csproj | 2 +- dotnet/samples/LearnResources/LearnResources.csproj | 2 +- .../Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj | 2 +- .../Connectors.OpenAI.UnitTests.csproj} | 2 +- .../Core/AutoFunctionInvocationFilterTests.cs | 0 .../Core/ClientCoreTests.cs | 0 .../Core/OpenAIChatMessageContentTests.cs | 0 .../Core/OpenAIFunctionTests.cs | 0 .../Core/OpenAIFunctionToolCallTests.cs | 0 .../Core/OpenAIWithDataStreamingChatMessageContentTests.cs | 0 .../Extensions/ChatHistoryExtensionsTests.cs | 0 .../Extensions/KernelBuilderExtensionsTests.cs | 0 .../Extensions/KernelFunctionMetadataExtensionsTests.cs | 0 .../Extensions/OpenAIPluginCollectionExtensionsTests.cs | 0 .../Extensions/ServiceCollectionExtensionsTests.cs | 0 .../Services/OpenAIAudioToTextServiceTests.cs | 0 .../Services/OpenAIChatCompletionServiceTests.cs | 0 .../Services/OpenAIFileServiceTests.cs | 0 .../Services/OpenAITextEmbeddingGenerationServiceTests.cs | 0 .../Services/OpenAITextToAudioServiceTests.cs | 0 .../Services/OpenAITextToImageServiceTests.cs | 0 .../Settings/OpenAIAudioToTextExecutionSettingsTests.cs | 0 .../Settings/OpenAIPromptExecutionSettingsTests.cs | 0 .../Settings/OpenAITextToAudioExecutionSettingsTests.cs | 0 .../chat_completion_invalid_streaming_test_response.txt | 0 ...at_completion_multiple_function_calls_test_response.json | 0 .../chat_completion_single_function_call_test_response.json | 0 ...tion_streaming_multiple_function_calls_test_response.txt | 0 ...pletion_streaming_single_function_call_test_response.txt | 0 .../TestData/chat_completion_streaming_test_response.txt | 0 .../TestData/chat_completion_test_response.json | 0 .../chat_completion_with_data_streaming_test_response.txt | 0 .../TestData/chat_completion_with_data_test_response.json | 0 .../filters_multiple_function_calls_test_response.json | 0 ...ters_streaming_multiple_function_calls_test_response.txt | 0 .../TestData/text-embeddings-multiple-response.txt | 0 .../TestData/text-embeddings-response.txt | 0 .../TestData/text-to-image-response.txt | 0 .../ToolCallBehaviorTests.cs | 0 .../Connectors.OpenAI.csproj} | 2 +- .../Core/ClientCore.AudioToText.cs | 0 .../Core/ClientCore.ChatCompletion.cs | 0 .../Core/ClientCore.Embeddings.cs | 0 .../Core/ClientCore.TextToAudio.cs | 0 .../Core/ClientCore.TextToImage.cs | 0 .../Core/ClientCore.cs | 0 .../Core/OpenAIChatMessageContent.cs | 0 .../Core/OpenAIFunction.cs | 0 .../Core/OpenAIFunctionToolCall.cs | 0 .../Core/OpenAIStreamingChatMessageContent.cs | 0 .../Extensions/ChatHistoryExtensions.cs | 0 .../Extensions/OpenAIKernelBuilderExtensions.cs | 0 .../Extensions/OpenAIKernelFunctionMetadataExtensions.cs | 0 .../Extensions/OpenAIMemoryBuilderExtensions.cs | 0 .../Extensions/OpenAIPluginCollectionExtensions.cs | 0 .../Extensions/OpenAIServiceCollectionExtensions.cs | 0 .../Models/OpenAIFilePurpose.cs | 0 .../Models/OpenAIFileReference.cs | 0 .../Services/OpenAIAudioToTextService.cs | 0 .../Services/OpenAIChatCompletionService.cs | 0 .../Services/OpenAIFileService.cs | 0 .../Services/OpenAITextEmbbedingGenerationService.cs | 0 .../Services/OpenAITextToAudioService.cs | 0 .../Services/OpenAITextToImageService.cs | 0 .../Settings/OpenAIAudioToTextExecutionSettings.cs | 0 .../Settings/OpenAIFileUploadExecutionSettings.cs | 0 .../Settings/OpenAIPromptExecutionSettings.cs | 0 .../Settings/OpenAITextToAudioExecutionSettings.cs | 2 +- .../ToolCallBehavior.cs | 0 .../Functions.Prompty.UnitTests.csproj | 2 +- dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj | 2 +- 77 files changed, 16 insertions(+), 16 deletions(-) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj => Connectors.OpenAI.UnitTests/Connectors.OpenAI.UnitTests.csproj} (97%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/AutoFunctionInvocationFilterTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/ClientCoreTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/OpenAIChatMessageContentTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/OpenAIFunctionTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/OpenAIFunctionToolCallTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Core/OpenAIWithDataStreamingChatMessageContentTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Extensions/ChatHistoryExtensionsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Extensions/KernelBuilderExtensionsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Extensions/KernelFunctionMetadataExtensionsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Extensions/OpenAIPluginCollectionExtensionsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Extensions/ServiceCollectionExtensionsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAIAudioToTextServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAIChatCompletionServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAIFileServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAITextEmbeddingGenerationServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAITextToAudioServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Services/OpenAITextToImageServiceTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Settings/OpenAIAudioToTextExecutionSettingsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Settings/OpenAIPromptExecutionSettingsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/Settings/OpenAITextToAudioExecutionSettingsTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_invalid_streaming_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_multiple_function_calls_test_response.json (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_single_function_call_test_response.json (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_streaming_single_function_call_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_streaming_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_test_response.json (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_with_data_streaming_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/chat_completion_with_data_test_response.json (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/filters_multiple_function_calls_test_response.json (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/filters_streaming_multiple_function_calls_test_response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/text-embeddings-multiple-response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/text-embeddings-response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/TestData/text-to-image-response.txt (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2.UnitTests => Connectors.OpenAI.UnitTests}/ToolCallBehaviorTests.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2/Connectors.OpenAIV2.csproj => Connectors.OpenAI/Connectors.OpenAI.csproj} (94%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.AudioToText.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.ChatCompletion.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.Embeddings.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.TextToAudio.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.TextToImage.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/ClientCore.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/OpenAIChatMessageContent.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/OpenAIFunction.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/OpenAIFunctionToolCall.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Core/OpenAIStreamingChatMessageContent.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/ChatHistoryExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/OpenAIKernelBuilderExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/OpenAIKernelFunctionMetadataExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/OpenAIMemoryBuilderExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/OpenAIPluginCollectionExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Extensions/OpenAIServiceCollectionExtensions.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Models/OpenAIFilePurpose.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Models/OpenAIFileReference.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAIAudioToTextService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAIChatCompletionService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAIFileService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAITextEmbbedingGenerationService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAITextToAudioService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Services/OpenAITextToImageService.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Settings/OpenAIAudioToTextExecutionSettings.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Settings/OpenAIFileUploadExecutionSettings.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Settings/OpenAIPromptExecutionSettings.cs (100%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/Settings/OpenAITextToAudioExecutionSettings.cs (99%) rename dotnet/src/Connectors/{Connectors.OpenAIV2 => Connectors.OpenAI}/ToolCallBehavior.cs (100%) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index e3c792ee957c..fa7d9fbd3007 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -90,7 +90,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{5C246969-D ProjectSection(SolutionItems) = preProject src\InternalUtilities\test\AssertExtensions.cs = src\InternalUtilities\test\AssertExtensions.cs src\InternalUtilities\test\HttpMessageHandlerStub.cs = src\InternalUtilities\test\HttpMessageHandlerStub.cs - src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs = src\Connectors\Connectors.OpenAIV2.UnitTests\Utils\MoqExtensions.cs + src\Connectors\Connectors.OpenAI.UnitTests\Utils\MoqExtensions.cs = src\Connectors\Connectors.OpenAI.UnitTests\Utils\MoqExtensions.cs src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs = src\InternalUtilities\test\MultipleHttpMessageHandlerStub.cs src\InternalUtilities\test\TestInternalUtilities.props = src\InternalUtilities\test\TestInternalUtilities.props EndProjectSection @@ -313,9 +313,9 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TimePlugin", "samples\Demos EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Memory.AzureCosmosDBNoSQL", "src\Connectors\Connectors.Memory.AzureCosmosDBNoSQL\Connectors.Memory.AzureCosmosDBNoSQL.csproj", "{B0B3901E-AF56-432B-8FAA-858468E5D0DF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2", "src\Connectors\Connectors.OpenAIV2\Connectors.OpenAIV2.csproj", "{8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAI", "src\Connectors\Connectors.OpenAI\Connectors.OpenAI.csproj", "{8AC4D976-BBBA-44C7-9CFD-567F0B4751D8}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAIV2.UnitTests", "src\Connectors\Connectors.OpenAIV2.UnitTests\Connectors.OpenAIV2.UnitTests.csproj", "{A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.OpenAI.UnitTests", "src\Connectors\Connectors.OpenAI.UnitTests\Connectors.OpenAI.UnitTests.csproj", "{A2E659A5-0CE5-4CBF-B9F6-F8604B2AF0BF}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.AzureOpenAI", "src\Connectors\Connectors.AzureOpenAI\Connectors.AzureOpenAI.csproj", "{6744272E-8326-48CE-9A3F-6BE227A5E777}" EndProject diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index aca9ceb8887e..89a9ea004dc5 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -63,7 +63,7 @@ - + diff --git a/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj b/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj index fadc608dbda2..8df5f889470e 100644 --- a/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj +++ b/dotnet/samples/Demos/CodeInterpreterPlugin/CodeInterpreterPlugin.csproj @@ -18,7 +18,7 @@ - + diff --git a/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj b/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj index 7065ed5b64b4..f891f0d85a5c 100644 --- a/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj +++ b/dotnet/samples/Demos/ContentSafety/ContentSafety.csproj @@ -13,7 +13,7 @@ - + diff --git a/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj b/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj index 562d0cc883aa..06dfceda8b48 100644 --- a/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj +++ b/dotnet/samples/Demos/HomeAutomation/HomeAutomation.csproj @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj index a174d3f4a954..abd289077625 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj +++ b/dotnet/samples/Demos/StepwisePlannerMigration/StepwisePlannerMigration.csproj @@ -8,7 +8,7 @@ - + diff --git a/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj b/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj index cbbe6d95b6cc..37a777d6a97e 100644 --- a/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj +++ b/dotnet/samples/Demos/TimePlugin/TimePlugin.csproj @@ -15,7 +15,7 @@ - + diff --git a/dotnet/samples/LearnResources/LearnResources.csproj b/dotnet/samples/LearnResources/LearnResources.csproj index 72cff80ad017..d639fc8a0cee 100644 --- a/dotnet/samples/LearnResources/LearnResources.csproj +++ b/dotnet/samples/LearnResources/LearnResources.csproj @@ -52,7 +52,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index ec2bb48623c3..77f9f612f7eb 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -29,6 +29,6 @@ - + \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Connectors.OpenAI.UnitTests.csproj similarity index 97% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Connectors.OpenAI.UnitTests.csproj index 8ac5c7716e98..e187080a2c35 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Connectors.OpenAIV2.UnitTests.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Connectors.OpenAI.UnitTests.csproj @@ -33,7 +33,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/AutoFunctionInvocationFilterTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/AutoFunctionInvocationFilterTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/AutoFunctionInvocationFilterTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/ClientCoreTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/ClientCoreTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIChatMessageContentTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIChatMessageContentTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIChatMessageContentTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionToolCallTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIFunctionToolCallTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIFunctionToolCallTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Core/OpenAIWithDataStreamingChatMessageContentTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ChatHistoryExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/ChatHistoryExtensionsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelBuilderExtensionsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelBuilderExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelBuilderExtensionsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/KernelFunctionMetadataExtensionsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/OpenAIPluginCollectionExtensionsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Extensions/ServiceCollectionExtensionsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIAudioToTextServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIAudioToTextServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIAudioToTextServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIChatCompletionServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIFileServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAIFileServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIFileServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextEmbeddingGenerationServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextToAudioServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToAudioServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextToAudioServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextToImageServiceTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Services/OpenAITextToImageServiceTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAITextToImageServiceTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIAudioToTextExecutionSettingsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAIPromptExecutionSettingsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Settings/OpenAITextToAudioExecutionSettingsTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_invalid_streaming_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_multiple_function_calls_test_response.json diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_single_function_call_test_response.json rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_single_function_call_test_response.json diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_multiple_function_calls_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_single_function_call_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_streaming_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_streaming_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_test_response.json similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_test_response.json rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_test_response.json diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_with_data_streaming_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/chat_completion_with_data_test_response.json rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/chat_completion_with_data_test_response.json diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_multiple_function_calls_test_response.json rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/filters_multiple_function_calls_test_response.json diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/filters_streaming_multiple_function_calls_test_response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-multiple-response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-embeddings-multiple-response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-embeddings-response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-embeddings-response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-embeddings-response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-to-image-response.txt similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/TestData/text-to-image-response.txt rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/TestData/text-to-image-response.txt diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/ToolCallBehaviorTests.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2.UnitTests/ToolCallBehaviorTests.cs rename to dotnet/src/Connectors/Connectors.OpenAI.UnitTests/ToolCallBehaviorTests.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj similarity index 94% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj rename to dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj index d3466a87a2ea..250ad6b5025e 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Connectors.OpenAIV2.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj @@ -2,7 +2,7 @@ - Microsoft.SemanticKernel.Connectors.OpenAIV2 + Microsoft.SemanticKernel.Connectors.OpenAI $(AssemblyName) net8.0;netstandard2.0 true diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.AudioToText.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.AudioToText.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.AudioToText.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.ChatCompletion.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.Embeddings.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToAudio.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToAudio.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.TextToImage.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/ClientCore.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIChatMessageContent.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIChatMessageContent.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIChatMessageContent.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIFunction.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunction.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIFunction.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIFunctionToolCall.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIFunctionToolCall.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIFunctionToolCall.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Core/OpenAIStreamingChatMessageContent.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Core/OpenAIStreamingChatMessageContent.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/ChatHistoryExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/ChatHistoryExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/ChatHistoryExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelBuilderExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelFunctionMetadataExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIKernelFunctionMetadataExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelFunctionMetadataExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIMemoryBuilderExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIMemoryBuilderExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIMemoryBuilderExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIPluginCollectionExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIPluginCollectionExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIPluginCollectionExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Extensions/OpenAIServiceCollectionExtensions.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs b/dotnet/src/Connectors/Connectors.OpenAI/Models/OpenAIFilePurpose.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFilePurpose.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Models/OpenAIFilePurpose.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs b/dotnet/src/Connectors/Connectors.OpenAI/Models/OpenAIFileReference.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Models/OpenAIFileReference.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Models/OpenAIFileReference.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIAudioToTextService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIAudioToTextService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIAudioToTextService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIChatCompletionService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIChatCompletionService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIFileService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAIFileService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAIFileService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextEmbbedingGenerationService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToAudioService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToAudioService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToAudioService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Services/OpenAITextToImageService.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIAudioToTextExecutionSettings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIAudioToTextExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIAudioToTextExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIFileUploadExecutionSettings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIFileUploadExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIFileUploadExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAIPromptExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAIPromptExecutionSettings.cs diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs similarity index 99% rename from dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs rename to dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs index e805578f8cc6..6e1e6fadff11 100644 --- a/dotnet/src/Connectors/Connectors.OpenAIV2/Settings/OpenAITextToAudioExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. /* Phase 4 -Bringing the OpenAITextToAudioExecutionSettings class to the OpenAIV2 connector as is +Bringing the OpenAITextToAudioExecutionSettings class to the OpenAI connector as is */ diff --git a/dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs b/dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs similarity index 100% rename from dotnet/src/Connectors/Connectors.OpenAIV2/ToolCallBehavior.cs rename to dotnet/src/Connectors/Connectors.OpenAI/ToolCallBehavior.cs diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj b/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj index 750e678395f2..74e77f9544fa 100644 --- a/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj +++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/Functions.Prompty.UnitTests.csproj @@ -27,7 +27,7 @@ - + diff --git a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj b/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj index d6f5f1bb08e1..194753a700ad 100644 --- a/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj +++ b/dotnet/src/Planners/Planners.OpenAI/Planners.OpenAI.csproj @@ -32,7 +32,7 @@ - + From 93bfab40ef741ab757d7406ba07999bdefd370b6 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Sat, 27 Jul 2024 09:07:02 -0700 Subject: [PATCH 128/226] .Net: OpenAI V2 Migration - Apply recommendations (#7471) Resolve #7346 --------- Co-authored-by: Roger Barreto --- dotnet/Directory.Packages.props | 2 +- .../Connectors.AzureOpenAI.csproj | 2 +- .../CompatibilitySuppressions.xml | 1222 +++++++++++++++++ .../Connectors.OpenAI.csproj | 2 +- .../Core/ClientCore.Embeddings.cs | 6 - .../Core/ClientCore.TextToImage.cs | 9 - .../OpenAIKernelBuilderExtensions.cs | 4 - .../OpenAIServiceCollectionExtensions.cs | 8 - .../OpenAITextEmbbedingGenerationService.cs | 4 - .../Services/OpenAITextToImageService.cs | 14 - .../OpenAITextToAudioExecutionSettings.cs | 5 - .../ClientResultExceptionExtensions.cs | 6 - .../Policies/GeneratedActionPipelinePolicy.cs | 6 - .../AI/TextToImage/ITextToImageService.cs | 4 - .../Utilities/OpenAI/MockPipelineResponse.cs | 5 - .../Utilities/OpenAI/MockResponseHeaders.cs | 5 - 16 files changed, 1225 insertions(+), 79 deletions(-) create mode 100644 dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 52e755e4d285..17fc4dd97ac5 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -8,7 +8,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 77f9f612f7eb..b6c4106d5f23 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -24,7 +24,7 @@ - + diff --git a/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml b/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml new file mode 100644 index 000000000000..05e86e2b3f75 --- /dev/null +++ b/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml @@ -0,0 +1,1222 @@ + + + + + CP0001 + T:Microsoft.SemanticKernel.ChatHistoryExtensions + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIAudioToTextService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextEmbeddingGenerationService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextGenerationService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToAudioService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToImageService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataChatMessageContent + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataStreamingChatMessageContent + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingTextContent + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextGenerationService + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.ChatHistoryExtensions + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIAudioToTextService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextEmbeddingGenerationService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextGenerationService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToAudioService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToImageService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataChatMessageContent + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataStreamingChatMessageContent + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingTextContent + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0001 + T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextGenerationService + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings.get_Temperature + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatMessageContent.get_ToolCalls + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFunction.ToFunctionDefinition + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPluginCollectionExtensions.TryGetFunctionAndArguments(Microsoft.SemanticKernel.IReadOnlyKernelPluginCollection,Azure.AI.OpenAI.ChatCompletionsFunctionToolCall,Microsoft.SemanticKernel.KernelFunction@,Microsoft.SemanticKernel.KernelArguments@) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(Microsoft.SemanticKernel.PromptExecutionSettings,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_AzureChatExtensionsOptions + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_FrequencyPenalty + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_PresencePenalty + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_ResultsPerPrompt + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_Temperature + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_TopP + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_AzureChatExtensionsOptions(Azure.AI.OpenAI.AzureChatExtensionsOptions) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_ResultsPerPrompt(System.Int32) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_FinishReason + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_ToolCallUpdate + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextEmbeddingGenerationService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToAudioExecutionSettings.get_Speed + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Int32) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.Uri,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings.get_Temperature + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatMessageContent.get_ToolCalls + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFunction.ToFunctionDefinition + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPluginCollectionExtensions.TryGetFunctionAndArguments(Microsoft.SemanticKernel.IReadOnlyKernelPluginCollection,Azure.AI.OpenAI.ChatCompletionsFunctionToolCall,Microsoft.SemanticKernel.KernelFunction@,Microsoft.SemanticKernel.KernelArguments@) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(Microsoft.SemanticKernel.PromptExecutionSettings,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_AzureChatExtensionsOptions + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_FrequencyPenalty + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_PresencePenalty + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_ResultsPerPrompt + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_Temperature + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_TopP + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_AzureChatExtensionsOptions(Azure.AI.OpenAI.AzureChatExtensionsOptions) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_ResultsPerPrompt(System.Int32) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_FinishReason + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_ToolCallUpdate + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextEmbeddingGenerationService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToAudioExecutionSettings.get_Speed + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Int32) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.Uri,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll + true + + \ No newline at end of file diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj index 250ad6b5025e..7b5586ffca2f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj @@ -7,7 +7,7 @@ net8.0;netstandard2.0 true $(NoWarn);NU5104;SKEXP0001,SKEXP0010 - false + true diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs index 483c726fa959..1476d0b15158 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.Embeddings.cs @@ -1,11 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* -Phase 01 - -This class was created to simplify any Text Embeddings Support from the v1 ClientCore -*/ - using System; using System.ClientModel; using System.Collections.Generic; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs index ac6111088ebf..1cb9c5993eae 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.TextToImage.cs @@ -1,14 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* -Phase 02 - -- This class was created focused in the Image Generation using the SDK client instead of the own client in V1. -- Added Checking for empty or whitespace prompt. -- Removed the format parameter as this is never called in V1 code. Plan to implement it in the future once we change the ITextToImageService abstraction, using PromptExecutionSettings. -- Allow custom size for images when the endpoint is not the default OpenAI v1 endpoint. -*/ - using System.ClientModel; using System.Threading; using System.Threading.Tasks; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs index c713f3076ac3..c322ead2b671 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIKernelBuilderExtensions.cs @@ -1,9 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* Phase 4 -- Added missing OpenAIClient extensions for audio -- Updated the Experimental attribute to the correct value 0001 -> 0010 (Connector) - */ using System; using System.Diagnostics.CodeAnalysis; using System.Net.Http; diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs index 02662815e1d8..ed191d3dda0f 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Extensions/OpenAIServiceCollectionExtensions.cs @@ -18,14 +18,6 @@ namespace Microsoft.SemanticKernel; #pragma warning disable IDE0039 // Use local function -/* Phase 02 -- Add endpoint parameter for both Embedding and TextToImage services extensions. -- Removed unnecessary Validation checks (that are already happening in the service/client constructors) -- Added openAIClient extension for TextToImage service. -- Changed parameters order for TextToImage service extension (modelId comes first). -- Made modelId a required parameter of TextToImage services. - -*/ ///

/// Sponsor extensions class for . /// diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs index ce3cdcab43b8..aa70819020d0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextEmbbedingGenerationService.cs @@ -12,10 +12,6 @@ namespace Microsoft.SemanticKernel.Connectors.OpenAI; -/* Phase 02 -Adding the non-default endpoint parameter to the constructor. -*/ - /// /// OpenAI implementation of /// diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs index 48953d56912b..f51e7d7c0141 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Services/OpenAITextToImageService.cs @@ -8,20 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.TextToImage; -/* Phase 02 -- Breaking the current constructor parameter order to follow the same order as the other services. -- Added custom endpoint support, and removed ApiKey validation, as it is performed by the ClientCore when the Endpoint is not provided. -- Added custom OpenAIClient support. -- Updated "organization" parameter to "organizationId". -- "modelId" parameter is now required in the constructor. - -- Added OpenAIClient breaking glass constructor. - -Phase 08 -- Removed OpenAIClient breaking glass constructor -- Reverted the order and parameter names. -*/ - namespace Microsoft.SemanticKernel.Connectors.OpenAI; /// diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs index 6e1e6fadff11..cfb9cfa39dd0 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Settings/OpenAITextToAudioExecutionSettings.cs @@ -1,10 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* Phase 4 -Bringing the OpenAITextToAudioExecutionSettings class to the OpenAI connector as is - -*/ - using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text.Json; diff --git a/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs index 75cc074b862d..feca5e79618c 100644 --- a/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs +++ b/dotnet/src/InternalUtilities/openai/Extensions/ClientResultExceptionExtensions.cs @@ -1,11 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* -Phase 01: -This class is introduced in exchange for the original RequestExceptionExtensions class of Azure.Core to the new ClientException from System.ClientModel, -Preserved the logic as is. -*/ - using System.ClientModel; using System.Diagnostics.CodeAnalysis; using System.Net; diff --git a/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs b/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs index 931f12957965..8ee5865edc2c 100644 --- a/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs +++ b/dotnet/src/InternalUtilities/openai/Policies/GeneratedActionPipelinePolicy.cs @@ -1,11 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* Phase 03 -Adapted from OpenAI SDK original policy with warning updates. - -Original file: https://github.com/openai/openai-dotnet/blob/0b97311f58dfb28bd883d990f68d548da040a807/src/Utility/GenericActionPipelinePolicy.cs#L8 -*/ - using System; using System.ClientModel.Primitives; using System.Collections.Generic; diff --git a/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs b/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs index b30f78f3c0ca..7370a6eb38ef 100644 --- a/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs +++ b/dotnet/src/SemanticKernel.Abstractions/AI/TextToImage/ITextToImageService.cs @@ -5,10 +5,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel.Services; -/* Phase 02 -- Changing "description" parameter to "prompt" to better match the OpenAI API and avoid confusion. -*/ - namespace Microsoft.SemanticKernel.TextToImage; /// diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs index 2e254c53d04e..d147f1c98df1 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockPipelineResponse.cs @@ -1,10 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* Phase 01 -This class was imported and adapted from the System.ClientModel Unit Tests. -https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockPipelineResponse.cs -*/ - using System; using System.ClientModel.Primitives; using System.IO; diff --git a/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs index 97c9776b4b25..01d698512be5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Utilities/OpenAI/MockResponseHeaders.cs @@ -1,10 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* Phase 01 -This class was imported and adapted from the System.ClientModel Unit Tests. -https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/core/System.ClientModel/tests/TestFramework/Mocks/MockResponseHeaders.cs -*/ - using System; using System.ClientModel.Primitives; using System.Collections.Generic; From 719cce3f0bc4e2691314f91000dd3aa3c34888cf Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 31 Jul 2024 14:00:06 +0100 Subject: [PATCH 129/226] .Net: OpenAI V2 Migration - Small fixes (#7532) ### Motivation and Context Small fixes: - Remove Text Generation from the Package descriptions. --- dotnet/docs/OPENAI-CONNECTOR-MIGRATION.md | 196 ++++++++++++++++++ .../AutoFunctionCallingController.cs | 4 + .../Controllers/StepwisePlannerController.cs | 4 + .../Plugins/TimePlugin.cs | 4 + .../Plugins/WeatherPlugin.cs | 4 + .../Services/IPlanProvider.cs | 4 + .../Services/PlanProvider.cs | 5 + .../Connectors.AzureOpenAI.csproj | 2 +- .../Connectors.OpenAI.csproj | 2 +- 9 files changed, 223 insertions(+), 2 deletions(-) create mode 100644 dotnet/docs/OPENAI-CONNECTOR-MIGRATION.md diff --git a/dotnet/docs/OPENAI-CONNECTOR-MIGRATION.md b/dotnet/docs/OPENAI-CONNECTOR-MIGRATION.md new file mode 100644 index 000000000000..784de5347fb0 --- /dev/null +++ b/dotnet/docs/OPENAI-CONNECTOR-MIGRATION.md @@ -0,0 +1,196 @@ +# OpenAI Connector Migration Guide + +This manual prepares you for the migration of your OpenAI Connector to the new OpenAI Connector. The new OpenAI Connector is a complete rewrite of the existing OpenAI Connector and is designed to be more efficient, reliable, and scalable. This manual will guide you through the migration process and help you understand the changes that have been made to the OpenAI Connector. + +## 1. Package Setup when Using Azure + +If you are working with Azure and or OpenAI public APIs, you will need to change the package from `Microsoft.SemanticKernel.Connectors.OpenAI` to `Microsoft.SemanticKernel.Connectors.AzureOpenAI`, + +> [!IMPORTANT] +> The `Microsoft.SemanticKernel.Connectors.AzureOpenAI` package depends on the `Microsoft.SemanticKernel.Connectors.OpenAI` package so there's no need to add both to your project when using `OpenAI` related types. + +```diff +- // Before +- using Microsoft.SemanticKernel.Connectors.OpenAI; ++ After ++ using Microsoft.SemanticKernel.Connectors.AzureOpenAI; +``` + +### 1.1 AzureOpenAIClient + +When using Azure with OpenAI, before where you were using `OpenAIClient` you will need to update your code to use the new `AzureOpenAIClient` type. + +### 1.2 Services + +All services below now belong to the `Microsoft.SemanticKernel.Connectors.AzureOpenAI` namespace. + +- `AzureOpenAIAudioToTextService` +- `AzureOpenAIChatCompletionService` +- `AzureOpenAITextEmbeddingGenerationService` +- `AzureOpenAITextToAudioService` +- `AzureOpenAITextToImageService` + +## 2. Text Generation Deprecated + +The latest `OpenAI` SDK does not support text generation modality, when migrating to their underlying SDK we had to drop the support and removed `TextGeneration` specific services but the existing `ChatCompletion` ones still supports (implements `ITextGenerationService`). + +If you were using any of the `OpenAITextGenerationService` or `AzureOpenAITextGenerationService` you will need to update your code to target a chat completion model instead, using `OpenAIChatCompletionService` or `AzureOpenAIChatCompletionService` instead. + +> [!NOTE] +> OpenAI and AzureOpenAI `ChatCompletion` services also implement the `ITextGenerationService` interface and that may not require any changes to your code if you were targeting the `ITextGenerationService` interface. + +tags: +`OpenAITextGenerationService`,`AzureOpenAITextGenerationService`, +`AddOpenAITextGeneration`,`AddAzureOpenAITextGeneration` + +## 3. ChatCompletion Multiple Choices Deprecated + +The latest `OpenAI` SDK does not support multiple choices, when migrating to their underlying SDK we had to drop the support and removed `ResultsPerPrompt` also from the `OpenAIPromptExecutionSettings`. + +tags: `ResultsPerPrompt`,`results_per_prompt` + +## 4. OpenAI File Service Deprecation + +The `OpenAIFileService` was deprecated in the latest version of the OpenAI Connector. We strongly recommend to update your code to use the new `OpenAIClient.GetFileClient()` for file management operations. + +## 5. SemanticKernel MetaPackage + +To be retro compatible with the new OpenAI and AzureOpenAI Connectors, our `Microsoft.SemanticKernel` meta package changed its dependency to use the new `Microsoft.SemanticKernel.Connectors.AzureOpenAI` package that depends on the `Microsoft.SemanticKernel.Connectors.OpenAI` package. This way if you are using the metapackage, no change is needed to get access to `Azure` related types. + +## 6. Contents + +### 6.1 OpenAIChatMessageContent + +- The `Tools` property type has changed from `IReadOnlyList` to `IReadOnlyList`. + +- Inner content type has changed from `ChatCompletionsFunctionToolCall` to `ChatToolCall`. + +- Metadata type `FunctionToolCalls` has changed from `IEnumerable` to `IEnumerable`. + +### 6.2 OpenAIStreamingChatMessageContent + +- The `FinishReason` property type has changed from `CompletionsFinishReason` to `FinishReason`. +- The `ToolCallUpdate` property has been renamed to `ToolCallUpdates` and its type has changed from `StreamingToolCallUpdate?` to `IReadOnlyList?`. +- The `AuthorName` property is not initialized because it's not provided by the underlying library anymore. + +## 6.3 Metrics for AzureOpenAI Connector + +The meter `s_meter = new("Microsoft.SemanticKernel.Connectors.OpenAI");` and the relevant counters still have old names that contain "openai" in them, such as: + +- `semantic_kernel.connectors.openai.tokens.prompt` +- `semantic_kernel.connectors.openai.tokens.completion` +- `semantic_kernel.connectors.openai.tokens.total` + +## 7. Using Azure with your data (Data Sources) + +With the new `AzureOpenAIClient`, you can now specify your datasource thru the options and that requires a small change in your code to the new type. + +Before + +```csharp +var promptExecutionSettings = new OpenAIPromptExecutionSettings +{ + AzureChatExtensionsOptions = new AzureChatExtensionsOptions + { + Extensions = [ new AzureSearchChatExtensionConfiguration + { + SearchEndpoint = new Uri(TestConfiguration.AzureAISearch.Endpoint), + Authentication = new OnYourDataApiKeyAuthenticationOptions(TestConfiguration.AzureAISearch.ApiKey), + IndexName = TestConfiguration.AzureAISearch.IndexName + }] + }; +}; +``` + +After + +```csharp +var promptExecutionSettings = new AzureOpenAIPromptExecutionSettings +{ + AzureChatDataSource = new AzureSearchChatDataSource + { + Endpoint = new Uri(TestConfiguration.AzureAISearch.Endpoint), + Authentication = DataSourceAuthentication.FromApiKey(TestConfiguration.AzureAISearch.ApiKey), + IndexName = TestConfiguration.AzureAISearch.IndexName + } +}; +``` + +## 8. Breaking glass scenarios + +Breaking glass scenarios are scenarios where you may need to update your code to use the new OpenAI Connector. Below are some of the breaking changes that you may need to be aware of. + +#### 8.1 KernelContent Metadata + +Some of the keys in the content metadata dictionary have changed, you will need to update your code to when using the previous key names. + +- `Created` -> `CreatedAt` + +#### 8.2 Prompt Filter Results + +The `PromptFilterResults` metadata type has changed from `IReadOnlyList` to `ContentFilterResultForPrompt`. + +#### 8.3 Content Filter Results + +The `ContentFilterResultsForPrompt` type has changed from `ContentFilterResultsForChoice` to `ContentFilterResultForResponse`. + +#### 8.4 Finish Reason + +The FinishReason metadata string value has changed from `stop` to `Stop` + +#### 8.5 Tool Calls + +The ToolCalls metadata string value has changed from `tool_calls` to `ToolCalls` + +#### 8.6 LogProbs / Log Probability Info + +The `LogProbabilityInfo` type has changed from `ChatChoiceLogProbabilityInfo` to `IReadOnlyList`. + +#### 8.7 Finish Details, Index, and Enhancements + +All of above have been removed. + +#### 8.8 Token Usage + +The Token usage naming convention from `OpenAI` changed from `Completion`, `Prompt` tokens to `Output` and `Input` respectively. You will need to update your code to use the new naming. + +The type also changed from `CompletionsUsage` to `ChatTokenUsage`. + +[Example of Token Usage Metadata Changes](https://github.com/microsoft/semantic-kernel/pull/7151/files#diff-a323107b9f8dc8559a83e50080c6e34551ddf6d9d770197a473f249589e8fb47) + +```diff +- Before +- var usage = FunctionResult.Metadata?["Usage"] as CompletionsUsage; +- var completionTokesn = usage?.CompletionTokens ?? 0; +- var promptTokens = usage?.PromptTokens ?? 0; + ++ After ++ var usage = FunctionResult.Metadata?["Usage"] as ChatTokenUsage; ++ var promptTokens = usage?.InputTokens ?? 0; ++ var completionTokens = completionTokens: usage?.OutputTokens ?? 0; + +totalTokens: usage?.TotalTokens ?? 0; +``` + +#### 8.9 OpenAIClient + +The `OpenAIClient` type previously was a Azure specific namespace type but now it is an `OpenAI` SDK namespace type, you will need to update your code to use the new `OpenAIClient` type. + +When using Azure, you will need to update your code to use the new `AzureOpenAIClient` type. + +#### 8.10 Pipeline Configuration + +The new `OpenAI` SDK uses a different pipeline configuration, and has a dependency on `System.ClientModel` package. You will need to update your code to use the new `HttpClientPipelineTransport` transport configuration where before you were using `HttpClientTransport` from `Azure.Core.Pipeline`. + +[Example of Pipeline Configuration](https://github.com/microsoft/semantic-kernel/pull/7151/files#diff-fab02d9a75bf43cb57f71dddc920c3f72882acf83fb125d8cad963a643d26eb3) + +```diff +var clientOptions = new OpenAIClientOptions +{ +- // Before: From Azure.Core.Pipeline +- Transport = new HttpClientTransport(httpClient), + ++ // After: From OpenAI SDK -> System.ClientModel ++ Transport = new HttpClientPipelineTransport(httpClient), +}; +``` diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs index e65f12d59eb0..37a390fee69a 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/AutoFunctionCallingController.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary + using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.SemanticKernel; @@ -9,6 +11,8 @@ using StepwisePlannerMigration.Plugins; using StepwisePlannerMigration.Services; +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Controllers; /// diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs index 7a0041062341..096ce4795fb3 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Controllers/StepwisePlannerController.cs @@ -1,5 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary + using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.SemanticKernel; @@ -9,6 +11,8 @@ using StepwisePlannerMigration.Plugins; using StepwisePlannerMigration.Services; +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Controllers; /// diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs index 1bfdcde9a236..80b976702ed3 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/TimePlugin.cs @@ -1,9 +1,13 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary + using System; using System.ComponentModel; using Microsoft.SemanticKernel; +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Plugins; /// diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/WeatherPlugin.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/WeatherPlugin.cs index dfd72dd36c2c..52658a47e13e 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/WeatherPlugin.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Plugins/WeatherPlugin.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary + using System.ComponentModel; using Microsoft.SemanticKernel; +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Plugins; /// diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Services/IPlanProvider.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Services/IPlanProvider.cs index 4bdae07f6ed7..695a3a18e9c9 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Services/IPlanProvider.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Services/IPlanProvider.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +#pragma warning disable IDE0005 // Using directive is unnecessary + using Microsoft.SemanticKernel.ChatCompletion; +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Services; /// diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs index 033473c3c42b..a61251f9eb49 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs @@ -4,6 +4,8 @@ using System.Text.Json; using Microsoft.SemanticKernel.ChatCompletion; +#pragma warning disable IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Services; /// @@ -17,3 +19,6 @@ public ChatHistory GetPlan(string fileName) return JsonSerializer.Deserialize(plan)!; } } + +#pragma warning restore IDE0005 // Using directive is unnecessary + diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index b6c4106d5f23..e4f5be3d533a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -16,7 +16,7 @@ Semantic Kernel - Azure OpenAI connectors - Semantic Kernel connectors for Azure OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + Semantic Kernel connectors for Azure OpenAI. Contains clients for chat completion, embedding and DALL-E text to image. diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj index 7b5586ffca2f..ecd01d172e11 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj @@ -18,7 +18,7 @@ Semantic Kernel - OpenAI connector - Semantic Kernel connectors for OpenAI. Contains clients for text generation, chat completion, embedding and DALL-E text to image. + Semantic Kernel connectors for OpenAI. Contains clients for chat completion, embedding and DALL-E text to image. From f419ac213c96ba11ab7bdd10e29eec2415ec82aa Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 09:46:19 -0700 Subject: [PATCH 130/226] Clean-up --- .../Concepts/Agents/Legacy_AgentDelegation.cs | 2 +- .../Concepts/Resources/Plugins/MenuPlugin.cs | 34 ------------------- .../Step8_OpenAIAssistant.cs | 6 ---- 3 files changed, 1 insertion(+), 41 deletions(-) delete mode 100644 dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs index 08f7e99096f0..b4b0ed93199f 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs @@ -31,7 +31,7 @@ public async Task RunAsync() try { - var plugin = KernelPluginFactory.CreateFromType(); + var plugin = KernelPluginFactory.CreateFromType(); var menuAgent = Track( await new AgentBuilder() diff --git a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs deleted file mode 100644 index be82177eda5d..000000000000 --- a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.SemanticKernel; - -namespace Plugins; - -public sealed class MenuPlugin -{ - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - - [KernelFunction, Description("Provides a list of specials from the menu.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } - - [KernelFunction, Description("Provides the price of the requested menu item.")] - public string GetItemPrice( - [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } -} diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs index 5e5aa604a77a..dda6ea31df81 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs @@ -80,12 +80,6 @@ private OpenAIServiceConfiguration GetOpenAIConfiguration() private sealed class MenuPlugin { - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - [KernelFunction, Description("Provides a list of specials from the menu.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] public string GetSpecials() From cd24a45a1e441e90fd907debf4b943b7498ec8df Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 09:58:08 -0700 Subject: [PATCH 131/226] Legacy clean-up --- .../samples/Concepts/Agents/Legacy_Agents.cs | 8 +----- .../Resources/Plugins/LegacyMenuPlugin.cs | 25 ------------------- 2 files changed, 1 insertion(+), 32 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs index 0a03d4c10809..31cc4926392b 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs @@ -48,18 +48,12 @@ public async Task RunWithMethodFunctionsAsync() await ChatAsync( "Agents.ToolAgent.yaml", // Defined under ./Resources/Agents plugin, - arguments: new() { { LegacyMenuPlugin.CorrelationIdArgument, 3.141592653 } }, + arguments: null, "Hello", "What is the special soup?", "What is the special drink?", "Do you have enough soup for 5 orders?", "Thank you!"); - - Console.WriteLine("\nCorrelation Ids:"); - foreach (string correlationId in menuApi.CorrelationIds) - { - Console.WriteLine($"- {correlationId}"); - } } /// diff --git a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs index 7111e873cf4c..c383ea9025f1 100644 --- a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs +++ b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs @@ -7,12 +7,6 @@ namespace Plugins; public sealed class LegacyMenuPlugin { - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - /// /// Returns a mock item menu. /// @@ -20,8 +14,6 @@ public sealed class LegacyMenuPlugin [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] public string[] GetSpecials(KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(GetSpecials)); - return [ "Special Soup: Clam Chowder", @@ -39,8 +31,6 @@ public string GetItemPrice( string menuItem, KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(GetItemPrice)); - return "$9.99"; } @@ -55,21 +45,6 @@ public bool IsItem86d( int count, KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(IsItem86d)); - return count < 3; } - - private void CaptureCorrelationId(KernelArguments? arguments, string scope) - { - if (arguments?.TryGetValue(CorrelationIdArgument, out object? correlationId) ?? false) - { - string? correlationText = correlationId?.ToString(); - - if (!string.IsNullOrWhiteSpace(correlationText)) - { - this._correlationIds.Add($"{scope}:{correlationText}"); - } - } - } } From b4dfd7aaa12742bec9f97c7c313ef62c30781713 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 10:59:11 -0700 Subject: [PATCH 132/226] Type fix --- dotnet/samples/Concepts/Agents/MixedChat_Files.cs | 2 +- .../Concepts/Agents/OpenAIAssistant_FileManipulation.cs | 2 +- .../src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++-- dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs | 2 +- .../Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs | 8 ++++---- .../UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs | 6 +++--- .../UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs | 6 +++--- 9 files changed, 17 insertions(+), 17 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index 52b8b1920afa..24742278f6e5 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -46,7 +46,7 @@ await OpenAIAssistantAgent.CreateAsync( { EnableCodeInterpreter = true, // Enable code-interpreter ModelId = this.Model, - CodeInterpterFileIds = [uploadFile.Id] // Associate uploaded file with assistant + CodeInterpreterFileIds = [uploadFile.Id] // Associate uploaded file with assistant }); ChatCompletionAgent summaryAgent = diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 3de5b2d4f3ff..8a6c98bd3a38 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -39,7 +39,7 @@ await OpenAIAssistantAgent.CreateAsync( config, new() { - CodeInterpterFileIds = [uploadFile.Id], + CodeInterpreterFileIds = [uploadFile.Id], EnableCodeInterpreter = true, // Enable code-interpreter ModelId = this.Model, }); diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 1e393027e43d..acc7283fde4f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -45,7 +45,7 @@ public static async Task CreateThreadAsync(AssistantClient client, OpenA ThreadCreationOptions createOptions = new() { - ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpterFileIds), + ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpreterFileIds), }; if (options?.Messages != null) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 17ce8d4e1685..f22167e9033f 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -308,7 +308,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod Name = model.Name, Description = model.Description, Instructions = model.Instructions, - CodeInterpterFileIds = fileIds, + CodeInterpreterFileIds = fileIds, EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), Metadata = model.Metadata, ModelId = model.Model, @@ -328,7 +328,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - ToolResources = AssistantToolResourcesFactory.GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpterFileIds : null), + ToolResources = AssistantToolResourcesFactory.GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpreterFileIds : null), ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, Temperature = definition.Temperature, NucleusSamplingFactor = definition.TopP, diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index d16ff4dbb091..cb8cb6c84734 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -36,7 +36,7 @@ public sealed class OpenAIAssistantDefinition /// /// Optional file-ids made available to the code_interpreter tool, if enabled. /// - public IReadOnlyList? CodeInterpterFileIds { get; init; } + public IReadOnlyList? CodeInterpreterFileIds { get; init; } /// /// Set if code-interpreter is enabled. diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs index 4f596e000153..d2e8eb012e17 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs @@ -11,7 +11,7 @@ public sealed class OpenAIThreadCreationOptions /// /// Optional file-ids made available to the code_interpreter tool, if enabled. /// - public IReadOnlyList? CodeInterpterFileIds { get; init; } + public IReadOnlyList? CodeInterpreterFileIds { get; init; } /// /// Optional messages to initialize thread with.. diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 2bf7a8e2dd1c..de99478eea51 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -89,7 +89,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn { ModelId = "testmodel", EnableCodeInterpreter = true, - CodeInterpterFileIds = ["file1", "file2"], + CodeInterpreterFileIds = ["file1", "file2"], }; await this.VerifyAgentCreationAsync(definition); @@ -596,7 +596,7 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs // Verify detail definition Assert.Equal(sourceDefinition.VectorStoreId, agent.Definition.VectorStoreId); - Assert.Equal(sourceDefinition.CodeInterpterFileIds, agent.Definition.CodeInterpterFileIds); + Assert.Equal(sourceDefinition.CodeInterpreterFileIds, agent.Definition.CodeInterpreterFileIds); } private Task CreateAgentAsync() @@ -666,7 +666,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) builder.AppendLine(@$" ""model"": ""{definition.ModelId}"","); bool hasCodeInterpreter = definition.EnableCodeInterpreter; - bool hasCodeInterpreterFiles = (definition.CodeInterpterFileIds?.Count ?? 0) > 0; + bool hasCodeInterpreterFiles = (definition.CodeInterpreterFileIds?.Count ?? 0) > 0; bool hasFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); if (!hasCodeInterpreter && !hasFileSearch) { @@ -699,7 +699,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) if (hasCodeInterpreterFiles) { - string fileIds = string.Join(",", definition.CodeInterpterFileIds!.Select(fileId => "\"" + fileId + "\"")); + string fileIds = string.Join(",", definition.CodeInterpreterFileIds!.Select(fileId => "\"" + fileId + "\"")); builder.AppendLine(@$" ""code_interpreter"": {{ ""file_ids"": [{fileIds}] }}{(hasFileSearch ? "," : string.Empty)}"); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 32bab0ac0609..a91a043febfb 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -28,7 +28,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.Temperature); Assert.Null(definition.TopP); Assert.Null(definition.VectorStoreId); - Assert.Null(definition.CodeInterpterFileIds); + Assert.Null(definition.CodeInterpreterFileIds); Assert.False(definition.EnableCodeInterpreter); Assert.False(definition.EnableJsonResponse); } @@ -59,7 +59,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() ParallelToolCallsEnabled = false, TruncationMessageCount = 12, }, - CodeInterpterFileIds = ["file1"], + CodeInterpreterFileIds = ["file1"], EnableCodeInterpreter = true, EnableJsonResponse = true, }; @@ -78,7 +78,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal(12, definition.ExecutionOptions.TruncationMessageCount); Assert.False(definition.ExecutionOptions.ParallelToolCallsEnabled); Assert.Single(definition.Metadata); - Assert.Single(definition.CodeInterpterFileIds); + Assert.Single(definition.CodeInterpreterFileIds); Assert.True(definition.EnableCodeInterpreter); Assert.True(definition.EnableJsonResponse); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index ba4992304227..d4e680efee09 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -23,7 +23,7 @@ public void OpenAIThreadCreationOptionsInitialState() Assert.Null(options.Messages); Assert.Null(options.Metadata); Assert.Null(options.VectorStoreId); - Assert.Null(options.CodeInterpterFileIds); + Assert.Null(options.CodeInterpreterFileIds); } /// @@ -38,12 +38,12 @@ public void OpenAIThreadCreationOptionsAssignment() Messages = [new ChatMessageContent(AuthorRole.User, "test")], VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, - CodeInterpterFileIds = ["file1"], + CodeInterpreterFileIds = ["file1"], }; Assert.Single(definition.Messages); Assert.Single(definition.Metadata); Assert.Equal("#vs", definition.VectorStoreId); - Assert.Single(definition.CodeInterpterFileIds); + Assert.Single(definition.CodeInterpreterFileIds); } } From e2d34081f5b6269626970d2d61b002eea2229b6a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 11:03:38 -0700 Subject: [PATCH 133/226] Remove friend relationship to concepts --- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index fa73edc77cde..a5a4cde76d6f 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -38,7 +38,6 @@ - From d68b567cd68130054f243340f08948885242b2bc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 11:33:55 -0700 Subject: [PATCH 134/226] Update CharMaker sample to download and view images --- .../Agents/OpenAIAssistant_ChartMaker.cs | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 531e47b8ec0b..bef11e52bd6d 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -1,8 +1,11 @@ // Copyright (c) Microsoft. All rights reserved. +using System.Diagnostics; +using System.Threading; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; namespace Agents; @@ -23,11 +26,15 @@ public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(out [Fact] public async Task GenerateChartWithOpenAIAssistantAgentAsync() { + OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + + FileClient fileClient = config.CreateFileClient(); + // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: GetOpenAIConfiguration(), + config, new() { Instructions = AgentInstructions, @@ -55,6 +62,7 @@ Sum 426 1622 856 2904 """); await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); + await InvokeAgentAsync("Perfect, can you regenerate this as a line chart?"); } finally { @@ -78,9 +86,30 @@ async Task InvokeAgentAsync(string input) foreach (var fileReference in message.Items.OfType()) { Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); + + string downloadPath = await DownloadFileContentAsync(fileReference.FileId); + + Console.WriteLine($"# {message.Role}: @{fileReference.FileId} downloaded to {downloadPath}"); } } } + + async Task DownloadFileContentAsync(string fileId) + { + string filePath = Path.Combine(Environment.CurrentDirectory, $"{fileId}.jpg"); + BinaryData content = await fileClient.DownloadFileAsync(fileId); + await using var outputStream = File.OpenWrite(filePath); + await outputStream.WriteAsync(content.ToArray()); + + Process.Start( + new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C start {filePath}" + }); + + return filePath; + } } private OpenAIServiceConfiguration GetOpenAIConfiguration() From 475a9f6fb4e8d9f823af90e964a15b34dcf314d1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 11:36:43 -0700 Subject: [PATCH 135/226] Namespace --- dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index bef11e52bd6d..275cd8bcf36b 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; -using System.Threading; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; From 844cef9cda4a873d52bf4a8de69052d53190299d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 31 Jul 2024 11:48:55 -0700 Subject: [PATCH 136/226] Param comment --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f22167e9033f..f54129096618 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -188,7 +188,7 @@ public IAsyncEnumerable GetThreadMessagesAsync(string thread /// /// Delete the assistant definition. /// - /// + /// The to monitor for cancellation requests. The default is . /// True if assistant definition has been deleted /// /// Assistant based agent will not be useable after deletion. From dac78fd465abe04715e5d0fa2b0b9d67c837d739 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 1 Aug 2024 14:36:19 -0700 Subject: [PATCH 137/226] PR comments --- dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs | 3 +-- dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index 62c477dc57d3..d40755101309 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -62,8 +62,7 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi Console.WriteLine($"# {message.Role}: {fileId}"); Console.WriteLine($"# {message.Role}: {path}"); BinaryData content = await fileClient.DownloadFileAsync(fileId); - await using var outputStream = File.OpenWrite(filename); - await outputStream.WriteAsync(content.ToArray()); + File.WriteAllBytes(filename, content.ToArray()); Process.Start( new ProcessStartInfo { diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 275cd8bcf36b..4c2ea0c59534 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -97,8 +97,7 @@ async Task DownloadFileContentAsync(string fileId) { string filePath = Path.Combine(Environment.CurrentDirectory, $"{fileId}.jpg"); BinaryData content = await fileClient.DownloadFileAsync(fileId); - await using var outputStream = File.OpenWrite(filePath); - await outputStream.WriteAsync(content.ToArray()); + File.WriteAllBytes(filePath, content.ToArray()); Process.Start( new ProcessStartInfo From 03f245689d1eec81c519e11eaacb49c43b034e41 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 1 Aug 2024 14:47:44 -0700 Subject: [PATCH 138/226] Simplify --- dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs index 4d5302f5ae03..32985c457fc3 100644 --- a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -60,7 +60,7 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns { Kernel = kernel, Instructions = "Answer questions about the menu.", - ExecutionSettings = new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, + ExecutionSettings = new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, }; AgentGroupChat chat = new(); From 197a7e306bcb6d7034e8d55713b9669cd0d0618e Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 2 Aug 2024 06:38:07 -0700 Subject: [PATCH 139/226] Update dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs index 5b13a584deb0..f624dd62152b 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -27,7 +27,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsDefault() // Create options MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); - // Validate + // Assert Assert.NotNull(options); Assert.Empty(options.Metadata); } From a1848181bb5199fd1f7cbf1c0dadb294a7aa3ad9 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 2 Aug 2024 06:43:58 -0700 Subject: [PATCH 140/226] Update dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../Extensions/OpenAIServiceConfigurationExtensions.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs index 29e5e36c6f4c..4f2baa1acde5 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -16,11 +16,7 @@ public static class OpenAIServiceConfigurationExtensions /// /// The configuration public static FileClient CreateFileClient(this OpenAIServiceConfiguration configuration) - { - OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); - - return client.GetFileClient(); - } + => OpenAIClientFactory.CreateClient(configuration).GetFileClient(); /// /// Provide a newly created based on the specified configuration. From 042ab642f380a445a057a9571d525dd3e942dd33 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 2 Aug 2024 06:46:11 -0700 Subject: [PATCH 141/226] Cosmetic: Maximize single line code complexity and eliminate potential break-point. --- .../Extensions/OpenAIServiceConfigurationExtensions.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs index 4f2baa1acde5..cf31d0c1c876 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -23,9 +23,5 @@ public static FileClient CreateFileClient(this OpenAIServiceConfiguration config /// /// The configuration public static VectorStoreClient CreateVectorStoreClient(this OpenAIServiceConfiguration configuration) - { - OpenAIClient client = OpenAIClientFactory.CreateClient(configuration); - - return client.GetVectorStoreClient(); - } + => OpenAIClientFactory.CreateClient(configuration).GetVectorStoreClient(); } From 2a4baec55839a0a8a08b184c7e374dae55371b4f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 2 Aug 2024 06:46:24 -0700 Subject: [PATCH 142/226] Namespace --- .../OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs index cf31d0c1c876..9b3a55b9e6fe 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel.Agents.OpenAI.Internal; -using OpenAI; using OpenAI.Files; using OpenAI.VectorStores; From ae47cbf6ea09a53def0034bd1cda976ef0da047c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 2 Aug 2024 06:49:05 -0700 Subject: [PATCH 143/226] Remove silent failure --- .../OpenAI/Internal/AssistantMessageFactory.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs index 0814d9720ecf..eca1cab2c590 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -50,15 +50,15 @@ public static IEnumerable GetMessageContents(ChatMessageContent { yield return MessageContent.FromImageUrl(imageContent.Uri); } - //else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) - //{ - // SDK BUG - BAD SIGNATURE (https://github.com/openai/openai-dotnet/issues/135) - // URI does not accept the format used for `DataUri` - // Approach is inefficient anyway... - // yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); - //} - } - else if (content is FileReferenceContent fileContent) + else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) + { + //SDK BUG - BAD SIGNATURE (https://github.com/openai/openai-dotnet/issues/135) + // URI does not accept the format used for `DataUri` + // Approach is inefficient anyway... + //yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); + throw new KernelException($"{nameof(ImageContent.DataUri)} not supported for assistant input."); + } + else if (content is FileReferenceContent fileContent) { yield return MessageContent.FromImageFileId(fileContent.FileId); } From bd55f03f6d4ca5646e2d4ff918a10319cf5e8ded Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 2 Aug 2024 06:54:02 -0700 Subject: [PATCH 144/226] Braces --- dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs index eca1cab2c590..3d627a2e3e01 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -59,8 +59,9 @@ public static IEnumerable GetMessageContents(ChatMessageContent throw new KernelException($"{nameof(ImageContent.DataUri)} not supported for assistant input."); } else if (content is FileReferenceContent fileContent) - { - yield return MessageContent.FromImageFileId(fileContent.FileId); + { + yield return MessageContent.FromImageFileId(fileContent.FileId); + } } } } From f3c8af1f4eceec080f75f7f4f1afe341d47eb341 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 2 Aug 2024 10:42:45 -0700 Subject: [PATCH 145/226] Fix logic level --- .../src/Agents/OpenAI/Internal/AssistantMessageFactory.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs index 3d627a2e3e01..8b65961e2677 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -58,10 +58,10 @@ public static IEnumerable GetMessageContents(ChatMessageContent //yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); throw new KernelException($"{nameof(ImageContent.DataUri)} not supported for assistant input."); } - else if (content is FileReferenceContent fileContent) - { - yield return MessageContent.FromImageFileId(fileContent.FileId); - } + } + else if (content is FileReferenceContent fileContent) + { + yield return MessageContent.FromImageFileId(fileContent.FileId); } } } From 0c11eb3a36dbff322e8060d528832bca86fa559d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 5 Aug 2024 10:39:18 -0700 Subject: [PATCH 146/226] Port update --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 1 + dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index acc7283fde4f..756ddf0ee582 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -429,6 +429,7 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam ]) { AuthorName = agentName, + Metadata = new Dictionary { { OpenAIAssistantAgent.CodeInterpreterMetadataKey, true } }, }; } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f54129096618..7d542a30ab80 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -17,6 +17,11 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// public sealed class OpenAIAssistantAgent : KernelAgent { + /// + /// Metadata key that identifies code-interpreter content. + /// + public const string CodeInterpreterMetadataKey = "code"; + internal const string OptionsMetadataKey = "__run_options"; private readonly Assistant _assistant; From d492d84e91290b6cd3c66e430a8f05ac367299e8 Mon Sep 17 00:00:00 2001 From: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> Date: Tue, 6 Aug 2024 07:54:50 -0700 Subject: [PATCH 147/226] .Net: [Feature branch] Added release candidate suffix for production packages (#7623) ### Motivation and Context Preparation work for future release. --- dotnet/Directory.Build.props | 7 ++++++- dotnet/nuget/nuget-package.props | 4 ++-- .../Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj | 4 ++++ .../Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj | 4 ++++ .../PromptTemplates.Handlebars.csproj | 4 ++++ dotnet/src/Functions/Functions.Yaml/Functions.Yaml.csproj | 4 ++++ .../SemanticKernel.Abstractions.csproj | 4 ++++ dotnet/src/SemanticKernel.Core/SemanticKernel.Core.csproj | 4 ++++ .../SemanticKernel.MetaPackage.csproj | 5 ++++- 9 files changed, 36 insertions(+), 4 deletions(-) diff --git a/dotnet/Directory.Build.props b/dotnet/Directory.Build.props index 751afab85104..452ceb8542ac 100644 --- a/dotnet/Directory.Build.props +++ b/dotnet/Directory.Build.props @@ -11,6 +11,11 @@ disable + + + true + + disable @@ -30,4 +35,4 @@ <_Parameter1>false - \ No newline at end of file + diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index caecae748432..86b48afb495e 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -1,8 +1,8 @@ - 1.16.2 - + 1.18.0 + $(VersionPrefix)-$(VersionSuffix) $(VersionPrefix) diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index e4f5be3d533a..6c0d24c9ce12 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -10,6 +10,10 @@ false + + rc + + diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj index ecd01d172e11..30b637922494 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.OpenAI/Connectors.OpenAI.csproj @@ -10,6 +10,10 @@ true + + rc + + diff --git a/dotnet/src/Extensions/PromptTemplates.Handlebars/PromptTemplates.Handlebars.csproj b/dotnet/src/Extensions/PromptTemplates.Handlebars/PromptTemplates.Handlebars.csproj index aa6f9eb848c8..d5e3b2fc9e4b 100644 --- a/dotnet/src/Extensions/PromptTemplates.Handlebars/PromptTemplates.Handlebars.csproj +++ b/dotnet/src/Extensions/PromptTemplates.Handlebars/PromptTemplates.Handlebars.csproj @@ -9,6 +9,10 @@ true + + rc + + diff --git a/dotnet/src/Functions/Functions.Yaml/Functions.Yaml.csproj b/dotnet/src/Functions/Functions.Yaml/Functions.Yaml.csproj index dafc4377b0e0..4b4a5176cb36 100644 --- a/dotnet/src/Functions/Functions.Yaml/Functions.Yaml.csproj +++ b/dotnet/src/Functions/Functions.Yaml/Functions.Yaml.csproj @@ -8,6 +8,10 @@ true + + rc + + diff --git a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj index 81e196b63b91..2c2ed1b1aad1 100644 --- a/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj +++ b/dotnet/src/SemanticKernel.Abstractions/SemanticKernel.Abstractions.csproj @@ -8,6 +8,10 @@ true + + rc + + diff --git a/dotnet/src/SemanticKernel.Core/SemanticKernel.Core.csproj b/dotnet/src/SemanticKernel.Core/SemanticKernel.Core.csproj index 7eeee98743d5..ff9c1e8986c4 100644 --- a/dotnet/src/SemanticKernel.Core/SemanticKernel.Core.csproj +++ b/dotnet/src/SemanticKernel.Core/SemanticKernel.Core.csproj @@ -11,6 +11,10 @@ true + + rc + + diff --git a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj index 7ac522bca663..86cbde81153c 100644 --- a/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj +++ b/dotnet/src/SemanticKernel.MetaPackage/SemanticKernel.MetaPackage.csproj @@ -4,6 +4,9 @@ $(AssemblyName) net8.0;netstandard2.0 + + rc + @@ -15,4 +18,4 @@ Empowers app owners to integrate cutting-edge LLM technology quickly and easily - \ No newline at end of file + From 406c3d9c25feee6df8adf547da8acb6c66b36497 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 15:54:36 -0700 Subject: [PATCH 148/226] Checkpoint --- dotnet/SK-dotnet.sln | 32 +++-- .../ChatCompletion_FunctionTermination.cs | 35 +++--- .../Agents/ChatCompletion_Streaming.cs | 23 ++-- .../Agents/ComplexChat_NestedShopper.cs | 17 +-- .../Concepts/Agents/MixedChat_Agents.cs | 21 ++-- .../Concepts/Agents/MixedChat_Files.cs | 28 ++--- .../Concepts/Agents/MixedChat_Images.cs | 30 ++--- .../Agents/OpenAIAssistant_ChartMaker.cs | 50 ++------ .../Agents/OpenAIAssistant_CodeInterpreter.cs | 62 --------- .../OpenAIAssistant_FileManipulation.cs | 27 ++-- .../Agents/OpenAIAssistant_FileSearch.cs | 25 ++-- .../Agents/OpenAIAssistant_FileService.cs | 4 +- dotnet/samples/Concepts/Concepts.csproj | 5 +- .../GettingStartedWithAgents.csproj | 17 ++- .../Resources/cat.jpg | Bin 0 -> 37831 bytes .../Resources/employees.pdf | Bin 0 -> 43422 bytes .../{Step1_Agent.cs => Step01_Agent.cs} | 14 +-- .../{Step2_Plugins.cs => Step02_Plugins.cs} | 35 +++--- .../{Step3_Chat.cs => Step03_Chat.cs} | 14 +-- ....cs => Step04_KernelFunctionStrategies.cs} | 21 ++-- ...ep5_JsonResult.cs => Step05_JsonResult.cs} | 21 ++-- ...ction.cs => Step06_DependencyInjection.cs} | 49 ++------ .../{Step7_Logging.cs => Step07_Logging.cs} | 16 +-- ...OpenAIAssistant.cs => Step08_Assistant.cs} | 57 ++++----- .../Step09_Assistant_Vision.cs | 75 +++++++++++ .../Step10_AssistantTool_CodeInterpreter.cs | 55 ++++++++ .../Step11_AssistantTool_FileSearch.cs | 84 +++++++++++++ .../Logging/AgentChatLogMessages.cs | 2 +- .../Internal/AssistantToolResourcesFactory.cs | 6 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 38 +++++- .../OpenAI/OpenAIAssistantDefinition.cs | 7 +- .../samples/AgentUtilities/BaseAgentsTest.cs | 118 ++++++++++++++++++ .../samples/SamplesInternalUtilities.props | 5 +- 33 files changed, 598 insertions(+), 395 deletions(-) delete mode 100644 dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg create mode 100644 dotnet/samples/GettingStartedWithAgents/Resources/employees.pdf rename dotnet/samples/GettingStartedWithAgents/{Step1_Agent.cs => Step01_Agent.cs} (76%) rename dotnet/samples/GettingStartedWithAgents/{Step2_Plugins.cs => Step02_Plugins.cs} (76%) rename dotnet/samples/GettingStartedWithAgents/{Step3_Chat.cs => Step03_Chat.cs} (86%) rename dotnet/samples/GettingStartedWithAgents/{Step4_KernelFunctionStrategies.cs => Step04_KernelFunctionStrategies.cs} (85%) rename dotnet/samples/GettingStartedWithAgents/{Step5_JsonResult.cs => Step05_JsonResult.cs} (79%) rename dotnet/samples/GettingStartedWithAgents/{Step6_DependencyInjection.cs => Step06_DependencyInjection.cs} (65%) rename dotnet/samples/GettingStartedWithAgents/{Step7_Logging.cs => Step07_Logging.cs} (86%) rename dotnet/samples/GettingStartedWithAgents/{Step8_OpenAIAssistant.cs => Step08_Assistant.cs} (57%) create mode 100644 dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs create mode 100644 dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index fa7d9fbd3007..2a9596c5d08b 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -277,16 +277,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithAgents", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{77E141BA-AF5E-4C01-A970-6C07AC3CD55A}" ProjectSection(SolutionItems) = preProject - src\InternalUtilities\samples\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\ConfigurationNotFoundException.cs - src\InternalUtilities\samples\EnumerableExtensions.cs = src\InternalUtilities\samples\EnumerableExtensions.cs - src\InternalUtilities\samples\Env.cs = src\InternalUtilities\samples\Env.cs - src\InternalUtilities\samples\ObjectExtensions.cs = src\InternalUtilities\samples\ObjectExtensions.cs - src\InternalUtilities\samples\PlanExtensions.cs = src\InternalUtilities\samples\PlanExtensions.cs - src\InternalUtilities\samples\RepoFiles.cs = src\InternalUtilities\samples\RepoFiles.cs src\InternalUtilities\samples\SamplesInternalUtilities.props = src\InternalUtilities\samples\SamplesInternalUtilities.props - src\InternalUtilities\samples\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\TextOutputHelperExtensions.cs - src\InternalUtilities\samples\XunitLogger.cs = src\InternalUtilities\samples\XunitLogger.cs - src\InternalUtilities\samples\YourAppException.cs = src\InternalUtilities\samples\YourAppException.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Prompty", "src\Functions\Functions.Prompty\Functions.Prompty.csproj", "{12B06019-740B-466D-A9E0-F05BC123A47D}" @@ -350,6 +341,27 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MathPlugin", "MathPlugin", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{EE454832-085F-4D37-B19B-F94F7FC6984A}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\InternalUtilities\BaseTest.cs = src\InternalUtilities\samples\InternalUtilities\BaseTest.cs + src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs + src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs = src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs + src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs = src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs + src\InternalUtilities\samples\InternalUtilities\Env.cs = src\InternalUtilities\samples\InternalUtilities\Env.cs + src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs = src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs + src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs = src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs + src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs = src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs + src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs = src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs + src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs + src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs = src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs + src\InternalUtilities\samples\InternalUtilities\YourAppException.cs = src\InternalUtilities\samples\InternalUtilities\YourAppException.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30E0-7AC1-47F4-8244-57B066B43FD8}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs = src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -971,6 +983,8 @@ Global {6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} {4326A974-F027-4ABD-A220-382CC6BB0801} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} + {EE454832-085F-4D37-B19B-F94F7FC6984A} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} + {5C6C30E0-7AC1-47F4-8244-57B066B43FD8} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs index 12438c0fc328..c8d0531d9bda 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -12,7 +12,7 @@ namespace Agents; /// Demonstrate usage of for both direction invocation /// of and via . /// -public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseTest(output) +public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() @@ -44,25 +44,25 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() Console.WriteLine("================================"); foreach (ChatMessageContent message in chat) { - this.WriteContent(message); + this.WriteAgentChatMessage(message); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { // Do not add a message implicitly added to the history. - if (!content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + if (!response.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) { - chat.Add(content); + chat.Add(response); } - this.WriteContent(content); + this.WriteAgentChatMessage(response); } } } @@ -98,28 +98,23 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync() ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync(); for (int index = history.Length; index > 0; --index) { - this.WriteContent(history[index - 1]); + this.WriteAgentChatMessage(history[index - 1]); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.AddChatMessage(userContent); - this.WriteContent(userContent); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - this.WriteContent(content); + this.WriteAgentChatMessage(response); } } } - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - private Kernel CreateKernelWithFilter() { IKernelBuilder builder = Kernel.CreateBuilder(); diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 258e12166a6b..7e74e425536c 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -12,7 +12,7 @@ namespace Agents; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseTest(output) +public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ParrotName = "Parrot"; private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; @@ -66,32 +66,33 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync() // Local function to invoke agent and display the conversation messages. private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); StringBuilder builder = new(); - await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat)) + await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat)) { - if (string.IsNullOrEmpty(message.Content)) + if (string.IsNullOrEmpty(response.Content)) { continue; } if (builder.Length == 0) { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:"); + Console.WriteLine($"# {response.Role} - {response.AuthorName ?? "*"}:"); } - Console.WriteLine($"\t > streamed: '{message.Content}'"); - builder.Append(message.Content); + Console.WriteLine($"\t > streamed: '{response.Content}'"); + builder.Append(response.Content); } if (builder.Length > 0) { // Display full response and capture in chat history - Console.WriteLine($"\t > complete: '{builder}'"); - chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }); + ChatMessageContent response = new(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }; + chat.Add(message); + this.WriteAgentChatMessage(message); } } diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index 81b2914ade3b..e12dee448370 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; @@ -13,7 +14,7 @@ namespace Agents; /// Demonstrate usage of and /// to manage execution. /// -public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseTest(output) +public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseAgentsTest(output) { protected override bool ForceOpenAI => true; @@ -154,20 +155,20 @@ public async Task NestedChatWithAggregatorAgentAsync() Console.WriteLine(">>>> AGGREGATED CHAT"); Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - await foreach (ChatMessageContent content in chat.GetChatMessagesAsync(personalShopperAgent).Reverse()) + await foreach (ChatMessageContent message in chat.GetChatMessagesAsync(personalShopperAgent).Reverse()) { - Console.WriteLine($">>>> {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + WriteAgentChatMessage(message); } async Task InvokeChatAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in chat.InvokeAsync(personalShopperAgent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(personalShopperAgent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + WriteAgentChatMessage(response); } Console.WriteLine($"\n# IS COMPLETE: {chat.IsComplete}"); diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index 91add34e8693..18ab8a673ca1 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -10,7 +10,7 @@ namespace Agents; /// Demonstrate that two different agent types are able to participate in the same conversation. /// In this case a and participate. /// -public class MixedChat_Agents(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Agents(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -53,6 +53,7 @@ await OpenAIAssistantAgent.CreateAsync( Instructions = CopyWriterInstructions, Name = CopyWriterName, ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -76,16 +77,16 @@ await OpenAIAssistantAgent.CreateAsync( }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy @@ -94,10 +95,4 @@ private sealed class ApprovalTerminationStrategy : TerminationStrategy protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) => Task.FromResult(history[history.Count - 1].Content?.Contains("approve", StringComparison.OrdinalIgnoreCase) ?? false); } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index 24742278f6e5..f14ad8d1222d 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; @@ -13,7 +12,7 @@ namespace Agents; /// Demonstrate agent interacts with /// when it produces file output. /// -public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output) { /// /// Target OpenAI services. @@ -25,7 +24,7 @@ public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeFileAndGenerateReportAsync() { - OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); FileClient fileClient = config.CreateFileClient(); @@ -45,8 +44,9 @@ await OpenAIAssistantAgent.CreateAsync( new() { EnableCodeInterpreter = true, // Enable code-interpreter + CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant ModelId = this.Model, - CodeInterpreterFileIds = [uploadFile.Id] // Associate uploaded file with assistant + Metadata = AssistantSampleMetadata, }); ChatCompletionAgent summaryAgent = @@ -81,27 +81,15 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) { if (!string.IsNullOrWhiteSpace(input)) { + ChatMessageContent message = new(AuthorRole.User, input); chat.AddChatMessage(new(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + this.WriteAgentChatMessage(message); } - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"\n# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - - foreach (AnnotationContent annotation in content.Items.OfType()) - { - Console.WriteLine($"\t* '{annotation.Quote}' => {annotation.FileId}"); - BinaryData fileContent = await fileClient.DownloadFileAsync(annotation.FileId!); - Console.WriteLine($"\n{Encoding.Default.GetString(fileContent.ToArray())}"); - } + this.WriteAgentChatMessage(response); } } } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index cfbcd97c8260..e1bf8b2b7068 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -11,7 +11,7 @@ namespace Agents; /// Demonstrate agent interacts with /// when it produces image output. /// -public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Images(ITestOutputHelper output) : BaseAgentsTest(output) { /// /// Target OpenAI services. @@ -27,7 +27,7 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeDataAndGenerateChartAsync() { - OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); FileClient fileClient = config.CreateFileClient(); @@ -42,6 +42,7 @@ await OpenAIAssistantAgent.CreateAsync( Name = AnalystName, EnableCodeInterpreter = true, ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); ChatCompletionAgent summaryAgent = @@ -88,32 +89,15 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) { if (!string.IsNullOrWhiteSpace(input)) { + ChatMessageContent message = new(AuthorRole.User, input); chat.AddChatMessage(new(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + this.WriteAgentChatMessage(message); } - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"\n# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } - - foreach (FileReferenceContent fileReference in message.Items.OfType()) - { - Console.WriteLine($"\t* Generated image - @{fileReference.FileId}"); - BinaryData fileContent = await fileClient.DownloadFileAsync(fileReference.FileId!); - string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png"); - await File.WriteAllBytesAsync($"{filePath}.png", fileContent.ToArray()); - Console.WriteLine($"\t* Local path - {filePath}"); - } + this.WriteAgentChatMessage(response); } } } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 4c2ea0c59534..af8990096a65 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Diagnostics; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; @@ -12,7 +11,7 @@ namespace Agents; /// Demonstrate using code-interpreter with to /// produce image content displays the requested charts. /// -public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseAgentsTest(output) { /// /// Target Open AI services. @@ -25,7 +24,7 @@ public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(out [Fact] public async Task GenerateChartWithOpenAIAssistantAgentAsync() { - OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); FileClient fileClient = config.CreateFileClient(); @@ -40,6 +39,7 @@ await OpenAIAssistantAgent.CreateAsync( Name = AgentName, EnableCodeInterpreter = true, ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -71,48 +71,14 @@ Sum 426 1622 856 2904 // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(new(AuthorRole.User, input)); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } - - foreach (var fileReference in message.Items.OfType()) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); - - string downloadPath = await DownloadFileContentAsync(fileReference.FileId); - - Console.WriteLine($"# {message.Role}: @{fileReference.FileId} downloaded to {downloadPath}"); - } + this.WriteAgentChatMessage(response); } } - - async Task DownloadFileContentAsync(string fileId) - { - string filePath = Path.Combine(Environment.CurrentDirectory, $"{fileId}.jpg"); - BinaryData content = await fileClient.DownloadFileAsync(fileId); - File.WriteAllBytes(filePath, content.ToArray()); - - Process.Start( - new ProcessStartInfo - { - FileName = "cmd.exe", - Arguments = $"/C start {filePath}" - }); - - return filePath; - } } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs deleted file mode 100644 index eb5169f40b3f..000000000000 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -namespace Agents; - -/// -/// Demonstrate using code-interpreter on . -/// -public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) -{ - protected override bool ForceOpenAI => false; - - [Fact] - public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() - { - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: GetOpenAIConfiguration(), - new() - { - EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, - }); - - // Create a chat for agent interaction. - AgentGroupChat chat = new(); - - // Respond to user input - try - { - await InvokeAgentAsync("Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); - } - finally - { - await agent.DeleteAsync(); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var content in chat.InvokeAsync(agent)) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } - } - } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); -} diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 8a6c98bd3a38..0f92b31ffb04 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; @@ -12,7 +11,7 @@ namespace Agents; /// /// Demonstrate using code-interpreter to manipulate and generate csv files with . /// -public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseAgentsTest(output) { /// /// Target OpenAI services. @@ -22,7 +21,7 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTe [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); FileClient fileClient = config.CreateFileClient(); @@ -42,6 +41,7 @@ await OpenAIAssistantAgent.CreateAsync( CodeInterpreterFileIds = [uploadFile.Id], EnableCodeInterpreter = true, // Enable code-interpreter ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -63,27 +63,14 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { + ChatMessageContent message = new(AuthorRole.User, input); chat.AddChatMessage(new(AuthorRole.User, input)); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - - foreach (AnnotationContent annotation in message.Items.OfType()) - { - Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); - BinaryData content = await fileClient.DownloadFileAsync(annotation.FileId!); - Console.WriteLine(Encoding.Default.GetString(content.ToArray())); - } + this.WriteAgentChatMessage(response); } } } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs index 550615c6bf3e..c73934421a7c 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs @@ -12,7 +12,7 @@ namespace Agents; /// /// Demonstrate using retrieval on . /// -public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseAgentsTest(output) { /// /// Retrieval tool not supported on Azure OpenAI. @@ -22,7 +22,7 @@ public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseTest(out [Fact] public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() { - OpenAIServiceConfiguration config = GetOpenAIConfiguration(); + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); FileClient fileClient = config.CreateFileClient(); @@ -47,12 +47,13 @@ await OpenAIAssistantAgent.CreateAsync( config, new() { - ModelId = this.Model, VectorStoreId = vectorStore.Id, + ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. - var chat = new AgentGroupChat(); + AgentGroupChat chat = new(); // Respond to user input try @@ -71,20 +72,14 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(new(AuthorRole.User, input)); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (var content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } - - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs index 38bac46f648a..a8f31622c753 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs @@ -28,7 +28,7 @@ public async Task UploadAndRetrieveFilesAsync() new BinaryContent(data: await EmbeddedResource.ReadAllAsync("travelinfo.txt"), mimeType: "text/plain") { InnerContent = "travelinfo.txt" } ]; - var fileContents = new Dictionary(); + Dictionary fileContents = new(); foreach (BinaryContent file in files) { OpenAIFileReference result = await fileService.UploadContentAsync(file, new(file.InnerContent!.ToString()!, OpenAIFilePurpose.FineTune)); @@ -49,7 +49,7 @@ public async Task UploadAndRetrieveFilesAsync() string? fileName = fileContents[fileReference.Id].InnerContent!.ToString(); ReadOnlyMemory data = content.Data ?? new(); - var typedContent = mimeType switch + BinaryContent typedContent = mimeType switch { "image/jpeg" => new ImageContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata }, "audio/wav" => new AudioContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata }, diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index e19ed5e0d8c5..32fe001cbf09 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -40,7 +40,10 @@ - + + + true + diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index b95bbd546d34..df9e025b678f 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -9,7 +9,7 @@ true - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -32,7 +32,10 @@ - + + + true + @@ -48,4 +51,14 @@ + + + Always + + + + + + + diff --git a/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg b/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e9f26de48fc542676a7461020206fab297c0314 GIT binary patch literal 37831 zcmbTdcT|&4^fwp;DN0e1-lQv4dJ7;bARr(p1PE268R@+%(n}&;x=NQ4DIxSuLJ=Z0 z^xkVi4gKZ&?r(R`*}r!8J~QV$=R7lW?&sW@XXf7fnd|ZEdB8nQH4QZY5fKr<^5y|t zPXS&4ZV?gvSN=zcZxjC~q$DK7x5-G!$o{M36n81e$?uSpk=>!XbLTGQjgV1L(@;@T z|M&jiApdp$uh&hXBqt;Puf_j2xo!pACno|B0f>pX0JrWF5#J}e?gVfE07SQMwEa)v z{}G~F#3Z*#Z&bQ-_oe~j-i`Xi#J6sgy-h-L(>n0xJAmZ=?FXDR4 z|0L&rUeQLUKZxUzc<&ZMafhCPk%^g?kN@!#0ZA!o8Cf~`7cW)S)L&_6zI|t42r@D@ zvHoCVYiIB9(cQz-%iG7-?`vpSctm7WbV6cMa!Ts=wDe!OdHDr}Ma91>tEv$-$lAL4 z_Kwaj6uP^ocW8KIbPPK_F}bj~w7jyqw!X26-#<7!IzAzsp8bdGKb-%k{2##nAGq${ z;JS5V0}`_Ta1q_|zNy6bNp5qBkv>q-BeQa){QpAse*ycyaZLg!iHUBEM|>Zk47kV|z5rs}P)ziyFu*3!y9EJYoQ6W-TlG^NkZ?D;E{_qn7PF5Q+>ynxzi z5X^^-Ubtc=`K`F_x!r;lz8FGE!ifT-;;LP%|IJ8me6Z?ZUm}~WZ=v4cz{NsJ3T8yl zqy}dk#g5qCv1bJ;mgu4^EU04tiXrg#f2tn*j+3oE z9Nk#kMc7TiUwrfp8x;B2w7SkZ-Gt&Gx9E%(54%~}WB$2=(`7pRGe%^f z@u)HUE;2S#jm8FZxMiM*<39e7*vV`{h(6b^s;F&vA(h`Ud0z|qS%LOT&I4dpu}sXz}#oRL$n} zd$7-??h)Jqk3r3lOK4aZ%1nxd3~7AREe5=AM4Hy)8Bk_kFKdjK55BM)RD<2f6v-^b zgF>2P_{-8%{7u{cMZBAAx&mETXu}xj63-pLlt;s01@PK6fNIssp=JmjP$HAka24=$ z~g^;c8=>Ccu9T9^TBV>Mn3MjZ*&sdFJwRCbjFJ zi#+r*NKJ{}tY<0GV~5VRvH1=YXH;hf+HAceq!zB(>>O4!O*_iBdt6xq@A3Bc1X`5* zrgeuk<_#h050Pe$6H4~k1KBUY=#M-J3sB>OR*dG^ z-{yxUi%Nw9Nx6HPM?6?jRo;B~^3=>Mdcc-WK<_Kuj^|KAl`qF*_8K7N%$c6_t^Y^G z1LZXC&xXY3HG5mN*7!1bm_GXBknrCJ<`WgSb1`}_Q`fJDyMiM7N7dkKKxK0VRXs|x z&OphE=b%CH)dos%Q*`3s_~{7nZT{ruq(y8aZy_X=#e7aHd-0Wt>AtdG--?s+4A0S) z*+erp#S01sP1l&BnI9|pqs>f;CjKX2Ba8@CpbW$u)%>n$M`Uf^6Qd?L3&( z7~&U}^mKND5HyMqED_9SN_xQkCb!&eNvMw4De&=Fi_KQqtpP!%($%YxS`P^o`5m7x zEb;G-3^~6zcAPiK!HZh#t^wn)#hiAIXs_m{fikbFQ+4Y4XH$=pri#JcB`V0JZ8CNFTjJ%-pCZb24T#c*-LQWji_;2+qV|p{y zh~b6_unxTYOh8uG;jV`9C3-cb7>B~PkM;4S90-UBBsdFm21%f2WHCt6dRT-BRoPV> z>lfkvuIkdbl>%T_>*O|#8d>cjJpaN0*FL+YSq0m^nP0!YW)(f{sz^LqtZmo}KbDs1 z_jYzzp3z|M`C!P&!MF^6jgzB;(lVLnJKIHmMI`F4??K>v4HD@!6m6#aW?CJ0iEZfe zSpNtq%eRd*%GC7*G`S;xl30FO_m{oV}NT?qnln=)|aC!8a(U~xZ(@Zc@58lwbvPC zSh=_Hu;44?ScCl%Z+SgqZ>)YJlv?-}B=nZ7_r-X(VpEA;5POH?r!tQr!6hLEM&&ra z+b!Y4y5Fj$#lgG0_nz>=2%ZBF2?N|U;MjDkd_-IH&F4ez%&ZQQAjGP!l8$Nh(E(Q; z|L{v!Y>lDVd)b{aovD1Yzd_B2c;T<);GWKp)GHu1bWmoXV1D}5tiqR7wTd*3`$<+6 z8j3WGyAu7@g!nw;VX`1oG-zFp+EX5 zn&yDsIy=ur_QJQ6?QMs?DG!Qmm^|p1?C<VcC>Gc z4?=2x(M5@teL&Tm<))nMy@0=C7<&zv&uG^_=Y>rOa}HmaAP3?ECsl>wQDptxB%f$7 zq)pbx!8j=tQgP<+%Qaxpq`{~KN866yO2@+{Iy&z;FsEj@ay{>b5?Q(wI-8z@N(jWq z5fuc5f=w>f%25$1-pTkB1?x3VSTWfgD|XdX;|pq05L6f*jpm8+gy-Qm*--^9}o9m%|eM5d3P)tZZ`xvUfRPb zamm?r<)1>v2wmc))*N1MDyaURYP~q{McW$p%A8jf~r;X#YQh>Z0Y-pEaS& zlr$#iH&ey$GV?+ix1>5W)gf<|o#83bRNdmBng)OMsD#PLHLKM+mi_3M;mZ|b{B1*=0`*GM%_aaXUv<)^dRenFAT0m8qw zA;Uu5v=jfpIh<}!4&ybTjY~2og!VF`tG;VxtUf~AXdDci5oP2ZKUAz;ro_fv*b;p6 z0;KWJ20gOqO2NA00TX%7)qTm+-k-e=GA*gIlh69`FFNWAp+LLEEG^xkBPwU3^i_9O z>3`KjujVUJg~Xq!r-{FOv5?wQ{$mlTL=BM-Dl7g-FLv>)tuAHKL#w-tGRM=V+eZA} zz^i3I-(2Bxoj0?!7+uK{zAC-rxoNyxQ-)(cBGExuzjoAKvz4?;&V{Ln*@LKz7ZBa4lUWm<>WK_aP(P0=y5+d1Pq91N zI2sEaA7TL$c@3aBGSkFrV5`ry&aDckCEp15c7~0pDZS|m$JMs+oRBuBUVd#BoVxnQ zY=|g@(0>z+rkCB3Ql+TiJ+kIdX)NMrwe$34+!W>bKHNT-QQ-xtm8+dU+-Jyq``V~`M<_yS_YQjGhU1;+s{q9n?NVD zN@cXIsTocTrT9DwJGj83QgawZ#`MW-!nc8#3NRGF~1Gf&6 zK^!UusDBM%^6>@f+U~t)Zm7Lq4OPzX;KhU63$pu&mZQ4UEF4)ogE?RJ*_>9mLX?5N zgRq?`JN1~r44EF5ZZ#pO?<~9tU-)kLlgYptR>QV7VZ(-NW=XG>t69zq)hd=Su`OYd z8z0opfh)HqdXVS@Xny%dPPwT!#2=J%)vHXIhr_mBx|=o+)1SWq z6A*FAI`!79GJ0yjU#wqhoo0=^IMr0>&e$0glPQDvV%P4tneR0Y2lN8_HB@WTH6}NA zCrxoBHkiByD#6T0d>WOw^_;0TJ|eQq=H9&T4i*96!zNNHG4=f z&ljyM;@d-aJk2Yl>O0mtHMx+NU^+*Y;ZQqcCcC~mY;W8dIC^@37t7SstlU-G>;kc>Z?WU!~YqML=J zaQ7vOUF2P~ouY>-Jm&5w760y{Rpq2^tsvVibR#>xLhQUn$b*UwD-Jv#_l#8Q4cN2a za`ao6wftA@IR(;>nDTCb+9GS5MXdHOD`Ndo;?&}# zPn&ZK*^}3U_;5R6rE$DF=k2{KnLwKu&Av>wj&D924f}98ywdL;$Z|JU%c-nVgZuXM z*M9-y2E^yNjHKBhV*_ss=p0Um(tt_zl-0Jv^D;*O?bdI{os*nM#SC>#UZ;6&DI%Zc(9OPU`?+sMtX=-(xp$EZDf^#j3%T)7pE=e z&#g+2S`$Ti+29rE(PVR)qB5o|uF)YDEUQixx+7E*8r;+4)Tru5})*0IZqqFp<%}9RHcBEEOp0Eu66d;t6Mhd4E=To~a23xD|eoa>TvGxBRCva$i9hDe=#<;#O`stnEW*oVxA=d*>yb`g00GVT! zvhx(z_6+K@`$ndBGf2mxtfxO$SGtT|h{ixrGHTPDt{R47VD6w1H@^RdGT^s5OLLy} zK0FOC5sGir_}pPxx%#tD&;0ChS69K%mZMNg`HEhwcyK-I z!k44&LwN2sB00{$r~UA_EJN6wWlB!iDMju_)@wJlCAa$QAFvnZ1N@&yKcyQ(;Gzz> zZxQJZGdwM+`S2jj%cMFJ(;lXrD$y^8=dcNjj%JooE3+56VDEQO*96+XZlUZyKKpRT zgUyKv96v7vzfC*5sg_B6i8GykGIglrcQmWPei@I0FDi^W|GZSB zQv+OtnI#%cXJ`e?lx(?em&e_aRIGmYdpHE9pn1j{+-1?XIuHa_l$KS#N3hVrd5D8I zm;=i~#UBj$iiJ63lp7scXYRZA`(mSnb7^?`IQnJg0s=az<(Un0`u)!zW})dV_R^w^ih- ze+v(cXyh7TyK$%+HR&8B2`;i~8Wy2nVM2}68f?NY#{}bRKlovDeUOX1h*D6>4*$fO zj*@$O@%jXt{9$=&SN&BC?&M{}x&DjGy!fg=x(k>YMobV9kL!RUWKA?GNCAeDK1*H( z86N-Z2#;fH@fFp}_NNy2TVBeZGmf`fNO@u<-dC$V2MRJv6==$8FhdSDL78hAZvQ&T zc@Alb6)ad!rGKi;@-)@Ig6HXNsJAyaai*R(-Vdcn_uBRe1BswuPFjTCfLwpT)*_s- zc#ci-E>ch4jOdJRBurt8j}>V@S{043U+tT|L>HmNTa$M6W{!5`gkD3IMc zD&KT*8eFN|3Lb_Z)u9D4ajW1g-gRz){yGf>|HX!u&n+}GQJ$%uM&m`jFWrVTh83RO z#5+A5+^{E66fhTIsC!JDe+`HqvC@vNtW-j*yPe&XO;lW(2+6bKQrA;<2}?1N6$S z0qrjWCHGA$+uunhgiWvKEept0yw`auH~`cxy$nBt_;N!xH*LEeSav#FYJGurNfs~UVSl-{iUvUX`XPkm!C@i4pC-8=^{?A48{Yzhf+EgRaaN^ zj^h$zq1(AimrM3}8tHaqb<{A)+W-|eN( zlP9W3yxChK5J71H6gbD6&}Mp{my?1U3Fec1?miVLndv`isS1&t&)$$V1qBj_Tl-44 z14=N^y#p&VCde~nJmtGTNp`uc`v;kvY3-%Lk?fkDoUn0R zF2WO+7tg~tBWK^bBs$^dgXWpXYZLr`Tf7M7$#DqT)rx238v_Om-D{xOfRYo(W+p(! zTNgHlLZ5Wa1cR>P_ZD0p{k%qA=)qIGMl2YpiC&eM6b#CM|+#s127+&=6Xa z-d_&H-pTtiKrKUbL^jk!^9y85p$^XX`6Qp*5-vR9Bx0}Lz+te%Ggyb!9VU5s?jN~o1l|r4StH+=}DeT*#;6XSkP5hqW`QU z#nJ@lL$>PYCfU3NNk6S2m32+_b*VS?C(x|rE|m{R8QC*Et^v&3rHz_JEXzd8m$PV# z?_TSE{#Y{&0XKcM_)!*ogO&t2=$A9fpBmwa`0`Nm8sM41s4@kG++u0rdUerx`rSH$ z7#^^OBp8I=!`Ke+BxnG-d$XIugVA(1DT7Zc270#HJrh~xiHlC4Rm@SD*Qn~WLUOb7 znz_61mcuuZ!#lkMA(q3EN$+U!cj2eO@o+~STF}>eIbWTFS9RgweBT-g_1rP@;owMIj zrBRW}fP<2w^1|pPd&%EY18oyRQuWwzO}+EMRz)IzRKbvQ+0k9kaL3R_SDiYLDZBBj zjh;sbvF@g*hNb(yW%6dPY6Kto8b{xYq`(k=YxO&1yp`IC{S?BujI58hhFLmG-g_&JSu5r-f|ExmVD8CIBPN5JkmP@aLHO3& zmyrDabNNFCkSS=QYI_>FQ`M_P@v*!CZl_LB^W^=x*VLMP% z$bI!{AL_ehhP#iq13ITWTx|vJ(O#$*C?Ss3TNTxUDSVijoSYstLTwZRiYf;!|8RTB zbG%X0I2uqgBs{^3s^VDty^CI&*z1|m0TKsXd``&)V{ub0p_%Sx+jG@Hg^SA4n^^72 z!JS;*WigoG(+U8OFTk~j$7SZ#jzubd6lf^zlu)olKH;vwb18Mvm}(~73whg7?x27i z^pF>gYq_`kC_y!RqY)vTy5HyNeqi^5jg$x&efnY%8)ud6ABK#|%FL>)T>iNvM6sZ< zfl@Zvy5jX2*xWW18Zu?Cs6mJNx{&`i|x6nC;} za5_Hk$J~bf*)VS@Av}RAF`N28pIG#M)z{*W-6yx7IH@=*mxC+p=Vh-q1pg+v_rvPl z%_=S%YcXy!5nDB3zx_%_Tr*IUgE`a7(xbcImpUz9 z%`;L@S6q|P#yv!LQ#mV;o?WM}&S+b?qEj>V<>4iD#4#M#zLEWCcx~#u%KlH7giww* z30v;&nzy0~n&iOT_=ki3mRpR}H6SH4jK_|VWC@QFoNM_QOm&&`f>|x**p+4HUSLkR z4n^!mPj(+669fxT8m=Kk&-6W>5_I_sy4lOt_@hto-(@uOKl*H>o14O2;tCxtTOj29^UNr-#X9&a@VV`_%MPYE zyfk({am)WB*TW{5(M% zg#mA!q2i?a$CB8z+j|6jQ!29-#BV~Em|f0F8ctn8xnVM3;55Y;B{ zK{LUY@DtFbTDVHq!qYc@@$ZIzBzWlv7uBS@MSE8vcCWl-T5JQ=Ea>f#45<&yRQ(GE z4$Nt!Y{4NPdIbK;JU0DZ-1p=iBi*hNL;AqgtOjL?Ge`OzU7;^J%Jl{+N1J3v@Y@8N zg&BobX2s0cWddB~r*Bsd09zr?*7zA&{g={@YJp(u1uB`HvkwqYR|RrMK5&xl>pwm5 zF3&UBa+!AB+vsKl68hE!#Jn@MHs*%+f!poX?wM_uW+wDQmnUvhB^xJRim9qlHbc~7 z$HKn~(Q5s5Na^9~xF4y4Ax{BYZBqJEsFM7dSIi3e_T!xCs}g+JwY$jkHCEe3=-T4D zV|naF0J?cPe_#hxR+WJ3I5_lj30#jLRIThRRG~AlT!m-oN2;4(k*??z{Pur_Ow* z@Wjmfsx+`_p(WyVA_Pt^j;Rw+!rul@!qRg8;j+q_BgT_D=a5y zb!j=UX7)H?*g5p?i4=kIQy)_eIKvWFG6L|20&%Y>2cZ)gdxDv<^3@_!f#N)|(W=f(56r z$iVt^OBbn%dznR|U(B}b_IJq>n1lrm%^_S7 z@7>CT?r37Lu)2>JwGI~AnTb$)yxF=;O5%ZjE;x&zA%@VJ`3mN4>T<`1(U)s*B+IA~ zJWDljC;I<9*FXG|%%b-idvFcVi&{X2Jb4OFgn~I;z6y2D-_m~hej_h;#@aIRU+RTNS04s{PhsLo9Jkwt*C9A)H|h;b zD+s|Ier#irrS$u27Kj5WHhr!(QwK z^L&ae^$gSy)};#aucorredx>BXP;GmFS1p;R~aS@Kt?##sq5$4ZhkFL(Idr*epk_nCC8oz0TsmqY7B*JO@fn3id_p6N2fah z!oOzFc~fD~A#ia|_EB;L_wIXxq-7bFXJ}#)P#PfyjKBr}Jt7a;6m(N&q*=m{-%Pc{ zQ;0=;r>AXv%mruKM}9_|9=-Ej+^+HR8op?Ht4<%7y12C{xadlouOk+nS|`k&s;6OA z=h0L;_ck7{y_{%7Uz^sr+S(fY9j0bJUM{0QJj%!Z=7A#hpStYL)eT>_p zr{Wn*@lASdFf-sHPBTMH2JqaRDItE>h^QO=kkTK7xhvDh=`q+6})QzG5e+256i=_8FS%Nz1QlsjhoDcV1Cv=0@iA z$Zj0;rK6x4^!Im=0Wd_a^$=bb{C^`e4t_KJJmVT5G13B2rnvFa)z1|P z3}7TwR?lO+x8qK7sN-c{}Wy#97|EY?Kp|7cqL*{-_P zZ1WZ#9WtzU866;M=X$4uw=|W0dr`X=^r6mMZ1+JVj%EH+gQO|2<32+Zeb3T{=DIG!I`PV9_3zKK1*PMB0k zlc9pw{-&+Hd6n0SS@<7gzPrp3Y%LC=2Ce|$r&$*%_)X%juJTNymHJ+G}5NIEBO=7tJ;c15* zt9a}_jg!BfPeiA)@9)=RCP8^)+G3uNm}*;M#%Uj>lVv+L!P7aJh40n}capY-++qzA z8cdqb1CX2khmby^&$+N%vnTxx=ov@es?9Tn89@baMdD5+{t+oN8||7OPND@cAB`Y2 zF{sMGQ{TfS+-lR@F(&~A_p$Dj8PYgbfQ!GuPwGsS4R$lRHOaBH%J#s&l^yXlrO2qo zmAfy8S!as61>)mT3fB4lpf;K~cLJ$HI%;)EBf)->w+?EX9C8gHf2S%mzRSY6v}JKO z$E94PhGx^p-koo>yv#PoV;EV`Y`4Ya9K-ndTyuOARQ!0q*;vGw-*Jg&$j`zS?nqOX zJLZJAG~7Rf7z-cnRV&<0s0>VaKrt{zyb5$7b*M)n^1E_-1kIWFhw`|{ZEm}DRmJsP z7_DWpw}jhuSWHnnf;yz@MI$c2<>sa;jDU%8u+g$GG#SSpXI zOU_z;d!=D~?@O#8?wNAA++r0vHiYNKYN6nc(tFHfSSrm{JlrZ zXXA1}0Z$0CSef40_mXi>&7f-@Y4dg!z@zdEbTBoC*}3NsYKt)BKg zW4?HUl-UshCw{7i0~Kl3w;>bhjhk)a{ra21z0h68QE{c!sY58i5OD+O90kITC6ok65?k-ClkN zE_`7O}Szz|Vvkq0xjFTuqvb5Y`s@SS;>A($SH^pPZQYjYP^P_3k2jD>ZT& zSA60Cr|)P-$FY6bAkJ$*D7J$+AW~oM=cq@Sz%1}$k?ggE12(Q`gTxn|q|Bg1({F@!Xv}=Wn0%mVSOv+xo`M_jaX25VH?uU$7l@zAvToM{}@k zoL0HkV*&4b(g#@+dtHGn3owSlj=kF-LG7y(H9awQ5+W;t5ueteanLLs#jSw>iE%5J zWU+guLqmNQe#cuYC!0P5)?ai_8dU>R`da+69nLkk#7*WVHq(!wTj5n)*w@45V+uYz zce~vl27I}-EJ9=zdx*Yaj1-oAO!a=NUgqO9<5J#kx1Z>K^N(j?`u>^PYc2DZd-{nk z<%Po^{8|@y{7755BfW7lEGYpjT?^e{p~<4>HKvnOos{{&{++Z@YjL%Kt+~YOpZygb z%soC**rkZ#isSodiFI8mJS#u6(~^68i5=XqdkEU_K0%gjg3f+wJ-r5eEaB}z)C4E~ zTL3zI7(3DLw`^cW~-5`uNGipO3juzW||Gx|)|tP**VR z$G4*aFquoecAFzQKXcG%!)7DQ=Q5r@v8{!xT;Y@pW&85UF1*9A7=;zT?N zJNrd##QdjY5on7rv4TR!k{5yb6B$t?1!s#Jq2ADU%E@;C8t+JK28j5PQkY1LOP!as zE|sBSvetwj#QRiuYP~eW4DtK%f;pS$!ob#Gj*8j1`l9p1l*wQjWjI!&f%c_wt0;9l zOS6moH&UV{xvhP8`y7dVJ*Yc3WG;q_oQuT6?U~u_Lh+xBOR+ZqBU#44UpPkr>uz zO4kRr_w)28Qm(v)K&iKtRW@mU{B9Pm$XO(VHukAO&U>ux0>dKuBS*hhSbu5b=Klv; zISq16R~IW5vPQ@~-%v0S27?hnCk^(Ov`P!p(FJ;Hav#@9m{T0h_qCP1lNG!W3I63P zRAilJRM~~MXI68Hs^n}^X4OE(+@9P;kKm>uVzD5k2?rGhhgwq)pG8qUr}OW6#lx}g ze_zjxR71ZFy3p`Dd%V<{EkJH4Ko!R|WJbF!K5L&qSL^djMY1BfEwI%oc$R&6o+vsO zw{+}3CfQ_2^{PDeVH_=K2zW#z!>8?3w~s(e01dh1L^1^wYo^HGi?xwq!1SiC&;H)s z975_UViFc}s3d?Qyy~QDa%y*i7LyY9_p9M7NAj46l%3)z$*F?ctA9v?gfXAAe23g6 zp2PziHwq@8U4h`764iztVM-4Y2+?)kiK7^ke#O`-bql@X`~4osiF&bi`qrVG)Z!I; z5-ct!W7|UkH4sb@$<0Z!TpO-~Q(2HoPL}m_QCjEj=$M0Hsg@w<2{#=FxE+2B2%Mva`>GTcrBSTypyf>4}S{!py|DNAD4e&@# z#`f&Y6;1~bSugsR#3a|=1*UN{Dcp7Q_dvR@X2Gt6%%NvzMwm zCTs_OqU`B$+-3OAbSGYYmWlNsLE@{qmomi*v9Gz>W+zxIBBv*9xNnvw1)*WAtai0E z!;UGEu5;l|aC|b_QPX3^`;``hyGcf;ow-1Yap4t#`PGgk4kz*8Ka-X9l0lY7iV;dP zC94*cL314UM9K{ISe~ap?h)XvG6w$B0T(SP4>ZLrMC_<@Kqb-0W?XX$GV%5SZ_Nuz zw~(^hO5zZ=cg1K%B}BgvJ3obv$aIf`#}VRqbRCu#_e#`@zBz6q7nwM`*0LD*^_M~(R}yD?hSn;=W6g0ofruf z=jz8HJWetR>OfpUlkPFqzm^5iRe$pneA!6CDaM({C4#a2_wxO{ZunnVEQr3D9IumK z=2O3~A=+pxF))bSE0=cSReJVRer@%5W2h-cmqq`MpXxbg(7!m~C@8z&yDaKKvB~t1 zp@x?~a^KLr5HrW%0b>ZT8{L`e;CP5Avwnj_=s7yF#@W8lGA&_?=bS-kR_-dWK+08G zfv_7V%y!4Ri=7R-JeIOK*EuPc&8FGOtrkn$o5GaBF+W>5U(}uw`%O-2lQ|`ettFZP zb-(8l=4>s0)6sOiqY`h!W7GV0KSW%k`&I*thTv{Y5XRG7(W@?>q(?*qc(cY)Z8;jH z;NP8wI4~|~iAe7$OFxxkgbxe&%e&?aP<+amdzJ4XeJ1?YdkYbdt2Gt=h$xXi@S0H9 zl2g=&dFNJ|iPqscU>CH%eZZ(;kvORxOca93T!Bq#mJSkL8TLqAsuv}ZD$+)L@A(C) zK{qo!8so%6cJsn*-d$H2j zfQ_G#SDYdg{MleBY*@7EsKDek0Ool^t(EBi`*6u5(?Pq4*S5N9#;p0IIa{kB)C3mG zEJ`bq80aGf=YL@I(dN0PYo96~fU={45k5jQK8JdGHIh?R6X7cc&T?*xqmT07vx>=3 z=lx-;7@g3Q_*oYISCOV|lB4TMaAozme;mV`DlVfBwMowLmp=1$!;J7U<$0*ZeJRoK zmTKvW1vabK{1%l7|H>CDFkBZsJ6WxkMxMh%K8wJ|RJQ**snB2Y*WkYGZzi+tzBgjumB_; zN~NVc8&QyqIl~>UELLRNd^6VVpDb5A16R=4P_f3tb}j9^apf zyHa#ySX7-E%Wy57)^L=`_;eBsjIXaT_`buYDDQ4MP=$Z4%t~m-n2i8|B0r$3^dlbU zo?_}gGPim)_lf7B^0v*fH+(;QuqvTtA%}c|;QR_Ywzn;PJefE-xtqND<^d?E(3|N5 z)1nvfh@1|3|tQ+J`N!i1i0G{mB$o}PnNdN%5o2LR@M#kyi~gzYPRx9wvV=! z8dY&c9E7PqK@~eA{Cy8+B(#TK`Iqddt*bcGvOkA1rQrug-?o;chCBSQQH>Hs4p@d1 z?*#Rl)m^TPu26B}g9gX;y2L&*o;7kZ&9$BCb43O;{S^ytx2|Xoyy?bUUJzwjaoWqG zTz|icen81IcFlRHDYk`;S0~r2(d=aXJV;4BD>ee_Dpmq>9D;TA_2yxnT|}&FJhtXrV$>8EN=m7pC@PN= zp22>|UK_Bx3#dr&VRzCQLJT7ft06TJHsu6u?R8c5ZNH|0U)KJ}|h`pp_}&lnb8Nwm+d z6JO52?89R1pGLdDc~QnL7kt3G(MB1mQP`L2nD7y)t+u-m(qYfPvCAaUN_T$n6BobN z)iX~q&VK(h%%wy1T~b1Qix54sAyKywQd9P8`t+i=nodN=Hu-qwA|7IlKlFo!#0Cia zE;@Q^e{d*AutepCu%fo-AK4fU8U(Gkww|<}_APS0Y6PuLJBY1IBXufYP|`}qw^9JA zKcvi{ba_rT%n!;594L!>M@-EuU_!3;7$Sy=YV@p2&WAHzXCh!K<ZIq~I+%toPuD65yOKybH`LOU-xCHkxn;sB{m zXzojPsK65l1C74=!z(QoIe+(cBZ6-K+YOK-o|uy=s3;QM(>J1jB@>$!kRl^= zYR+4;=jNNGb%jMy6>Cd=qP#MFljQIR1T9=?<7dlu-Ug|C>0_$3!@KP7A3l4T@D%X0l~scJ zM<%w=;p8eHUU=uY@Z$oo;X}x4<<)}RA6jB0`E_#*`?lv4(v@j9 z_`X>+I~N`uBXSr_wXuyW70~{vks>S2&ot}u`=Qqo5C>Uxr;J(@XlJxn?Lh|TII`_k z92y5Rz30nk9*{_t z3k)`Zp(jP^ii~d9dm3p&f@TB~ij2>>*&Y1Id=AziOtTV!^Fyllq-jdMW8>n4sIaJ^ zzjrE=X?-Hn%cu9VA2sA3R6Vkt|0bDGSoUiYbPb?`JM-F{oz_nCs@46ZZ|K=7RQ)h% zxI1MxrgU%gLS;V9@k2Ad={TcQoualvy7ru!RIMv058$FEPAbdw-<3DQT6utFymOh& zpcLDC)I^zcc-hmcKcDSREX4j7MdumL=G%sGs->;fYVBQAYqzyWwDm_()ZSDnCAEny zt-TdRjZ(Esq*m-zBzEjnV#eNy5kcsi_xtl9$MNL2pX)xa^ZcEI{d)s`2({50VS$^A ztk5;)M)vPMi@b$2vn{Q*)t<@`B@5=iX_ecnW z5i5G>TFR2JUdFl)O9QpeZ@Gz=rJe(9A_AJkS(m@hYt#T+Rh&nG3M#fD^q=zq?1T@Z-YU39XJR)SR+20Ik0c>VR;i$7N$d|I1W zugpkBJj zJ0FsK{G5Ndo}x>pTv0huJ;>nQ2K$VaElqBXMAToq?Bj3B;#zO<_Yy8V z4&@gacBBF{dpc(uvsgh8Uw;}V<-6r<%diaV;4k_r6^k4sKP-}$bi0Y{ROD_X39#OZ zV{!S^*t9CuZQacO>0{y@4R3n)lG0p=l^n)_mDS9Ae(uIj$`4`YH6A6RXr;QB?*h+M zwQp02>5)U!KMG%;(I=Av`C$zgwycbni=e8xcDbe3M? zwYsWtSV{p$fWG>)jEl^UmIRbz{lBRBwObs4AD=6+Ij?VP?a~Bc0iwj1<+)SBj`orG8&wqua0e45Pgo zQUu2-YZd}|_|kwwD&ntCTi);0Ieo}rFQS4DYQC9c<}zD9gFIgH#8)ULY1pvlT>3I_ zLp0h-UY0QLFIu^tY!$JJb^#szbWo;Xasc75SeE>HyM^g`S18i0 zP+(g3AJdi_<}rHFhjTQ^qpKgAItFot7G!VrtoWy5`nfdGDUuHM9|hf}qC{bdi=u5t z$>mQ_T5xXgf61bL@*>tEXP?#nKS6qX#V;!d;@cZ8GN<`7@$0Zp2>KH&)p{NkpE{gV z{++kybFz}z{B&-JMkF<$LMKyu#zx{h)c1ULYs;_s#C(pmHoSE$B})&HL(Kidky%-8 z3%s*s#^coKwQ4cq#+Bf~&cv9FO6POva)?Mn6OfnhbKELm z!}K=VeayAaly!jxdI&r|bD+=rLbI^>0y*?KalK_n1Tdn_H44-Wwj2S%TG0z;7>A9G z_#Oobt@iJPg?Z&_oyq`KtLfuSKQ{oc(wq7=2Uq?ki*X)P%0&BU&Efldo6Xx#;6f!N z00*A_l=Lul(QcbFqP@kMa--!?2bp=M3DciwO}QRoHSv)Iz}ISuU-tC_{o}>r%xlDk zwJ0LS9;pUg^Ol!cBOZB4L;?Lger^S2Lff+&boFM~OJMA@onO(T7O!->!yj?o>t8NP z^eNublaO_@B|*eF>Ki~FIfT01{Q&th^$v^nprG?m1g~nZ%`=mBt?GIid4EX;HU z3S0ep=8X^|orvkb_=|0_V+&|S&=@P$e_0d+4USv&(T45!-WQR*vBXTHaIy~TcuZ{U z%W3NfYnH9>qArHN%F*fjqDQ?;-~Ia++o$JU7)<<*r(TgQWVxf2Kt54QkrAfP7HCfHY@ zU7(k>#k4S3sBOU-HRFFLY!6l+m{_}k`g*!!(ZM>kFEa$s0gmWlfnJZ}oI)3@>X|OP zFK->>oa!18OdE*$rMW4rt{}YY*j$y=R?EjYe}US>XF9@j=0tnA7ScXEBm4vGduhI56kxgYw46EM;>cf-}!MO%tyI7-B7xTM#Y>&xoIQK8pn&~FV-XBgT+aM zR^-((LU)!><;L6h+k6{`d{+o{pyWTqn*U*3W4sAh^O~|)K`gS1>zg!J%tQI9~ z&C6WwOC9v{N}cZ5m97PbDsD}e@Bbtm9-Szkn+qBFL)9HR$KMmd*qvJs+ZtOZmZQ~s zVLlx@JrRfB7yho&9rb}gave8jq&cnVWjPn==dC{R9PQCdHwx4N4zfJ^sVJ_=L%)+9 zko4QPRMFxG4YWx@)F)c6EKHi=6e^4p1Og0LxBs4 zwQB&^uU|i&x+3*{p|6B1Ph3RY-;_4DGan!QqY#);w!3)lT0nCQ-EDdY(?Zzc&CeelCBb^lfa!Po$%>58%37D{eoK&N-kq6^EVw(hd0bDW z1V1mL4y)S`l3yJ-j(XLXw?F*E9=FVIl`X#W&KIoQE7{|(GHo1JmE1Bv?*K=r(p^31 z@kZN4gs7MHl|Q1buvA99+49b#jKwH@skvSuNSfC)=n2TWx^=JurTg37_iMAxQl!=T zzsAkcC3j4-7fgY1fua%S(-(N~DeUQX9YUr$)?paA``>l(szC1J?DW)}98dI6uN*3Hd7w=`JvD3U{_3XL!)SFRH zLsSyW7$MQdFXq`c;KhAd|7YLn4Cc&Nv&||z#v*{L>|}8_o$NIT$kmC4V_x7*QJE?- zovYjmeEq-j?vFB1tG17dHWKZzCwPNOdZP%jZuQ6T=E~-;UmEgDer$HGq{o+y9k+owW_?MP6RJ;@ zN?JbT10ti8;?CbPj+QaT6j@0X8E6k^ppIT&DE}y5Q}a~fih)hT*#Qt4;CN& z%nda>j$?}`HuiBZR8MEoL(Az+NCL8nL zI`WD3pz|dT2oAH7cP;>d58P&VQnPlEspKZ`S z$dpvuIBd(mA9@DSAf_OKqI&6uEoJLt>yA#N@hL+oqb-ZGs&86ah~JlUo2GkR!0aFd zjhHHO!!~$%p4JqfU*Vy045WMN#12X--{_nKDF&nd^5XREK79!&!ivf1+>?zdF3)sE zP`{YuSz1Yfgt+^~mub?jW*Y-@zicO*8K%Z@K*5$s=q%hfSg9SC;@rKEM zQ)!oH(Z~h1*)Iz3fYZt{%f}N9^xmgNGb_P&-3W%_Rxv`fU)|>Fb8~699G(5OcS*F% zMa3>CCi86%$}^|w@vbNdhsDEUwHT0q;3|24`^?q??~hf9ig0B)2js)VAB#xvIa#nd zNAVSWJ74lg*aDA~u{u&c8tIV1_UV#yKYf$X8Ca-<_p+Da&uxvbc5Mg56Fa5FLOAhg z_jiEoj=}F4h-1?ceMeEL&Yh&}m=Uh{ZEu55kN+mmQ$hvsDRG_4qV3UaA)-FlYHzwh zjxSE>8*kqqQ4eR99y>QXf|i4+exc9Fb}8cT!*1gcYlmzl+b?nf)Z8m5Z=uG0Vi13Tsymn_2or@W} z<}K|!eyzSfO9NoWydN;MbBS#!(AC(FxwOU4?z0(o8WH8&5OZ~RUdgH!yOMV3l9$d zQ7m&$Y&11CCf;%{&KiQ%zj<+1?2xQ4&vi7>%$QJ6b5()c`$sWBQeV!lUKIKn4^m*~ z`yohaOHlvZyPj-R^kQhO1R5$senGbR`Eo+=sc^^*_ZAD^THAypfCJ827U@B-;npau zRQ^v(Bs%FDjI!IKop}4ahw$Mt-1pM?W%Np)tys*$ocSb%o>xCYO2Nk%EX9$V1My3mqQN0hI1uE($7DTm0(-s zBf|ENqUOLuVlQXs_HAoXioxU`OTTbE;S}=F>fxHdP{)e5!h?(G?Vvf@FUw`@?|i7& zIJY=255PSRyLcQY;p4bXJtm12dp%2u&t|KVV)Z_qmA&D;06Nc0%E6WR1UoY6%^*M$ zn-$fdtG~>fgg!ir^B*H=(z*7eWC}J}yE!1AJ~HiJeY>$v{#_aHn)@uK&+J}*d>d#v zD`qPjnQ_AaFt$B#jq7tvs)pjDE~pk!_reiF)bC~S9;(vux+q+Ov<=oM&s}Y1EFR_4 ziv9QKHT}GLn6qWsy|}=v^r|55M|q3%%*H>q>KF2_om-eV5Gt#Rrx+@fJV zwt#~ZN_btO;;lYptc2}}&Z-I(9w~%5Ue>-zFtFS^Q(@f>6(VN z?Qhef4r7W37bG05^+ zZNqEV;S5W!mK@VX^~Sd1jDwCGasR&;S<8TWWh?uSJKV`x;$%yTR;onK#@@%dPyLX? zqeZTq3!kM&xyvcL^DFc(Uzt8GwEOz2-hOoWSQ*+LW`2dVY(ONEj7tA*%AR_fIw9i)-Qc9rl=NKTW*qb$oQD$s3TDVr#)80#Rg0PEBjy1C^95o&Tfg;SEjfv$s#K zFw`5pCF%=2KihKv!Y%3Bzoxl#@J(8>b8MlpntFN@CT5r8q&_DEE~`gPZMtakUGl5d zs1AoAxA}p-0Jp_G3+(0XWpO{+jhWr%_bEn^ebNbV&s_z;tjr9N2J2q1Vg-y9Uu&B5 zDDwWZpwJZXaVqYS~1$XEibeeYz}qM5&cVuRyOT9o9-9P zUA~5kqJ7B)S$tEA_u7mKt%#r9A!5^ots5U90kdf_aKw{tvn<8H`AJg~6Y%9%rE&88%{KBO1g0d}Mz~}^>x|ZJBUWy|I1%A!yAhUT@+<&~RTUXMPZ5|%wfiM^d-{*C$ z7Z4C$L)lJLzCf+9R1o&u&!7#!gltF9D0_hI<+JlJnbR!NT_x-sfRWe=$db-C20VZP zrPD$4wO|^+Mhd(QAUf#UR_dOYn}gEJ+y$)#^kYRv<4p|{blQ<)$ixNF3FyJYhUl}C zceCkx{!gCHJ*R#$m~2+g`P`xFz*codl{-LFpIGo?&em#vA;2BJ#Y31u3(2uQx@FHZ z7tP53=B9gC5R$mHLfc6dkiYXLN9$_-_O}NqN#4=@hF-k@VI9tQdrN-pG0a2$6=EH;U(>Cnwc?@JlBosTT;)zJY4?H;;6XGI{i04N&RJNKr9!UxTqY$2v zOcdN2@;n)L9etDPq|NH%pi{*igWi{tvwZ{A7|HCvu67NME$elVdD|US zmKmxltzj77-ZQw`)Px`OL z#`U5%Q!WQG@#6t{?!PbPY;OkR<2!N859*b9t6j%mv^=iv5!n-R=X9YN2bXCtb+8h_ zSNC{XtY%$clVbi%f_LEY{{31MUfWt*2X;-WRGn`NV!$~4gK%_MJ9b1^ccv{^=$Eyv z7MQIm<&|LQU+lh_=Z#olPx&t9lfSP!d~>5zirOb8E)}6d9z@~7PsxAO+U!Qj^m|%T}8JJr6F@YY%i=D z@plns*Ei2gupt^ShKC4n%~ZlUZeWNI;4y7HEK;Cp_+t_iEFU> z9O@M)i;_QL)9mh_i{0&O^b^l7UMUH-Bi+APp*sxGX`6HBKyMp}^ddX{iavUzCMler z#z4u1`U4QcJ7^0^j19<|$Gs3H!x?#}^9P%)g(+2p39 zK20Tk?NZIgvXSzOA_ZiM%gy?Nr<2)Cyy9gzRMfju2Al&hlI+EL4>Lhy%}UX;m55xO zhIZK4^1ib57)HF(YKK!&RQ4v3-B8v{d6$QTeVfpEz|JXm#!~fQ~qbxruG`857;>LPK1Lc zay+Wh;%@CNyzjHTOilFiYGii2;VSnXF|c1VHXg0D9mzA!RdZqE2}{V?G6Y9R%2g^h zHtmo4#XB7@&_&TZ8*?r=v%jO;M=ACds?~W>gj^VFN16^xCKdnLxDP zQ|)Xl7Ro|-b1vwCER*g z&~qd+*~LmM{+!{6e){y~Z*IJ;V^jLg9g(LXkJiRzpYe(`R8bUp2I~}=7}7bOnE@E% zpfP?jY4c$-%#7Qwst+wF%{&OzpC}+caPG?0d33@X$AL@VwdGFKbhQCk z2nm!6SJQ9>UCA8#$Ev!02+_h*pO3D;C+C=$AuJDG{-VDA*k{{kG7W5b#2a;9MtTe; zJ_Mvc%w*x*^K2g(F5Yl~Q|tfcqTI2|?IfpKKZxrs!f$z$4alrbw|6zQLut~LgJ&wFa^R_D^4+KrXcxFuZud@s>#g14cRoCPgVyZYN|ZTpcY zz@u%{`$w1>Ip>UPg<*b$I#nw5SkgtrhFRzK1<_uFxb8 z_>9E->p&G#mxmGnouYO3p*$8NIHT9Wp1I{|xMZ8aFN7cPI_OHfvdAiZZRz4(#zvwu zJK{CUZfWt8sV!j4%y5#uur2q88GO#?3~ywHM~Z=*#ir0wGA=QE=U z-HHb4z1h5cG)_+|sHgzXn(Qu!W1b$|kKaCHSz?$$8^r7=n^UIyxX5?{|{IL^0qgP9C$K`|3kSBr?w`+W5QRP{) zmY}2Scl-=ul%sV}I7EaU>99z0S9w|jeN3&{yhoFQLV#NIIQKxAk|LxMnJr`r3d(G56u9MAA}4pmx550v4!k! z2tL~mSy*R%reB(c=YALe1tc%RukMVWz;oC%nw`?uMmZFo_f(vmMZY+fIV|n`F>?8{ zCa$ohalzs%Rk%F&RmKT;_XeI(2hZ9i8XKbH%u~n{-G#1$v;Q#NEj|hl1_>{;@Wyj! zMu+G-AzmlwKbngKX1siM!aHVmw6DY*F@*Onv<-oU)?XlNAsI&B{VlZTWof)#F;{%Qn^@<}RHznHld0BkP!j?Q1#-noq@lJ1-tZoz;X>gEBVKM2~U_v2C@v zc5UV|uRMyP29hE|r@8#P0C|^>1~#;-?f3W&xJfy|JbQ}nYzji{vnyHS3$KhJOCpE^ z`!0h|nx^~ozC8Sw-;}}2(3@KwB4pbb)bc}6TBZ3S@&mrdGdaxk^`S|M1{KXrdnMud zWrDP7iYmR|!To?rIOFoXoK_19iz$qSj8F*l6nI+WIa=so3=f9sDRK}bF|>s6x@m@# zK<8f{FX+5f$}-3}J>1^iUAD8AuOg%1H+i$Rk6jHm2v3L*zxSa^dxro(JyW}O^31ed z#WJ|2Eb@dgO0~hG2ZWDK_b)=vZ4>;pTT0qs*bk9AN6XRLt9}&6&`vVix|%$MB*8ne zv?D6-F%HswIqw~ssy&Oi_Sm`ZPzNHFQ5y#cFk0gu#fn*sxSy`+c`)>zT8OYq*>&q% z<^|7Ob{hG3^x4^<@9|b70w>A7-2`(0;WQwREY zteL$M=!Hl2OJ^=uz(;_UaD0<`tV!kenPoz-4F*4UW`-^3+krS6q@NDmuDqME{Uq%c zjUZFdrOM;~FYq4>7kH!$Khu~=hCaZP+Zsqpe$_^Z&f6jCCB$f5z>0obLssy$#LmYX z&HG^O7`+*34;q&V3uzRq8N-@{K%8t06;Um{OSt-e9-UJMTvpFrnc+0PMd?OlM*ll&*mTrgHGSn$GWiG5w zJpS2acbctp1&;FOz2EWRLnkxIs?x^g%T~v|nhy21wMEJxtHb2f2Z<-DBA+Ob)^w=; zW%(GXKc>EyiS0SFtuie(G9>Q3<}=mv5gm4QBM86+0_jAOu8u}XoXcfX`wt>aa(7J^ zd=1n^L$0*BSECW~HpMG*=P0lD{^c&?ws3NG!4j5!|DB6!T?u6lJ%;;>tktmXEt>T% znlHDw%%Y(zCeh1|Q{o)ky8WX+s;nIRh69(9Mv!ckW;qZ-qy*>gxm%CKvPUTOs^`it zdHPSF)riEq=fSi4)J1x}yA!o*pheBFOmHTl1#s6PZ|<=!`n&=1JwHc~h)2>yCb z@D|yshR(t`xj3c|-?=9sx3CDG{M3F4{DtG)Rlx_Z@WwjX(GzL=a*E?iN;4*h;~3=b z5;A+;y06o1gfg6bES!28#r}dl`VWm$86nBe-0Byfm3Gf~XA5VBIqsWLf4HOcD7jlC2DXr(h6H z&6!AboV9y;5#Vaej<5Hbcg@6O=AT$NG=2Y1pT}FxeZ_uTE~JWvvd-4#AH|KwM2F=9 z>JY6-0AGl3hu!TS?2gNFd-A|HP3~F#(nHp_A6EPx4Zg{scv% zz54rn>+#MGvB1SlV5rHNnF}2`?P@@*N(Tk`?UF6+($6d}oBXI}9!|<6?7i!6xWWVn zXN&8uNX5p)gWgNT5L02&ugRpC?&>;%Az4t3D2PVq1=Vw;&8lK_t4!-KdzmhB_va({ zOrm1&G3oY#Ty)M@l_1)Auuf~|G_)j5MMmJWbAxWPeHt({2ae zx)K}jDLdLp-Ky!d!}gP!V+84bgbfZEA13Xx@Bnv};juh4@~*$5q0D8IFPgUA4e~&Q z6f0y{=IZrDiwY##MN*Z{oP3dS)^%Ho;$2ahyPnb{`g{e!G>hJEZfdMEy(_wh_=e1S zO95x({j{lQhElC5BQXWT7we6swsp9I+_QysauYwTOwpPBSNCXHs^gCv?4S%vV$48} z*35<~yE`iqb%Ny^s)zIzl`xR1V9DsK2Lo6};zL1DxW;INQsKawnos({*lprVwDI`+ zr@Al&;d$x9MpB*L#?D`}ZA70w@!`qo?#2ZVu}ojr+;zelUZ?oL?G3p~u_BrlypXyYP;1;IwaguC%;xy@SJoF{p z_(4DQsl-1_nViLcMnt~6>c@K^@1(jHbsIiq{6}FIVW;Oxumd)Pj{wZIYzEsif*9d# z;1%2GMh~H5k(XA3OivZRH;<}XUK-I@C>*Idg?W5aFF({Nj9CVz+}UKomBp;*tdE8c z#g5J-x+xePq@&QV2JoGLI*o}mT_4>x7JPq5^Q@kU{u?l#x*?GnD@j!E;Qv-^Q~JEB zaM5*+y4&zs7u$9)YWhmHX_DNiaZ*Gl^aXC}I9Dsd5ahq2Y}RU6X>6UQ`I#-4@bRqE z5Fb=UhC>Ecs?-GZUp+*Fjrsyf`BZp#`<`o8wOZUv98eB94Y4s05`kvBE5NS^8x z!RnbH>*O<&As)SHo&xfM){djB2Im~TZeGnFWh!eLDT;U`%r?hJ(=GB41~#7+0)p0$a$vB3kL)%2o+WcSNU~QIg_)K z?33$U`VM9YdAxk^Nm4Fe`tre^%&mw&Su?9`1)T!N7u4~5ClBrP#HjkUny6~X0{YcQcc+|NQfi-Jufs7JUiBMLvKm27 z&8kvr6$P{`%{?EuK%{CPrv`SNN5IKVB#n{pIRYvSpfVzv9R;iTDOZ%G53B5BR@ZH# z{d-c-3U(D~E60f(9HI$?eGF;6~wxP?49;tkGUDPFfdzh0)$umUKATvuiRa z64u@}1vSiE+H_nL6s(9$#i zOu=Srq2woD?E<*fmO7(VabI0v=aYSY)r)i1H=#0^3{I9OT!OJbZF+a<;2QiX_I7?( zBjkUPlIkq^at`j5HC!Hqe+9$hbTDsejna&^l(RK=HxLXQ!Z29nS8{73cX*u zN@*wch1o1V-e6nlBi+SavjKORE$}tQ4CJtUSc<{~Yc<|De}qX~p;Gvm4Da}JSqcy* zZeWqU<{56s+z+>Mhod>I{{EvF3u!BO@}B=cvC2_Ro1%&(l0IIvOR+Jk-YTJ@TCinG zI`<4>8nD*O*mQ<}RdrLacA%BQIYk>8>2pgySd42$drzi*zTr@Nq!`-K!pqrf&i2k> zBsL+(Mgp}%{OIGe#gW-sWwjL)^5yTs>pq*(H@Ot-f^}4lT>076c1Ah+RIZbj3pSR( zH05~up{#%h>>F!KFcn7azG+Fn5k}+a{q1dF9D5@-;?!L!egQU4*4knTSKQ+do9(Hr zSyQPUpPO*Lb?2n(@=8C3Vp!P!6g0q1v{`n3dbFm`g6y(!0g66;4w~Xc{-GbMQFGr% zz>ERezJI0vf~c)T1MgF_-Po`%IQ>jTI<7|cYApxqR-(7+D%l;Pmkn23wO7e7--v#M zdROtbHzlC`P|;L4 zrOn^1kXY#cP$x;K@eJ*^3VJN|@(spP$XV>J*Z03&*N<0(2ab7J+N*0PD}S8{@8UwD zzA9`8aD~$cIc2XX>Bj+RbG4=*BMa$e<%fS#2nwY@x;a(GIDJCg>-vY2?=P)#g)5Lky*GLnZjhs6D)|-V7S=B3y}cIO z-spj=ESe9cCaQu#mK8XiUS5`&QSx@bMX~pH&zh!R`M`#?*jrq* z#@QT$CtI4*9eHS0pVQdS=9&|gf@kXuCuY-OEYjVMo)GEZJFhD3bX@*XP_HxYLU3Rm zyno+_ET`&|-H>S?u1rQoz|0{ttj^@K_HxJ!)xJiXeW+Gn zm?crEyOLZxqF7o3^x;JdH2Tq1erBbTKe@6YGeKka#>+pb4_QAGFE{@YiHQ88 z7ux~CSS055uG~fwwuiKQ)Q7E8QLUNMpD6S4>mtzxq#hu!c}9&j7n2P`c8Y(5{)-g5--(|K=6%WY$ZR?~ck7iPA;!y&N~8+_YgD!Sn+=A7U^ej|~6l zqrc0kJDv8yDaF@=CiAaKOSsR;gU;{Zx@FzN^?-q~@>4xov%_={aP;(0vt6%VB8Ya64xNJX5YsPgXC1B(Zee#chd_9dRZq`rFx5LjIXK36yIS5DfW(0_E)p#k z73Z>d(?Sqwa{1S07VfQi|LtLw?Cr)GDS6Dm$@t~@RauE%632MmX{aT5^&bV*AuCpO zcO!5aSbBTMhgZpwv3~D_Z{w(13&6Q8F^(WOrDkVJ-}csd^5+wcx4wSR0UG&Il?60ugn#e9ge>yZCGSl`?XzF_IpY;!t;>O*;3vxE801+7>HfG(ZTDoNO>i? zr5KY?)G-aM4~}YU8n0KEWBNP%&5@PqSN#Vy$_|-@E=TUN&*$71R3RZ#9dblQtX(ho zzKyG8exZMbMDBH|wtTmGp^)C+jq3j>jCe&H-#!I7Mqt(!;|l0Qw6G8+PCG?a`T%{s zlq;bip<)SvNAk~xjVp5eyPYx`nP#bOQ*af0CaGZ$7=cnm+ac>rL{Cc+#?|bO9uIny zV4|)O9LfHazLIy)n=?Hz-B$K}w&8H|wn*F{a$G2Q7Hq{n(QW*5%CQ>ik-c5Og@_EH z39NMW_w2zIaJXjix;jk>A3=QoGnyPUG}J<}X-Jd<2uSQ&OZvy0PMm7_i*LIEiUG{`&^{fh-(Xf0ViAeLd4-k3g}Wp=J^#U z@7|h?>_s%+QMMwp%2NtB1zdEmDXpLpszy&cm4-@Hdf2v`i-7{kWijN!4AZC(UE@M_ z7bFma*8z*$wB8|d8WV$yTnLtK(CFjPm%6R*(~ToWvSZ|(y}g^2m)=r-wswXuQhtc5 zbZN|IP3Bf#6t&Q74t)t zqA2Afl!72fC+=ujIHu}&Rzg{;!e-GF#}`7|1t6#X z0P`~61-!a4F$PboRpz*44Y)Qmflqf4ZN3(V>}}Nd)F8gu1tV z&D_!+sGiZX2${cf9_y@eCf%pe#f?^$ZKeaC5?Omd_8I(>?;2&d5ne*r!+0g8K#5+Q zil_$xN*ux+5kAd`^Wz$#P_fEtz^9M##yE@%aC7r_4^uWOUk>II&L9s>3UOY-q(q#0 zR@!I=mc34#Io09$14=fT+5l4c>?>?p-^DW%KL2{+l5L^4nI|_m78KkG)V=AN_K2A zgfR^sS?k>6sTB;oTgh-S{b?iT?-ZG3eeXm5<0jy|q&_g%J8aq&ZEWgq8lz(VvRsg= zrmKHf5fyti5GFsR%!{2>eiK!lBR|ZLn?S+-S*Z>lz(%vPWoJDA4NxMa#B(ND4hHK6 zEvVGY&kolN4_(6PZoc2Ar0IBKE8$s7;>_T}(C_gJIU6FiK>ChZ>AHashQy9!QO+Ay zHr|^GHzlW0;41_J{VrWL=L03h0)Js>32r{(jjP?_d4f=Ybvn0 z%_W)*@+geSc-(T8)0b%*t8%w?+Vm*y^#| z8A#Qp%rvpQ#Ly#WNO$)fqKq8Gs3ypv)l!}mYgkuoq$wIeNFQ2eKAJiX6XxtZ^GXSn->#-ty7>b=u@>gTC>mo#fz=#F4=>yW+I4<+e)Zc$es!Tzw&W^AYF z0C7bCZc&OQO==vCC@0b7U=CbOe=(jGGoOA_#!w2m8tL+iCRUi9 z=G3GHt^{nHg^f#lOU^rOkgk}K+gD;ud8FOipeG8*u@mYRtAYp^VJQ%u$1C?7Vu%T0 zV4}A7uGLUs*hN)x{RuaFjoY4ypZRzM+%u`RTi?q6N3j73+UU`bMLVS~i*D{$FXv$A zZix->%C1pnZ6Sw}Bb`bo!)|YR>8vfhcvhQOkOd_YAWC4e2jqo?soJ~P>>W?e)0ieU z)fHQi5^q!`k5=QMFk`Mkp;D=R$n$BkR*=-eUy}@_a)D;TO91nxPA zK!a! z4%=>PW2QM%96s}$%F4Q<=NX-StPwoOtAFG}2pMo8s`5~yDGj(Klyy2lx5aMlu!`1L zr$O;CD546C*kk-0ZoU+4fgwl9gevK|d&%cuGlffMgBck+>K#_kzEW#ldk@+HpK{xJ zSy*MG>#y?VZ&Zx4O03m7 zS{zP$N3@l=vNB{Nx4hr(TyB}r?qJvy{o!4@Z}|HoT4s3#-aia;Of;py4Lh_mJYS5c zgZ=eU)~W5`=GU5-FBY$Rmf9r_xlBVijon%y z+|asx$oOKYxX()6$H4}XnKIn0OE5-%c)oGGbgX*0W9FO92f`qPxsCN^Wza%yF6bWF zh-;qasiOG=&uHF8);|*++Aer<9URD@b~KuCewQMeOlG?GHeY{h0&+EKNhnz6rUvdA zE}+b(tK3&Vi+dEx-AP=ESUUzOT=6|eXoGuG5&#c~sg+uU1m53UExPk;93hns7Wcp$9 zw(Ox};~R&wJO2xP5rXcJ0ANQ_IUIdWbQXR)vGG@v4~?wtb#Js>tHz-KOK4R|*tqG_ zBKZ(j zW*fzl)d!mHz^r8F80V+B=Dy;FQygp*=O1S7?ADUEMy+PAzgKH3h7Uihm(`RPysxZZ z!{>DLcIndhXCZTdSQu?)u_Eac)6z z99t3oV8sui>R5jBCk8L+2e)B zTJzr-eWK3m%T<+aZll`~x{T!edSr3>R={*yAj2x$^z%|9m@gn*!J_8K1Sf&s~!y-q) z&#})q{Oe-Z#>=JMCEQon7Zy68DKvLTj3xveAuFDrfgP*F&T0L1r^=h#T{QJq^FA+# zGisR3YsE_GHDzV4it9_-`|o`%*!07!Lmh>J+3MEc+5Z4>BWyO)*KROKW9~Vw%fbHu z5L(XeCeZHIFC`c`okkt`8oJce zaG>PyM)JAio(4d#L-=v~8TgCDwjO@5d1D~SdB@x2hFG!A5QQqo*pNN()8<=WA6|H; z!P;D!tIKX#Ff?O<$2of$vHvl?hWOwggweb7)h}JZ#HG(ZlWzl|7i<_2qh8E#j zTO9}lFv%ka75N4;EvwBsl;a3FN!>y`v0CiAC4CZBS|7h;xwSZ66)857O-fGft94fV zZ+|21f7xgDLHG~x6Ty+*Nv7%(YBuT?{X9i`bq&leMt{{SA`!vCMr9oHkzKC6{{RHG z_$RGhE%Y$T&@`E7Y&9#@M*je30J$R^anin1_*wf*T6jBG9!0IJak{%nAK8TPGh}}D zJ>>0Uj20OT52($33E*Gbg5l$h)o-Q*1@hpxAh{i}k8n@7rFoObLX@93y1e&Zx}RNz z!eV7bI*m8oTF=dYUHTrMp?|?Nz60wzKl(4j-w%0vb~WU}=I%(?3=mB61H0(G>%%{3 zzxW|fg*E$Wuf87K+gaYWSZTNDBaUJI!x5FoPIKD5Qpfh$yR^NQNi^+7$7l>m)7(gn z>SX7FJ--@;Kilt5x4l_D!wT-pTuk>KTNekak~tjn(B#)n7dVB~snvJxpXs;YKdIJE z$tJryKkMXvRQy`~j`bgd-X*xwb?p!PCi>i!Xy=D%VVv%H^0*X7Xib8NQ^={ZWBn(6*W$@(sv zccX5%nnNwQJZ>UFe;U%Zg*@3NX*{=$a2eEPFh9C^6_MlL7Ojgis|J%hcB2k)-2VWD zaL{?34)Uc2H+tnJh_ zz-U!LY-MmgN59jxbJxEbKFcCTlNlJDzbW~f{gK@F>0H*GrQUdwW&YB*n$F(`aEt;N`jz00p9r_COFu5%U?$1(VlB#O;R-|X*uZT6- z(l|7|R_aLc^DVpN{c->p{*>uFU*f-+TSeBSY=h+sbN7Fd$G@$5W{2V%Yy25CVm5<= z7~EUaImb`NwRFD{FENFRZO7$pfN|@`73@*r{1(xV;8*s2G`=6i@hH4`sKYaE(zQUU zpbS9g2b|>f{OeOs_=|mN(X^U=q&AQPHN2mmK9L0GWb8iQ?8%lZx6?)Xmg#f3NZ~B{{Up3-o#hud_}@pbRq5N;k~51nosil z53Lu2vM%i?n_<>!*jtahwp3=cwgXV>$tKU?^H;Bn(6zF}}= z)g_V}h$Wm{eC`VtUBKaq2Pc#BSFZTW;m^a}dO!F{d?BUj8itmN&nC&4F73eX>C*rX z+2q%cYJL>cd}pQJPjJxbeq(vMY!SZEXu$B&IR`J(DmnvS50}cF8OdRtN>O?^-&K8; zlh)harJ?F)nOnmsskqd>soSEp+iyjEtgnB(ddzVtt}bk3JS87 z^LKULqi;-PgOl2g16Nyrhgys=Pj7f<%$JPOuKAgd-7+3XBb~p?HJ`3{e@DBrn&Rs6 zJB!I2<|xk9wUZbi9{C{l$0M#QAH|niAAq#0d2}0%PD{z-G0ha%WetE>fI1#=o}=qu zZHLO~;-sf$rrd7abbPD+AIkp#BjfP7(!o}B6jZ(0C1%rH-F%;>wz-v~-s)F&%l(<7 z!E0w_m3wqy9_$_oCz4N7&q3C*JY9a;rOa^MTH5Jhwz;}aNyk6{IAh7=`qdv0Y7pE? zMU2-8l^$f5Qw3Hz=L4YnXZhAdnlNJn*Az_U9Q^u zzUv=5FJ&vr)_PlAeDB@guii(f+3B7xjyJTqmeS%U#G3~Aaq77|9FCnnm2*lhW3Jo5 zGRHHZ8*U?Jjb0iC*WR~vpR`MCjMs@1ZR)M} zeL&57lHX&?t(Ub0N1xrcm*3X={{X=AJKq_2vqzKc+H4Aw+)XOwkr!-u%Op<$= zvGBv)z>c&1)Nx(SBAVt7E1QU&guXMOJo_>0{5s&`Yt1M2zcunck=Hao z0N?1b$>IGQP|$u+=0_%>_Q-m3kjM)D2OW5?o%KySO@qgmg5K`lJwDyciVS8&W87eM z8?#Qi_=l|Ok=onOcF~)s0V7kebKIO(KZ~FIk=wVg9+<97MKu)VX6@Ulq_EX0)KsOXuG)3erMsQhg{)0$7N7Rb zK+kJ)Ahr8EXUj2Xo=0$cbI{jef1(Xy{wr8xzM9%#s|&?;%J%A~a`h!jhV>a7^NRAD z-9{(fiF&tvWn*{|ZBo$$v_v|H^huWl7%Ep6ww zmc4#*t&prv3jwqff=4;8cGdp?Z!h>)EfuSOWGsvcYi%|~SP@ufcI6y_-=WS2OjqWl ze;r|p0MS{(*FXs#cYTI#0ouuprEoE{b|<|u4~~l^thU!y5X!e-D^E5WZoH6v0N@NB zoon&D)5AZn;vOdyoSS;9@9D0~?>>S#{W>(&Dq1DCW$UN>1N4&P{t64LS!}nudtF1$ zjbw>Tv7nI~<-t*mV3p%J3(f{>QtRX2h5jW;ANKO-Hx}~cBzucc41{D68w`+r4;_d# z`I`@m=Ci!Fdl!P@8&J}#-W)eQ4%q#_g_p)FjVDc?H$gQE#tRZ5+`d>-l08Q~ z4@&ZDcvC@ISZ3odEgrv?nw3k69Aw;=;(mkb`bUEAOIv8ZByS0eq~a7|CdVLQ31CYx z9Q@rm=QZ=E?Hl_p&8ow1b*K3A`$JEW37%66t;&9o~69y^i0`8rx91os|@cC32yO845W0RCOb{;=OFEgY!%U zMOu|s+Wu&!-_5K2?f$2wM~P|GYBg6f^ZdJ--WUCdwGC@sg59RGmDn>$G_DuP1E6** z^0CJXLC@DBrud`#G1~Z6>T7!>nZzF>%90j@xjg~k{(`goG4X>?*K~!kj!87jk2vqX z^BLKJ=nAfKJ@Jw2T}OjHEnMptZKdkhi6m%SE+GBfyz?O~jO5^f{?Dm3^;nE%bxN*|IjN`>IN* zJ92T6liIwW#uoaOyEJ!JvB-+1KQ97BW1fQp2iHAD4SkkH#8oQW+AqIH@;(P08uZ)d z_m`2CYo$p7eX~rswz`Fo7LI4%wY_uO@vf@J;!8D(+uqyV!m2*}WjADZ+y_s_yp`-O zEwxD{(!}=dA3K&{gD18>9+f1o;)!Nr%?z=te8YAFcj!M_`-tKx$tNiey z7031JaH}emjp+ORe65nAdJAG61( zM-Te4X1FfC#4kbl^rXB?Td3q{B)sy##?m*R&yGF0qUXfLt^B68w`*9y+vc6IfH=re X-`J9C)u%;8=#JWSR9(_<@Uj2d>mT>!jGU~c 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 tt_Qna&aFsUb~*nK+dRE#YYMVVf=Qm-$*ez@Z! literal 0 HcmV?d00001 diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs similarity index 76% rename from dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs rename to dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs index d7d4a0471b01..bc5bee5249e5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs @@ -9,7 +9,7 @@ namespace GettingStarted; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class Step1_Agent(ITestOutputHelper output) : BaseTest(output) +public class Step01_Agent(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ParrotName = "Parrot"; private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; @@ -37,15 +37,15 @@ public async Task UseSingleChatCompletionAgentAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { - chat.Add(content); + chat.Add(response); - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs similarity index 76% rename from dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs rename to dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs index 38741bbb2e7c..7cbd1b1b0706 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs @@ -11,7 +11,7 @@ namespace GettingStarted; /// Demonstrate creation of with a , /// and then eliciting its response to explicit user messages. /// -public class Step2_Plugins(ITestOutputHelper output) : BaseTest(output) +public class Step02_Plugins(ITestOutputHelper output) : BaseAgentsTest(output) { private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; @@ -45,37 +45,34 @@ public async Task UseChatCompletionWithPluginAgentAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { - chat.Add(content); + chat.Add(response); - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } - public sealed class MenuPlugin + private sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } + public string GetSpecials() => + """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """; [KernelFunction, Description("Provides the price of the requested menu item.")] public string GetItemPrice( [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } + string menuItem) => + "$9.99"; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs similarity index 86% rename from dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs rename to dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs index 5d0c185f95f5..1ada85d512f3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs @@ -11,7 +11,7 @@ namespace GettingStarted; /// that inform how chat proceeds with regards to: Agent selection, chat continuation, and maximum /// number of agent interactions. /// -public class Step3_Chat(ITestOutputHelper output) : BaseTest(output) +public class Step03_Chat(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -74,16 +74,16 @@ public async Task UseAgentGroupChatWithTwoAgentsAsync() }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy diff --git a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs similarity index 85% rename from dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs rename to dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs index 9cabe0193d3e..24a4a1dc70b5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs @@ -10,7 +10,7 @@ namespace GettingStarted; /// Demonstrate usage of and /// to manage execution. /// -public class Step4_KernelFunctionStrategies(ITestOutputHelper output) : BaseTest(output) +public class Step04_KernelFunctionStrategies(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -64,8 +64,9 @@ public async Task UseKernelFunctionStrategiesWithAgentGroupChatAsync() KernelFunction selectionFunction = KernelFunctionFactory.CreateFromPrompt( $$$""" - Your job is to determine which participant takes the next turn in a conversation according to the action of the most recent participant. + Determine which participant takes the next turn in a conversation based on the the most recent participant. State only the name of the participant to take the next turn. + No participant should take more than one turn in a row. Choose only from these participants: - {{{ReviewerName}}} @@ -73,8 +74,8 @@ State only the name of the participant to take the next turn. Always follow these rules when selecting the next participant: - After user input, it is {{{CopyWriterName}}}'a turn. - - After {{{CopyWriterName}}} replies, it is {{{ReviewerName}}}'s turn. - - After {{{ReviewerName}}} provides feedback, it is {{{CopyWriterName}}}'s turn. + - After {{{CopyWriterName}}}, it is {{{ReviewerName}}}'s turn. + - After {{{ReviewerName}}}, it is {{{CopyWriterName}}}'s turn. History: {{$history}} @@ -116,15 +117,15 @@ State only the name of the participant to take the next turn. }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent responese in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(responese); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs similarity index 79% rename from dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs rename to dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs index 20ad4c2096d4..8806c7d3b62d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs @@ -10,14 +10,14 @@ namespace GettingStarted; /// /// Demonstrate parsing JSON response. /// -public class Step5_JsonResult(ITestOutputHelper output) : BaseTest(output) +public class Step05_JsonResult(ITestOutputHelper output) : BaseAgentsTest(output) { private const int ScoreCompletionThreshold = 70; private const string TutorName = "Tutor"; private const string TutorInstructions = """ - Think step-by-step and rate the user input on creativity and expressivness from 1-100. + Think step-by-step and rate the user input on creativity and expressiveness from 1-100. Respond in JSON format with the following JSON schema: @@ -60,19 +60,20 @@ public async Task UseKernelFunctionStrategiesWithJsonResultAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + this.WriteAgentChatMessage(response); + + Console.WriteLine($"[IS COMPLETED: {chat.IsComplete}]"); } } } - private record struct InputScore(int score, string notes); + private record struct WritingScore(int score, string notes); private sealed class ThresholdTerminationStrategy : TerminationStrategy { @@ -80,7 +81,7 @@ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyLi { string lastMessageContent = history[history.Count - 1].Content ?? string.Empty; - InputScore? result = JsonResultTranslator.Translate(lastMessageContent); + WritingScore? result = JsonResultTranslator.Translate(lastMessageContent); return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs similarity index 65% rename from dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs rename to dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs index 21af5db70dce..a0d32f8cefba 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs @@ -3,23 +3,19 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; -using Resources; namespace GettingStarted; /// /// Demonstrate creation of an agent via dependency injection. /// -public class Step6_DependencyInjection(ITestOutputHelper output) : BaseTest(output) +public class Step06_DependencyInjection(ITestOutputHelper output) : BaseAgentsTest(output) { - private const int ScoreCompletionThreshold = 70; - private const string TutorName = "Tutor"; private const string TutorInstructions = """ - Think step-by-step and rate the user input on creativity and expressivness from 1-100. + Think step-by-step and rate the user input on creativity and expressiveness from 1-100. Respond in JSON format with the following JSON schema: @@ -80,50 +76,27 @@ public async Task UseDependencyInjectionToCreateAgentAsync() // Local function to invoke agent and display the conversation messages. async Task WriteAgentResponse(string input) { - Console.WriteLine($"# {AuthorRole.User}: {input}"); + ChatMessageContent message = new(AuthorRole.User, input); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agentClient.RunDemoAsync(input)) + await foreach (ChatMessageContent response in agentClient.RunDemoAsync(message)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } private sealed class AgentClient([FromKeyedServices(TutorName)] ChatCompletionAgent agent) { - private readonly AgentGroupChat _chat = - new() - { - ExecutionSettings = - new() - { - // Here a TerminationStrategy subclass is used that will terminate when - // the response includes a score that is greater than or equal to 70. - TerminationStrategy = new ThresholdTerminationStrategy() - } - }; - - public IAsyncEnumerable RunDemoAsync(string input) - { - // Create a chat for agent interaction. + private readonly AgentGroupChat _chat = new(); - this._chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + public IAsyncEnumerable RunDemoAsync(ChatMessageContent input) + { + this._chat.AddChatMessage(input); return this._chat.InvokeAsync(agent); } } - private record struct InputScore(int score, string notes); - - private sealed class ThresholdTerminationStrategy : TerminationStrategy - { - protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) - { - string lastMessageContent = history[history.Count - 1].Content ?? string.Empty; - - InputScore? result = JsonResultTranslator.Translate(lastMessageContent); - - return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold); - } - } + private record struct WritingScore(int score, string notes); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs similarity index 86% rename from dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs rename to dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs index 1ab559e668fb..3a48d407dea9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs @@ -8,13 +8,13 @@ namespace GettingStarted; /// -/// A repeat of with logging enabled via assignment +/// A repeat of with logging enabled via assignment /// of a to . /// /// /// Samples become super noisy with logging always enabled. /// -public class Step7_Logging(ITestOutputHelper output) : BaseTest(output) +public class Step07_Logging(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -81,16 +81,16 @@ public async Task UseLoggerFactoryWithAgentGroupChatAsync() }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs similarity index 57% rename from dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs rename to dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs index dda6ea31df81..bf3ddbac47f8 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs @@ -8,38 +8,36 @@ namespace GettingStarted; /// -/// This example demonstrates that outside of initialization (and cleanup), using -/// is no different from -/// even with with a . +/// This example demonstrates similarity between using +/// and (see: Step 2). /// -public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) +public class Step08_Assistant(ITestOutputHelper output) : BaseAgentsTest(output) { - protected override bool ForceOpenAI => false; - private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; [Fact] - public async Task UseSingleOpenAIAssistantAgentAsync() + public async Task UseSingleAssistantAgentAsync() { // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: GetOpenAIConfiguration(), + config: this.GetOpenAIConfiguration(), new() { Instructions = HostInstructions, Name = HostName, ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). KernelPlugin plugin = KernelPluginFactory.CreateFromType(); agent.Kernel.Plugins.Add(plugin); - // Create a thread for the agent interaction. - string threadId = await agent.CreateThreadAsync(); + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); // Respond to user input try @@ -58,45 +56,32 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - await agent.AddChatMessageAsync(threadId, new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agent.InvokeAsync(threadId)) + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) { - if (content.Role != AuthorRole.Tool) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } + this.WriteAgentChatMessage(response); } } } - private OpenAIServiceConfiguration GetOpenAIConfiguration() - => - this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); - private sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } + public string GetSpecials() => + """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """; [KernelFunction, Description("Provides the price of the requested menu item.")] public string GetItemPrice( [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } + string menuItem) => + "$9.99"; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs new file mode 100644 index 000000000000..c0d2d2151a3f --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Resources; + +namespace GettingStarted; + +/// +/// Demonstrate providing image input to . +/// +public class Step09_Assistant_Vision(ITestOutputHelper output) : BaseAgentsTest(output) +{ + /// + /// Azure currently only supports message of type=text. + /// + protected override bool ForceOpenAI => true; + + [Fact] + public async Task UseSingleAssistantAgentAsync() + { +// Define the agent + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config, + new() + { + ModelId = this.Model, + Metadata = AssistantSampleMetadata, + }); + + // Upload an image + await using Stream imageStream = EmbeddedResource.ReadStream("cat.jpg")!; + string fileId = await OpenAIAssistantAgent.UploadFileAsync(config, imageStream, "cat.jpg"); + + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); + + // Respond to user input + try + { + // Refer to public image by url + await InvokeAgentAsync(CreateMessageWithImageUrl("Describe this image.", "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/New_york_times_square-terabass.jpg/1200px-New_york_times_square-terabass.jpg")); + await InvokeAgentAsync(CreateMessageWithImageUrl("What are is the main color in this image?", "https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg")); + // Refer to uploaded image by file-id. + await InvokeAgentAsync(CreateMessageWithImageReference("Is there an animal in this image?", fileId)); + } + finally + { + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); + await config.CreateFileClient().DeleteFileAsync(fileId); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(ChatMessageContent message) + { + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + this.WriteAgentChatMessage(response); + } + } + } + + private ChatMessageContent CreateMessageWithImageUrl(string input, string url) + => new(AuthorRole.User, [new TextContent(input), new ImageContent(new Uri(url))]); + + private ChatMessageContent CreateMessageWithImageReference(string input, string fileId) + => new(AuthorRole.User, [new TextContent(input), new FileReferenceContent(fileId)]); +} diff --git a/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs new file mode 100644 index 000000000000..596bd690fcc1 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace GettingStarted; + +/// +/// Demonstrate using code-interpreter on . +/// +public class Step10_AssistantTool_CodeInterpreter(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseCodeInterpreterToolWithAssistantAgentAsync() + { + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: this.GetOpenAIConfiguration(), + new() + { + EnableCodeInterpreter = true, + ModelId = this.Model, + Metadata = AssistantSampleMetadata, + }); + + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); + + // Respond to user input + try + { + await InvokeAgentAsync("Use code to determine the values in the Fibonacci sequence that that are less then the value of 101?"); + } + finally + { + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + this.WriteAgentChatMessage(response); + } + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs new file mode 100644 index 000000000000..5769db1178ea --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; +using OpenAI.VectorStores; +using Resources; + +namespace GettingStarted; + +/// +/// Demonstrate using code-interpreter on . +/// +public class Step11_AssistantTool_FileSearch(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseFileSearchToolWithAssistantAgentAsync() + { + // Define the agent + OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + config: this.GetOpenAIConfiguration(), + new() + { + EnableFileSearch = true, + ModelId = this.Model, + Metadata = AssistantSampleMetadata, + }); + + // Upload file - Using a table of fictional employees. + FileClient fileClient = config.CreateFileClient(); + await using Stream stream = EmbeddedResource.ReadStream("employees.pdf")!; + OpenAIFileInfo fileInfo = await fileClient.UploadFileAsync(stream, "employees.pdf", FileUploadPurpose.Assistants); + + // Create a vector-store + VectorStoreClient vectorStoreClient = config.CreateVectorStoreClient(); + VectorStore vectorStore = + await vectorStoreClient.CreateVectorStoreAsync( + new VectorStoreCreationOptions() + { + FileIds = [fileInfo.Id], + Metadata = { { AssistantSampleMetadataKey, bool.TrueString } } + }); + + // Create a thread associated with a vector-store for the agent conversation. + string threadId = + await agent.CreateThreadAsync( + new OpenAIThreadCreationOptions + { + VectorStoreId = vectorStore.Id, + Metadata = AssistantSampleMetadata, + }); + + // Respond to user input + try + { + await InvokeAgentAsync("Who is the youngest employee?"); + await InvokeAgentAsync("Who works in sales?"); + await InvokeAgentAsync("I have a customer request, who can help me?"); + } + finally + { + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); + await vectorStoreClient.DeleteVectorStoreAsync(vectorStore); + await fileClient.DeleteFileAsync(fileInfo); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + this.WriteAgentChatMessage(response); + } + } + } +} diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs index 314d68ce8cd8..b971fe2ce8d4 100644 --- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs +++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs @@ -61,7 +61,7 @@ public static partial void LogAgentChatAddingMessages( [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "[{MethodName}] Adding Messages: {MessageCount}.")] + Message = "[{MethodName}] Added Messages: {MessageCount}.")] public static partial void LogAgentChatAddedMessages( this ILogger logger, string methodName, diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs index e7566a5db4f8..6874e1d21755 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs @@ -19,18 +19,18 @@ internal static class AssistantToolResourcesFactory /// An optionallist of file-identifiers for the 'code_interpreter' tool. public static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds) { - bool hasFileSearch = !string.IsNullOrWhiteSpace(vectorStoreId); + bool hasVectorStore = !string.IsNullOrWhiteSpace(vectorStoreId); bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0; ToolResources? toolResources = null; - if (hasFileSearch || hasCodeInterpreterFiles) + if (hasVectorStore || hasCodeInterpreterFiles) { toolResources = new ToolResources() { FileSearch = - hasFileSearch ? + hasVectorStore ? new FileSearchToolResources() { VectorStoreIds = [vectorStoreId!], diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 7d542a30ab80..fc775a4c4130 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text.Json; @@ -9,6 +10,7 @@ using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using OpenAI; using OpenAI.Assistants; +using OpenAI.Files; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -53,7 +55,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// Define a new . /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service. + /// Configuration for accessing the API service. /// The assistant definition. /// The to monitor for cancellation requests. The default is . /// An instance @@ -86,7 +88,7 @@ public static async Task CreateAsync( /// /// Retrieve a list of assistant definitions: . /// - /// Configuration for accessing the Assistants API service. + /// Configuration for accessing the API service. /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( @@ -107,7 +109,7 @@ public static async IAsyncEnumerable ListDefinitionsA /// Retrieve a by identifier. /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service. + /// Configuration for accessing the API service. /// The agent identifier /// The to monitor for cancellation requests. The default is . /// An instance @@ -164,6 +166,26 @@ public async Task DeleteThreadAsync( return await this._client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false); } + /// + /// Uploads an file for the purpose of using with assistant. + /// + /// Configuration for accessing the API service. + /// The content to upload + /// The name of the file + /// The to monitor for cancellation requests. The default is . + /// The file identifier + /// + /// Use the directly for more advanced file operations. + /// + public static async Task UploadFileAsync(OpenAIServiceConfiguration config, Stream stream, string name, CancellationToken cancellationToken = default) + { + FileClient client = config.CreateFileClient(); + + OpenAIFileInfo fileInfo = await client.UploadFileAsync(stream, name, FileUploadPurpose.Assistants, cancellationToken).ConfigureAwait(false); + + return fileInfo.Id; + } + /// /// Adds a message to the specified thread. /// @@ -303,7 +325,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod } IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds; - string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.Single(); + string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.SingleOrDefault(); bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject; return @@ -315,6 +337,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod Instructions = model.Instructions, CodeInterpreterFileIds = fileIds, EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), + EnableFileSearch = model.Tools.Any(t => t is FileSearchToolDefinition), Metadata = model.Metadata, ModelId = model.Model, EnableJsonResponse = enableJsonResponse, @@ -333,7 +356,10 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - ToolResources = AssistantToolResourcesFactory.GenerateToolResources(definition.VectorStoreId, definition.EnableCodeInterpreter ? definition.CodeInterpreterFileIds : null), + ToolResources = + AssistantToolResourcesFactory.GenerateToolResources( + definition.EnableFileSearch ? definition.VectorStoreId : null, + definition.EnableCodeInterpreter ? definition.CodeInterpreterFileIds : null), ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, Temperature = definition.Temperature, NucleusSamplingFactor = definition.TopP, @@ -358,7 +384,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss assistantCreationOptions.Tools.Add(ToolDefinition.CreateCodeInterpreter()); } - if (!string.IsNullOrWhiteSpace(definition.VectorStoreId)) + if (definition.EnableFileSearch) { assistantCreationOptions.Tools.Add(ToolDefinition.CreateFileSearch()); } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index cb8cb6c84734..b79101e98434 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -43,6 +43,11 @@ public sealed class OpenAIAssistantDefinition /// public bool EnableCodeInterpreter { get; init; } + /// + /// Set if file-search is enabled. + /// + public bool EnableFileSearch { get; init; } + /// /// Set if json response-format is enabled. /// @@ -71,7 +76,7 @@ public sealed class OpenAIAssistantDefinition public float? TopP { get; init; } /// - /// Enables file-search if specified. + /// Requires file-search if specified. /// public string? VectorStoreId { get; init; } diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs new file mode 100644 index 000000000000..34211fb97661 --- /dev/null +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs @@ -0,0 +1,118 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.ObjectModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +/// +/// Base class for samples that demonstrate the usage of agents. +/// +public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Metadata key to indicate the assistant as created for a sample. + /// + protected const string AssistantSampleMetadataKey = "sksample"; + + /// + /// Metadata to indicate the assistant as created for a sample. + /// + /// + /// While the samples do attempt delete the assistants it creates, it is possible + /// that some assistants may remain. This metadata can be used to identify and sample + /// agents for clean-up. + /// + protected static readonly ReadOnlyDictionary AssistantSampleMetadata = + new(new Dictionary + { + { AssistantSampleMetadataKey, bool.TrueString } + }); + + /// + /// Provide a according to the configuration settings. + /// + protected OpenAIServiceConfiguration GetOpenAIConfiguration() + => + this.UseOpenAIConfig ? + OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : + OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + + /// + /// %%% REMOVE ??? + /// + protected async Task WriteAgentResponseAsync(IAsyncEnumerable messages, ChatHistory? history = null) + { + await foreach (ChatMessageContent message in messages) + { + if (history != null && + !message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + { + history.Add(message); + } + + this.WriteAgentChatMessage(message); + } + } + + /// + /// Common method to write formatted agent chat content to the console. + /// + protected void WriteAgentChatMessage(ChatMessageContent message) + { + // Include ChatMessageContent.AuthorName in output, if present. + string authorExpression = message.Role == AuthorRole.User ? string.Empty : $" - {message.AuthorName ?? "*"}"; + // Include TextContent (via ChatMessageContent.Content), if present. + string contentExpression = string.IsNullOrWhiteSpace(message.Content) ? string.Empty : message.Content; + bool isCode = message.Metadata?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false; + string codeMarker = isCode ? "\n [CODE]\n" : " "; + Console.WriteLine($"\n# {message.Role}{authorExpression}:{codeMarker}{contentExpression}"); + + // Provide visibility for inner content (that isn't TextContent). + foreach (KernelContent item in message.Items) + { + if (item is AnnotationContent annotation) + { + Console.WriteLine($" [{item.GetType().Name}] {annotation.Quote}: File #{annotation.FileId}"); + //BinaryData fileContent = await fileClient.DownloadFileAsync(annotation.FileId!); // %%% COMMON + //Console.WriteLine($"\n{Encoding.Default.GetString(fileContent.ToArray())}"); + //Console.WriteLine($"\t[{item.GetType().Name}] {functionCall.Id}"); + } + if (item is FileReferenceContent fileReference) + { + Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}"); + //BinaryData fileContent = await fileClient.DownloadFileAsync(fileReference.FileId!); // %%% COMMON + //string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png"); + //await File.WriteAllBytesAsync($"{filePath}.png", fileContent.ToArray()); + //Console.WriteLine($"\t* Local path - {filePath}"); + } + if (item is ImageContent image) + { + Console.WriteLine($" [{item.GetType().Name}] {image.Uri?.ToString() ?? image.DataUri ?? $"{image.Data?.Length} bytes"}"); + } + else if (item is FunctionCallContent functionCall) + { + Console.WriteLine($" [{item.GetType().Name}] {functionCall.Id}"); + } + else if (item is FunctionResultContent functionResult) + { + Console.WriteLine($" [{item.GetType().Name}] {functionResult.CallId}"); + } + } + } + + //private async Task DownloadFileContentAsync(string fileId) + //{ + // string filePath = Path.Combine(Environment.CurrentDirectory, $"{fileId}.jpg"); + // BinaryData content = await fileClient.DownloadFileAsync(fileId); + // File.WriteAllBytes(filePath, content.ToArray()); + + // Process.Start( + // new ProcessStartInfo + // { + // FileName = "cmd.exe", + // Arguments = $"/C start {filePath}" + // }); + + // return filePath; + //} +} diff --git a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props index 0c47e16d8d93..df5205c40a82 100644 --- a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props +++ b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props @@ -1,5 +1,8 @@ - + + \ No newline at end of file From 9fd6b92165950c3b0ec16f4886ab90aed5626045 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 16:08:27 -0700 Subject: [PATCH 149/226] Unit-tests --- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 3 ++- .../OpenAI/OpenAIAssistantAgentTests.cs | 22 +++++++++++++++++-- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 3 +++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index fc775a4c4130..c55b0dd42b91 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.CompilerServices; @@ -343,7 +344,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod EnableJsonResponse = enableJsonResponse, TopP = model.NucleusSamplingFactor, Temperature = model.Temperature, - VectorStoreId = vectorStoreId, + VectorStoreId = string.IsNullOrWhiteSpace(vectorStoreId) ? null : vectorStoreId, ExecutionOptions = options, }; } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index de99478eea51..59073cdb1802 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -95,6 +95,23 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn await this.VerifyAgentCreationAsync(definition); } + /// + /// Verify the invocation and response of + /// for an agent with a file-search and no vector-store + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() + { + OpenAIAssistantDefinition definition = + new() + { + ModelId = "testmodel", + EnableFileSearch = true, + }; + + await this.VerifyAgentCreationAsync(definition); + } + /// /// Verify the invocation and response of /// for an agent with a vector-store-id (for file-search). @@ -106,6 +123,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() new() { ModelId = "testmodel", + EnableFileSearch = true, VectorStoreId = "#vs1", }; @@ -563,7 +581,7 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs Assert.Equal(hasCodeInterpreter, agent.Tools.OfType().Any()); bool hasFileSearch = false; - if (!string.IsNullOrWhiteSpace(sourceDefinition.VectorStoreId)) + if (sourceDefinition.EnableFileSearch) { hasFileSearch = true; ++expectedToolCount; @@ -667,7 +685,7 @@ public static string CreateAgentPayload(OpenAIAssistantDefinition definition) bool hasCodeInterpreter = definition.EnableCodeInterpreter; bool hasCodeInterpreterFiles = (definition.CodeInterpreterFileIds?.Count ?? 0) > 0; - bool hasFileSearch = !string.IsNullOrWhiteSpace(definition.VectorStoreId); + bool hasFileSearch = definition.EnableFileSearch; if (!hasCodeInterpreter && !hasFileSearch) { builder.AppendLine(@" ""tools"": [],"); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index a91a043febfb..08ff94ac0c5b 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -27,6 +27,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.ExecutionOptions); Assert.Null(definition.Temperature); Assert.Null(definition.TopP); + Assert.False(definition.EnableFileSearch); Assert.Null(definition.VectorStoreId); Assert.Null(definition.CodeInterpreterFileIds); Assert.False(definition.EnableCodeInterpreter); @@ -47,6 +48,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() ModelId = "testmodel", Instructions = "testinstructions", Description = "testdescription", + EnableFileSearch = true, VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, Temperature = 2, @@ -69,6 +71,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Equal("testmodel", definition.ModelId); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); + Assert.True(definition.EnableFileSearch); Assert.Equal("#vs", definition.VectorStoreId); Assert.Equal(2, definition.Temperature); Assert.Equal(0, definition.TopP); From 5b54de481c2de9f22bce75ce5391f254d7187574 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 17:04:01 -0700 Subject: [PATCH 150/226] Serialization tests and attributes --- .../Internal/AssistantRunOptionsFactory.cs | 1 - .../OpenAI/OpenAIAssistantDefinition.cs | 13 ++++++ .../OpenAIAssistantInvocationOptions.cs | 12 +++++ .../OpenAI/OpenAIThreadCreationOptions.cs | 5 ++ .../UnitTests/OpenAI/AssertCollection.cs | 46 +++++++++++++++++++ .../OpenAI/OpenAIAssistantDefinitionTests.cs | 32 +++++++++++++ .../OpenAIAssistantInvocationOptionsTests.cs | 24 ++++++++++ .../OpenAIThreadCreationOptionsTests.cs | 28 +++++++++-- 8 files changed, 155 insertions(+), 6 deletions(-) create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs index 03f0b5ca067a..981c646254af 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -31,7 +31,6 @@ public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition defin ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), - //ToolConstraint - Not Currently Supported (https://github.com/microsoft/semantic-kernel/issues/6795) TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, }; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index b79101e98434..f52d468c8d6f 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -16,6 +17,7 @@ public sealed class OpenAIAssistantDefinition /// /// The description of the assistant. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Description { get; init; } /// @@ -26,31 +28,37 @@ public sealed class OpenAIAssistantDefinition /// /// The system instructions for the assistant to use. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Instructions { get; init; } /// /// The name of the assistant. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; init; } /// /// Optional file-ids made available to the code_interpreter tool, if enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? CodeInterpreterFileIds { get; init; } /// /// Set if code-interpreter is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableCodeInterpreter { get; init; } /// /// Set if file-search is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableFileSearch { get; init; } /// /// Set if json response-format is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableJsonResponse { get; init; } /// @@ -58,11 +66,13 @@ public sealed class OpenAIAssistantDefinition /// storing additional information about that object in a structured format.Keys /// may be up to 64 characters in length and values may be up to 512 characters in length. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary? Metadata { get; init; } /// /// The sampling temperature to use, between 0 and 2. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public float? Temperature { get; init; } /// @@ -73,15 +83,18 @@ public sealed class OpenAIAssistantDefinition /// /// Recommended to set this or temperature but not both. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public float? TopP { get; init; } /// /// Requires file-search if specified. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? VectorStoreId { get; init; } /// /// Default execution options for each agent invocation. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public OpenAIAssistantExecutionOptions? ExecutionOptions { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs index 1aa0c3ffa745..0653c83a13e2 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -14,47 +15,56 @@ public sealed class OpenAIAssistantInvocationOptions /// /// Override the AI model targeted by the agent. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? ModelName { get; init; } /// /// Set if code_interpreter tool is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableCodeInterpreter { get; init; } /// /// Set if file_search tool is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableFileSearch { get; init; } /// /// Set if json response-format is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? EnableJsonResponse { get; init; } /// /// The maximum number of completion tokens that may be used over the course of the run. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxCompletionTokens { get; init; } /// /// The maximum number of prompt tokens that may be used over the course of the run. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? MaxPromptTokens { get; init; } /// /// Enables parallel function calling during tool use. Enabled by default. /// Use this property to disable. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public bool? ParallelToolCallsEnabled { get; init; } /// /// When set, the thread will be truncated to the N most recent messages in the thread. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? TruncationMessageCount { get; init; } /// /// The sampling temperature to use, between 0 and 2. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public float? Temperature { get; init; } /// @@ -65,6 +75,7 @@ public sealed class OpenAIAssistantInvocationOptions /// /// Recommended to set this or temperature but not both. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public float? TopP { get; init; } /// @@ -72,5 +83,6 @@ public sealed class OpenAIAssistantInvocationOptions /// storing additional information about that object in a structured format.Keys /// may be up to 64 characters in length and values may be up to 512 characters in length. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary? Metadata { get; init; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs index d2e8eb012e17..3f39c43d03dc 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -11,16 +12,19 @@ public sealed class OpenAIThreadCreationOptions /// /// Optional file-ids made available to the code_interpreter tool, if enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? CodeInterpreterFileIds { get; init; } /// /// Optional messages to initialize thread with.. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyList? Messages { get; init; } /// /// Enables file-search if specified. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? VectorStoreId { get; init; } /// @@ -28,5 +32,6 @@ public sealed class OpenAIThreadCreationOptions /// storing additional information about that object in a structured format.Keys /// may be up to 64 characters in length and values may be up to 512 characters in length. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary? Metadata { get; init; } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs new file mode 100644 index 000000000000..cd51c736ac18 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +internal static class AssertCollection +{ + public static void Equal(IReadOnlyList? source, IReadOnlyList? target, Func? adapter = null) + { + if (source == null) + { + Assert.Null(target); + return; + } + + Assert.NotNull(target); + Assert.Equal(source.Count, target.Count); + + adapter ??= (x) => x; + + for (int i = 0; i < source.Count; i++) + { + Assert.Equal(adapter(source[i]), adapter(target[i])); + } + } + + public static void Equal(IReadOnlyDictionary? source, IReadOnlyDictionary? target) + { + if (source == null) + { + Assert.Null(target); + return; + } + + Assert.NotNull(target); + Assert.Equal(source.Count, target.Count); + + foreach ((TKey key, TValue value) in source) + { + Assert.True(target.TryGetValue(key, out TValue? targetValue)); + Assert.Equal(value, targetValue); + } + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index 08ff94ac0c5b..fa8d903419a5 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json; using Microsoft.SemanticKernel.Agents.OpenAI; using Xunit; @@ -32,6 +33,8 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.Null(definition.CodeInterpreterFileIds); Assert.False(definition.EnableCodeInterpreter); Assert.False(definition.EnableJsonResponse); + + ValidateSerialization(definition); } /// @@ -84,5 +87,34 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.Single(definition.CodeInterpreterFileIds); Assert.True(definition.EnableCodeInterpreter); Assert.True(definition.EnableJsonResponse); + + ValidateSerialization(definition); + } + + private static void ValidateSerialization(OpenAIAssistantDefinition source) + { + string json = JsonSerializer.Serialize(source); + + OpenAIAssistantDefinition? target = JsonSerializer.Deserialize(json); + + Assert.NotNull(target); + Assert.Equal(source.Id, target.Id); + Assert.Equal(source.Name, target.Name); + Assert.Equal(source.ModelId, target.ModelId); + Assert.Equal(source.Instructions, target.Instructions); + Assert.Equal(source.Description, target.Description); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + Assert.Equal(source.Temperature, target.Temperature); + Assert.Equal(source.TopP, target.TopP); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter); + Assert.Equal(source.ExecutionOptions?.MaxCompletionTokens, target.ExecutionOptions?.MaxCompletionTokens); + Assert.Equal(source.ExecutionOptions?.MaxPromptTokens, target.ExecutionOptions?.MaxPromptTokens); + Assert.Equal(source.ExecutionOptions?.TruncationMessageCount, target.ExecutionOptions?.TruncationMessageCount); + Assert.Equal(source.ExecutionOptions?.ParallelToolCallsEnabled, target.ExecutionOptions?.ParallelToolCallsEnabled); + AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds); + AssertCollection.Equal(source.Metadata, target.Metadata); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs index 1d63a6e2e9c0..692dee85f1aa 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json; using Microsoft.SemanticKernel.Agents.OpenAI; using Xunit; @@ -29,6 +30,8 @@ public void OpenAIAssistantInvocationOptionsInitialState() Assert.Null(options.EnableJsonResponse); Assert.False(options.EnableCodeInterpreter); Assert.False(options.EnableFileSearch); + + ValidateSerialization(options); } /// @@ -64,5 +67,26 @@ public void OpenAIAssistantInvocationOptionsAssignment() Assert.True(options.EnableCodeInterpreter); Assert.True(options.EnableJsonResponse); Assert.True(options.EnableFileSearch); + + ValidateSerialization(options); + } + + private static void ValidateSerialization(OpenAIAssistantInvocationOptions source) + { + string json = JsonSerializer.Serialize(source); + + OpenAIAssistantInvocationOptions? target = JsonSerializer.Deserialize(json); + + Assert.NotNull(target); + Assert.Equal(source.ModelName, target.ModelName); + Assert.Equal(source.Temperature, target.Temperature); + Assert.Equal(source.TopP, target.TopP); + Assert.Equal(source.MaxCompletionTokens, target.MaxCompletionTokens); + Assert.Equal(source.MaxPromptTokens, target.MaxPromptTokens); + Assert.Equal(source.TruncationMessageCount, target.TruncationMessageCount); + Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter); + Assert.Equal(source.EnableJsonResponse, target.EnableJsonResponse); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + AssertCollection.Equal(source.Metadata, target.Metadata); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index d4e680efee09..496f429f0793 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; @@ -24,6 +25,8 @@ public void OpenAIThreadCreationOptionsInitialState() Assert.Null(options.Metadata); Assert.Null(options.VectorStoreId); Assert.Null(options.CodeInterpreterFileIds); + + ValidateSerialization(options); } /// @@ -32,7 +35,7 @@ public void OpenAIThreadCreationOptionsInitialState() [Fact] public void OpenAIThreadCreationOptionsAssignment() { - OpenAIThreadCreationOptions definition = + OpenAIThreadCreationOptions options = new() { Messages = [new ChatMessageContent(AuthorRole.User, "test")], @@ -41,9 +44,24 @@ public void OpenAIThreadCreationOptionsAssignment() CodeInterpreterFileIds = ["file1"], }; - Assert.Single(definition.Messages); - Assert.Single(definition.Metadata); - Assert.Equal("#vs", definition.VectorStoreId); - Assert.Single(definition.CodeInterpreterFileIds); + Assert.Single(options.Messages); + Assert.Single(options.Metadata); + Assert.Equal("#vs", options.VectorStoreId); + Assert.Single(options.CodeInterpreterFileIds); + + ValidateSerialization(options); + } + + private static void ValidateSerialization(OpenAIThreadCreationOptions source) + { + string json = JsonSerializer.Serialize(source); + + OpenAIThreadCreationOptions? target = JsonSerializer.Deserialize(json); + + Assert.NotNull(target); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds); + AssertCollection.Equal(source.Messages, target.Messages, m => m.Items.Count); // ChatMessageContent already validated for deep serialization + AssertCollection.Equal(source.Metadata, target.Metadata); } } From d029bd49e8820420567c496fd60b02ad32227aae Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 17:09:59 -0700 Subject: [PATCH 151/226] Image DataUrl support --- .../src/Agents/OpenAI/Internal/AssistantMessageFactory.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs index 8b65961e2677..4c31a1bcf291 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -52,11 +52,7 @@ public static IEnumerable GetMessageContents(ChatMessageContent } else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) { - //SDK BUG - BAD SIGNATURE (https://github.com/openai/openai-dotnet/issues/135) - // URI does not accept the format used for `DataUri` - // Approach is inefficient anyway... - //yield return MessageContent.FromImageUrl(new Uri(imageContent.DataUri!)); - throw new KernelException($"{nameof(ImageContent.DataUri)} not supported for assistant input."); + yield return MessageContent.FromImageUrl(new(imageContent.DataUri!)); } } else if (content is FileReferenceContent fileContent) From a7cdba519afdcefbc146b241bad515d069d0acc9 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 17:12:49 -0700 Subject: [PATCH 152/226] Remove duplicate sample --- .../Agents/OpenAIAssistant_FileSearch.cs | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs deleted file mode 100644 index c73934421a7c..000000000000 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileSearch.cs +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using OpenAI.Files; -using OpenAI.VectorStores; -using Resources; - -namespace Agents; - -/// -/// Demonstrate using retrieval on . -/// -public class OpenAIAssistant_FileSearch(ITestOutputHelper output) : BaseAgentsTest(output) -{ - /// - /// Retrieval tool not supported on Azure OpenAI. - /// - protected override bool ForceOpenAI => true; - - [Fact] - public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() - { - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); - - FileClient fileClient = config.CreateFileClient(); - - OpenAIFileInfo uploadFile = - await fileClient.UploadFileAsync( - new BinaryData(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!), - "travelinfo.txt", - FileUploadPurpose.Assistants); - - VectorStoreClient vectorStoreClient = config.CreateVectorStoreClient(); - VectorStoreCreationOptions vectorStoreOptions = - new() - { - FileIds = [uploadFile.Id] - }; - VectorStore vectorStore = await vectorStoreClient.CreateVectorStoreAsync(vectorStoreOptions); - - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config, - new() - { - VectorStoreId = vectorStore.Id, - ModelId = this.Model, - Metadata = AssistantSampleMetadata, - }); - - // Create a chat for agent interaction. - AgentGroupChat chat = new(); - - // Respond to user input - try - { - await InvokeAgentAsync("Where did sam go?"); - await InvokeAgentAsync("When does the flight leave Seattle?"); - await InvokeAgentAsync("What is the hotel contact info at the destination?"); - } - finally - { - await agent.DeleteAsync(); - await vectorStoreClient.DeleteVectorStoreAsync(vectorStore); - await fileClient.DeleteFileAsync(uploadFile); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - ChatMessageContent message = new(AuthorRole.User, input); - chat.AddChatMessage(new(AuthorRole.User, input)); - this.WriteAgentChatMessage(message); - - await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) - { - this.WriteAgentChatMessage(response); - } - } - } -} From 13f19867a17c6d0c12fa18c48c0e92df42cdad6d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 18:23:10 -0700 Subject: [PATCH 153/226] Update concept samples --- .../Agents/ChatCompletion_Streaming.cs | 4 +- .../Agents/ComplexChat_NestedShopper.cs | 7 +- .../Concepts/Agents/MixedChat_Files.cs | 10 +-- .../Concepts/Agents/MixedChat_Images.cs | 6 +- .../Agents/OpenAIAssistant_ChartMaker.cs | 6 +- .../OpenAIAssistant_FileManipulation.cs | 8 +- .../samples/AgentUtilities/BaseAgentsTest.cs | 89 +++++++++++-------- 7 files changed, 61 insertions(+), 69 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 7e74e425536c..071acc59a3f4 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -91,8 +91,8 @@ private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, { // Display full response and capture in chat history ChatMessageContent response = new(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }; - chat.Add(message); - this.WriteAgentChatMessage(message); + chat.Add(response); + this.WriteAgentChatMessage(response); } } diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index e12dee448370..0d7b27917d78 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; @@ -16,8 +15,6 @@ namespace Agents; /// public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseAgentsTest(output) { - protected override bool ForceOpenAI => true; - private const string InternalLeaderName = "InternalLeader"; private const string InternalLeaderInstructions = """ @@ -157,7 +154,7 @@ public async Task NestedChatWithAggregatorAgentAsync() await foreach (ChatMessageContent message in chat.GetChatMessagesAsync(personalShopperAgent).Reverse()) { - WriteAgentChatMessage(message); + this.WriteAgentChatMessage(message); } async Task InvokeChatAsync(string input) @@ -168,7 +165,7 @@ async Task InvokeChatAsync(string input) await foreach (ChatMessageContent response in chat.InvokeAsync(personalShopperAgent)) { - WriteAgentChatMessage(response); + this.WriteAgentChatMessage(response); } Console.WriteLine($"\n# IS COMPLETE: {chat.IsComplete}"); diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index f14ad8d1222d..2b80ff5cee6f 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -14,11 +14,6 @@ namespace Agents; /// public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - private const string SummaryInstructions = "Summarize the entire conversation for the user in natural language."; [Fact] @@ -43,8 +38,8 @@ await OpenAIAssistantAgent.CreateAsync( config, new() { - EnableCodeInterpreter = true, // Enable code-interpreter - CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant + EnableCodeInterpreter = true, + CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant code-interpreter ModelId = this.Model, Metadata = AssistantSampleMetadata, }); @@ -89,6 +84,7 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { this.WriteAgentChatMessage(response); + await this.DownloadResponseContentAsync(fileClient, response); } } } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index e1bf8b2b7068..25f94de4c11e 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -13,11 +13,6 @@ namespace Agents; /// public class MixedChat_Images(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - private const string AnalystName = "Analyst"; private const string AnalystInstructions = "Create charts as requested without explanation."; @@ -97,6 +92,7 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { this.WriteAgentChatMessage(response); + await this.DownloadResponseImageAsync(fileClient, response); } } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index af8990096a65..512fa5bbb0a2 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -13,11 +13,6 @@ namespace Agents; /// public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target Open AI services. - /// - protected override bool ForceOpenAI => true; - private const string AgentName = "ChartMaker"; private const string AgentInstructions = "Create charts as requested without explanation."; @@ -78,6 +73,7 @@ async Task InvokeAgentAsync(string input) await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { this.WriteAgentChatMessage(response); + await this.DownloadResponseImageAsync(fileClient, response); } } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index 0f92b31ffb04..f25e17600b99 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -13,11 +13,6 @@ namespace Agents; /// public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { @@ -38,8 +33,8 @@ await OpenAIAssistantAgent.CreateAsync( config, new() { + EnableCodeInterpreter = true, CodeInterpreterFileIds = [uploadFile.Id], - EnableCodeInterpreter = true, // Enable code-interpreter ModelId = this.Model, Metadata = AssistantSampleMetadata, }); @@ -70,6 +65,7 @@ async Task InvokeAgentAsync(string input) await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { this.WriteAgentChatMessage(response); + await this.DownloadResponseContentAsync(fileClient, response); } } } diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs index 34211fb97661..7bfbd3fd4df0 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.ObjectModel; +using System.Diagnostics; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; /// /// Base class for samples that demonstrate the usage of agents. @@ -37,23 +39,6 @@ protected OpenAIServiceConfiguration GetOpenAIConfiguration() OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); - /// - /// %%% REMOVE ??? - /// - protected async Task WriteAgentResponseAsync(IAsyncEnumerable messages, ChatHistory? history = null) - { - await foreach (ChatMessageContent message in messages) - { - if (history != null && - !message.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) - { - history.Add(message); - } - - this.WriteAgentChatMessage(message); - } - } - /// /// Common method to write formatted agent chat content to the console. /// @@ -73,19 +58,12 @@ protected void WriteAgentChatMessage(ChatMessageContent message) if (item is AnnotationContent annotation) { Console.WriteLine($" [{item.GetType().Name}] {annotation.Quote}: File #{annotation.FileId}"); - //BinaryData fileContent = await fileClient.DownloadFileAsync(annotation.FileId!); // %%% COMMON - //Console.WriteLine($"\n{Encoding.Default.GetString(fileContent.ToArray())}"); - //Console.WriteLine($"\t[{item.GetType().Name}] {functionCall.Id}"); } - if (item is FileReferenceContent fileReference) + else if (item is FileReferenceContent fileReference) { Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}"); - //BinaryData fileContent = await fileClient.DownloadFileAsync(fileReference.FileId!); // %%% COMMON - //string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png"); - //await File.WriteAllBytesAsync($"{filePath}.png", fileContent.ToArray()); - //Console.WriteLine($"\t* Local path - {filePath}"); } - if (item is ImageContent image) + else if (item is ImageContent image) { Console.WriteLine($" [{item.GetType().Name}] {image.Uri?.ToString() ?? image.DataUri ?? $"{image.Data?.Length} bytes"}"); } @@ -100,19 +78,52 @@ protected void WriteAgentChatMessage(ChatMessageContent message) } } - //private async Task DownloadFileContentAsync(string fileId) - //{ - // string filePath = Path.Combine(Environment.CurrentDirectory, $"{fileId}.jpg"); - // BinaryData content = await fileClient.DownloadFileAsync(fileId); - // File.WriteAllBytes(filePath, content.ToArray()); + protected async Task DownloadResponseContentAsync(FileClient client, ChatMessageContent message) + { + foreach (KernelContent item in message.Items) + { + if (item is AnnotationContent annotation) + { + await this.DownloadFileContentAsync(client, annotation.FileId!); + } + } + } - // Process.Start( - // new ProcessStartInfo - // { - // FileName = "cmd.exe", - // Arguments = $"/C start {filePath}" - // }); + protected async Task DownloadResponseImageAsync(FileClient client, ChatMessageContent message) + { + foreach (KernelContent item in message.Items) + { + if (item is FileReferenceContent fileReference) + { + await this.DownloadFileContentAsync(client, fileReference.FileId, launchViewer: true); + } + } + } + + private async Task DownloadFileContentAsync(FileClient client, string fileId, bool launchViewer = false) + { + OpenAIFileInfo fileInfo = client.GetFile(fileId); + if (fileInfo.Purpose == OpenAIFilePurpose.AssistantsOutput) + { + string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(fileInfo.Filename)); + if (launchViewer) + { + filePath = Path.ChangeExtension(filePath, ".png"); + } + + BinaryData content = await client.DownloadFileAsync(fileId); + File.WriteAllBytes(filePath, content.ToArray()); + Console.WriteLine($" File #{fileId} saved to: {filePath}"); - // return filePath; - //} + if (launchViewer) + { + Process.Start( + new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C start {filePath}" + }); + } + } + } } From 7dc67fd76da0780f809cf5fe0206c101086d3656 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 18:28:57 -0700 Subject: [PATCH 154/226] Whitespace --- .../samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs index c0d2d2151a3f..1ddd018c4c14 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs @@ -19,7 +19,7 @@ public class Step09_Assistant_Vision(ITestOutputHelper output) : BaseAgentsTest( [Fact] public async Task UseSingleAssistantAgentAsync() { -// Define the agent + // Define the agent OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( From e74aa194f11a00fc8a4a32d84ab630d5b7b0732c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 6 Aug 2024 18:32:29 -0700 Subject: [PATCH 155/226] Namespace --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index c55b0dd42b91..9c7802a2ef61 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; -using System.ComponentModel; using System.IO; using System.Linq; using System.Runtime.CompilerServices; From 84aece36957834e602873ebc3f6c4c56db5867cf Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:18:13 +0100 Subject: [PATCH 156/226] Fix Azure namespace --- .../Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs | 1 + .../Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs | 1 + .../samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs | 1 + 3 files changed, 3 insertions(+) diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs index db8e259f4e7a..a233fef17eef 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs @@ -3,6 +3,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using Memory.VectorStoreFixtures; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Redis; using Microsoft.SemanticKernel.Data; diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs index 18f0e5b476ca..9bb47b759cde 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs @@ -4,6 +4,7 @@ using Memory.VectorStoreFixtures; using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Connectors.Redis; diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs index 341e5c2bbda2..fe4edbeeca13 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs @@ -2,6 +2,7 @@ using System.Text.Json; using Memory.VectorStoreFixtures; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Data; From 77fefb9640776dde1eaf51f1bf21e40ffe26e367 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Wed, 7 Aug 2024 11:49:57 +0100 Subject: [PATCH 157/226] Fix namespace order --- .../Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs | 1 - .../Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs | 1 - .../samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs | 1 - 3 files changed, 3 deletions(-) diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs index a233fef17eef..cbfc5c1b0b24 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_CustomMapper.cs @@ -4,7 +4,6 @@ using System.Text.Json.Nodes; using Memory.VectorStoreFixtures; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Redis; using Microsoft.SemanticKernel.Data; using Microsoft.SemanticKernel.Embeddings; diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs index 9bb47b759cde..6aa4d84cebab 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_MultiStore.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Connectors.Redis; using Microsoft.SemanticKernel.Data; diff --git a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs index fe4edbeeca13..75013b8196ac 100644 --- a/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs +++ b/dotnet/samples/Concepts/Memory/VectorStore_DataIngestion_Simple.cs @@ -3,7 +3,6 @@ using System.Text.Json; using Memory.VectorStoreFixtures; using Microsoft.SemanticKernel.Connectors.AzureOpenAI; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Connectors.Qdrant; using Microsoft.SemanticKernel.Data; using Microsoft.SemanticKernel.Embeddings; From 41482ba31dbb2566d42f999c3b0c9b87fd47e850 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 09:23:22 -0700 Subject: [PATCH 158/226] "Breaking Glass" Checkpoint --- .../Concepts/Agents/MixedChat_Agents.cs | 2 +- .../Concepts/Agents/MixedChat_Files.cs | 6 +- .../Concepts/Agents/MixedChat_Images.cs | 6 +- .../Agents/OpenAIAssistant_ChartMaker.cs | 6 +- .../OpenAIAssistant_FileManipulation.cs | 6 +- .../Step08_Assistant.cs | 2 +- .../Step09_Assistant_Vision.cs | 8 +- .../Step10_AssistantTool_CodeInterpreter.cs | 2 +- .../Step11_AssistantTool_FileSearch.cs | 8 +- .../OpenAIServiceConfigurationExtensions.cs | 26 --- .../OpenAI/Internal/OpenAIClientFactory.cs | 106 ----------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 62 +++---- .../src/Agents/OpenAI/OpenAIClientProvider.cs | 172 ++++++++++++++++++ .../OpenAI/OpenAIServiceConfiguration.cs | 84 --------- ...enAIServiceConfigurationExtensionsTests.cs | 40 ---- .../Internal/OpenAIClientFactoryTests.cs | 87 --------- .../OpenAI/OpenAIAssistantAgentTests.cs | 6 +- .../OpenAI/OpenAIClientProviderTests.cs | 71 ++++++++ .../OpenAI/OpenAIServiceConfigurationTests.cs | 84 --------- .../Agents/OpenAIAssistantAgentTests.cs | 6 +- .../samples/AgentUtilities/BaseAgentsTest.cs | 8 +- 21 files changed, 301 insertions(+), 497 deletions(-) delete mode 100644 dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs delete mode 100644 dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs delete mode 100644 dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index 18ab8a673ca1..c387ff5704c2 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -47,7 +47,7 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync() OpenAIAssistantAgent agentWriter = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: this.GetOpenAIConfiguration(), + provider: this.GetClientProvider(), definition: new() { Instructions = CopyWriterInstructions, diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index 2b80ff5cee6f..982e41417c13 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -19,9 +19,9 @@ public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output) [Fact] public async Task AnalyzeFileAndGenerateReportAsync() { - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); - FileClient fileClient = config.CreateFileClient(); + FileClient fileClient = provider.Client.GetFileClient(); OpenAIFileInfo uploadFile = await fileClient.UploadFileAsync( @@ -35,7 +35,7 @@ await fileClient.UploadFileAsync( OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + provider, new() { EnableCodeInterpreter = true, diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 25f94de4c11e..4fae255c9b86 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -22,15 +22,15 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseAgentsTest(output) [Fact] public async Task AnalyzeDataAndGenerateChartAsync() { - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); - FileClient fileClient = config.CreateFileClient(); + FileClient fileClient = provider.Client.GetFileClient(); // Define the agents OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + provider, new() { Instructions = AnalystInstructions, diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 512fa5bbb0a2..08aee21c8707 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -19,15 +19,15 @@ public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseAgentsTe [Fact] public async Task GenerateChartWithOpenAIAssistantAgentAsync() { - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); - FileClient fileClient = config.CreateFileClient(); + FileClient fileClient = provider.Client.GetFileClient(); // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + provider, new() { Instructions = AgentInstructions, diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index f25e17600b99..bca6118041e4 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -16,9 +16,9 @@ public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseAg [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); - FileClient fileClient = config.CreateFileClient(); + FileClient fileClient = provider.Client.GetFileClient(); OpenAIFileInfo uploadFile = await fileClient.UploadFileAsync( @@ -30,7 +30,7 @@ await fileClient.UploadFileAsync( OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + provider, new() { EnableCodeInterpreter = true, diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs index bf3ddbac47f8..c937c7de9c70 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs @@ -23,7 +23,7 @@ public async Task UseSingleAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: this.GetOpenAIConfiguration(), + provider: this.GetClientProvider(), new() { Instructions = HostInstructions, diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs index 1ddd018c4c14..29a6e108df24 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs @@ -20,11 +20,11 @@ public class Step09_Assistant_Vision(ITestOutputHelper output) : BaseAgentsTest( public async Task UseSingleAssistantAgentAsync() { // Define the agent - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config, + provider, new() { ModelId = this.Model, @@ -33,7 +33,7 @@ await OpenAIAssistantAgent.CreateAsync( // Upload an image await using Stream imageStream = EmbeddedResource.ReadStream("cat.jpg")!; - string fileId = await OpenAIAssistantAgent.UploadFileAsync(config, imageStream, "cat.jpg"); + string fileId = await agent.UploadFileAsync(imageStream, "cat.jpg"); // Create a thread for the agent conversation. string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); @@ -51,7 +51,7 @@ await OpenAIAssistantAgent.CreateAsync( { await agent.DeleteThreadAsync(threadId); await agent.DeleteAsync(); - await config.CreateFileClient().DeleteFileAsync(fileId); + await provider.Client.GetFileClient().DeleteFileAsync(fileId); } // Local function to invoke agent and display the conversation messages. diff --git a/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs index 596bd690fcc1..d623c8a28b7b 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs @@ -17,7 +17,7 @@ public async Task UseCodeInterpreterToolWithAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: this.GetOpenAIConfiguration(), + provider: this.GetClientProvider(), new() { EnableCodeInterpreter = true, diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs index 5769db1178ea..bfcd93dd3ecb 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -17,11 +17,11 @@ public class Step11_AssistantTool_FileSearch(ITestOutputHelper output) : BaseAge public async Task UseFileSearchToolWithAssistantAgentAsync() { // Define the agent - OpenAIServiceConfiguration config = this.GetOpenAIConfiguration(); + OpenAIClientProvider provider = this.GetClientProvider(); OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: this.GetOpenAIConfiguration(), + provider: this.GetClientProvider(), new() { EnableFileSearch = true, @@ -30,12 +30,12 @@ await OpenAIAssistantAgent.CreateAsync( }); // Upload file - Using a table of fictional employees. - FileClient fileClient = config.CreateFileClient(); + FileClient fileClient = provider.Client.GetFileClient(); await using Stream stream = EmbeddedResource.ReadStream("employees.pdf")!; OpenAIFileInfo fileInfo = await fileClient.UploadFileAsync(stream, "employees.pdf", FileUploadPurpose.Assistants); // Create a vector-store - VectorStoreClient vectorStoreClient = config.CreateVectorStoreClient(); + VectorStoreClient vectorStoreClient = provider.Client.GetVectorStoreClient(); VectorStore vectorStore = await vectorStoreClient.CreateVectorStoreAsync( new VectorStoreCreationOptions() diff --git a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs deleted file mode 100644 index 9b3a55b9e6fe..000000000000 --- a/dotnet/src/Agents/OpenAI/Extensions/OpenAIServiceConfigurationExtensions.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel.Agents.OpenAI.Internal; -using OpenAI.Files; -using OpenAI.VectorStores; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Extension method for creating OpenAI clients from a . -/// -public static class OpenAIServiceConfigurationExtensions -{ - /// - /// Provide a newly created based on the specified configuration. - /// - /// The configuration - public static FileClient CreateFileClient(this OpenAIServiceConfiguration configuration) - => OpenAIClientFactory.CreateClient(configuration).GetFileClient(); - - /// - /// Provide a newly created based on the specified configuration. - /// - /// The configuration - public static VectorStoreClient CreateVectorStoreClient(this OpenAIServiceConfiguration configuration) - => OpenAIClientFactory.CreateClient(configuration).GetVectorStoreClient(); -} diff --git a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs b/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs deleted file mode 100644 index 97c3cc978f68..000000000000 --- a/dotnet/src/Agents/OpenAI/Internal/OpenAIClientFactory.cs +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System.ClientModel.Primitives; -using System.Net.Http; -using System.Threading; -using Azure.AI.OpenAI; -using Microsoft.SemanticKernel.Http; -using OpenAI; - -namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; - -/// -/// Factory for creating . -/// -internal static class OpenAIClientFactory -{ - /// - /// Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key. - /// - private const string SingleSpaceKey = " "; - - /// - /// Creates an OpenAI client based on the provided configuration. - /// - /// Configuration required to target a specific Open AI service - /// An initialized Open AI client - public static OpenAIClient CreateClient(OpenAIServiceConfiguration config) - { - // Inspect options - switch (config.Type) - { - case OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI: - { - AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(config); - - if (config.Credential is not null) - { - return new AzureOpenAIClient(config.Endpoint, config.Credential, clientOptions); - } - - if (!string.IsNullOrEmpty(config.ApiKey)) - { - return new AzureOpenAIClient(config.Endpoint, config.ApiKey!, clientOptions); - } - - throw new KernelException($"Unsupported configuration state: {config.Type}. No api-key or credential present."); - } - case OpenAIServiceConfiguration.OpenAIServiceType.OpenAI: - { - OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(config); - return new OpenAIClient(config.ApiKey ?? SingleSpaceKey, clientOptions); - } - default: - throw new KernelException($"Unsupported configuration type: {config.Type}"); - } - } - - private static AzureOpenAIClientOptions CreateAzureClientOptions(OpenAIServiceConfiguration config) - { - AzureOpenAIClientOptions options = - new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = config.Endpoint, - }; - - ConfigureClientOptions(config.HttpClient, options); - - return options; - } - - private static OpenAIClientOptions CreateOpenAIClientOptions(OpenAIServiceConfiguration config) - { - OpenAIClientOptions options = - new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = config.Endpoint ?? config.HttpClient?.BaseAddress, - }; - - ConfigureClientOptions(config.HttpClient, options); - - return options; - } - - private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options) - { - options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); - - if (httpClient is not null) - { - options.Transport = new HttpClientPipelineTransport(httpClient); - options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. - options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout - } - } - - private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) - => - new((message) => - { - if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) - { - message.Request.Headers.Set(headerName, headerValue); - } - }); -} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 9c7802a2ef61..b13afaecf901 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -26,6 +26,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent internal const string OptionsMetadataKey = "__run_options"; + private readonly OpenAIClientProvider _provider; private readonly Assistant _assistant; private readonly AssistantClient _client; private readonly string[] _channelKeys; @@ -55,23 +56,23 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// Define a new . /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the API service. + /// Configuration for accessing the API service. /// The assistant definition. /// The to monitor for cancellation requests. The default is . /// An instance public static async Task CreateAsync( Kernel kernel, - OpenAIServiceConfiguration config, + OpenAIClientProvider provider, OpenAIAssistantDefinition definition, CancellationToken cancellationToken = default) { // Validate input Verify.NotNull(kernel, nameof(kernel)); - Verify.NotNull(config, nameof(config)); + Verify.NotNull(provider, nameof(provider)); Verify.NotNull(definition, nameof(definition)); // Create the client - AssistantClient client = CreateClient(config); + AssistantClient client = CreateClient(provider); // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); @@ -79,7 +80,7 @@ public static async Task CreateAsync( // Instantiate the agent return - new OpenAIAssistantAgent(client, model, DefineChannelKeys(config)) + new OpenAIAssistantAgent(model, provider, client) { Kernel = kernel, }; @@ -88,15 +89,15 @@ public static async Task CreateAsync( /// /// Retrieve a list of assistant definitions: . /// - /// Configuration for accessing the API service. + /// Configuration for accessing the API service. /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( - OpenAIServiceConfiguration config, + OpenAIClientProvider provider, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create the client - AssistantClient client = CreateClient(config); + AssistantClient client = CreateClient(provider); // Query and enumerate assistant definitions await foreach (Assistant model in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) @@ -109,25 +110,25 @@ public static async IAsyncEnumerable ListDefinitionsA /// Retrieve a by identifier. /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the API service. + /// Configuration for accessing the API service. /// The agent identifier /// The to monitor for cancellation requests. The default is . /// An instance public static async Task RetrieveAsync( Kernel kernel, - OpenAIServiceConfiguration config, + OpenAIClientProvider provider, string id, CancellationToken cancellationToken = default) { // Create the client - AssistantClient client = CreateClient(config); + AssistantClient client = CreateClient(provider); // Retrieve the assistant Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) // Instantiate the agent return - new OpenAIAssistantAgent(client, model, DefineChannelKeys(config)) + new OpenAIAssistantAgent(model, provider, client) { Kernel = kernel, }; @@ -169,7 +170,6 @@ public async Task DeleteThreadAsync( /// /// Uploads an file for the purpose of using with assistant. /// - /// Configuration for accessing the API service. /// The content to upload /// The name of the file /// The to monitor for cancellation requests. The default is . @@ -177,9 +177,9 @@ public async Task DeleteThreadAsync( /// /// Use the directly for more advanced file operations. /// - public static async Task UploadFileAsync(OpenAIServiceConfiguration config, Stream stream, string name, CancellationToken cancellationToken = default) + public async Task UploadFileAsync(Stream stream, string name, CancellationToken cancellationToken = default) { - FileClient client = config.CreateFileClient(); + FileClient client = this._provider.Client.GetFileClient(); OpenAIFileInfo fileInfo = await client.UploadFileAsync(stream, name, FileUploadPurpose.Assistants, cancellationToken).ConfigureAwait(false); @@ -299,13 +299,14 @@ internal void ThrowIfDeleted() /// Initializes a new instance of the class. /// private OpenAIAssistantAgent( - AssistantClient client, Assistant model, - IEnumerable channelKeys) + OpenAIClientProvider provider, + AssistantClient client) { + this._provider = provider; this._assistant = model; - this._client = client; - this._channelKeys = channelKeys.ToArray(); + this._client = provider.Client.GetAssistantClient(); + this._channelKeys = provider.ConfigurationKeys.ToArray(); this.Definition = CreateAssistantDefinition(model); @@ -392,32 +393,19 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss return assistantCreationOptions; } - private static AssistantClient CreateClient(OpenAIServiceConfiguration config) + private static AssistantClient CreateClient(OpenAIClientProvider config) { - OpenAIClient openAIClient = OpenAIClientFactory.CreateClient(config); - return openAIClient.GetAssistantClient(); + return config.Client.GetAssistantClient(); } - private static IEnumerable DefineChannelKeys(OpenAIServiceConfiguration config) + private static IEnumerable DefineChannelKeys(OpenAIClientProvider config) { // Distinguish from other channel types. yield return typeof(AgentChannel).FullName!; - // Distinguish between different Azure OpenAI endpoints or OpenAI services. - yield return config.Endpoint != null ? config.Endpoint.ToString() : "openai"; - - // Custom client receives dedicated channel. - if (config.HttpClient is not null) + foreach (string key in config.ConfigurationKeys) { - if (config.HttpClient.BaseAddress is not null) - { - yield return config.HttpClient.BaseAddress.AbsoluteUri; - } - - foreach (string header in config.HttpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) - { - yield return header; - } + yield return key; } } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs new file mode 100644 index 000000000000..2a9c18dfb283 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.SemanticKernel.Http; +using OpenAI; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// %%% +/// +public sealed class OpenAIClientProvider +{ + /// + /// Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key. + /// + private const string SingleSpaceKey = " "; + + /// + /// %%% + /// + public OpenAIClient Client { get; } + + internal IEnumerable ConfigurationKeys { get; } + + private OpenAIClientProvider(OpenAIClient client, IEnumerable keys) + { + this.Client = client; + this.ConfigurationKeys = keys; + } + + /// + /// Produce a based on . + /// + /// The API key + /// The service endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForAzureOpenAI(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + Verify.NotNull(endpoint, nameof(endpoint)); + + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + + return new(new AzureOpenAIClient(endpoint, apiKey!, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// The credentials + /// The service endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNull(credential, nameof(credential)); + Verify.NotNull(endpoint, nameof(endpoint)); + + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + + return new(new AzureOpenAIClient(endpoint, credential, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// An optional endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForOpenAI(Uri? endpoint = null, HttpClient? httpClient = null) + { + OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient); + return new(new OpenAIClient(SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// The API key + /// An optional endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForOpenAI(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null) + { + OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient); + return new(new OpenAIClient(apiKey ?? SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// %%% + /// + public static OpenAIClientProvider FromClient(OpenAIClient client) + { + return new(client, [client.GetType().FullName!, client.GetHashCode().ToString()]); + } + + private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, HttpClient? httpClient) + { + AzureOpenAIClientOptions options = + new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint, + }; + + ConfigureClientOptions(httpClient, options); + + return options; + } + + private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient) + { + OpenAIClientOptions options = + new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint ?? httpClient?.BaseAddress, + }; + + ConfigureClientOptions(httpClient, options); + + return options; + } + + private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options) + { + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout + } + } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + => + new((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + + private static IEnumerable CreateConfigurationKeys(Uri? endpoint, HttpClient? httpClient) + { + if (endpoint != null) + { + yield return endpoint.ToString(); + } + + if (httpClient is not null) + { + if (httpClient.BaseAddress is not null) + { + yield return httpClient.BaseAddress.AbsoluteUri; + } + + foreach (string header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) + { + yield return header; + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs deleted file mode 100644 index 1bc6431e5487..000000000000 --- a/dotnet/src/Agents/OpenAI/OpenAIServiceConfiguration.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Azure.Core; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Configuration to target a specific Open AI service. -/// -public sealed class OpenAIServiceConfiguration -{ - internal enum OpenAIServiceType - { - AzureOpenAI, - OpenAI, - } - - /// - /// Produce a that targets an Azure OpenAI endpoint using an API key. - /// - /// The API key - /// The service endpoint - /// Custom for HTTP requests. - public static OpenAIServiceConfiguration ForAzureOpenAI(string apiKey, Uri endpoint, HttpClient? httpClient = null) - { - Verify.NotNullOrWhiteSpace(apiKey, nameof(apiKey)); - Verify.NotNull(endpoint, nameof(endpoint)); - - return - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIServiceType.AzureOpenAI, - }; - } - - /// - /// Produce a that targets an Azure OpenAI endpoint using an token credentials. - /// - /// The credentials - /// The service endpoint - /// Custom for HTTP requests. - public static OpenAIServiceConfiguration ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) - { - Verify.NotNull(credential, nameof(credential)); - Verify.NotNull(endpoint, nameof(endpoint)); - - return - new() - { - Credential = credential, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIServiceType.AzureOpenAI, - }; - } - - /// - /// Produce a that targets OpenAI services using an API key. - /// - /// The API key - /// An optional endpoint - /// Custom for HTTP requests. - public static OpenAIServiceConfiguration ForOpenAI(string? apiKey, Uri? endpoint = null, HttpClient? httpClient = null) - { - return - new() - { - ApiKey = apiKey, - Endpoint = endpoint, - HttpClient = httpClient, - Type = OpenAIServiceType.OpenAI, - }; - } - - internal string? ApiKey { get; init; } - internal TokenCredential? Credential { get; init; } - internal Uri? Endpoint { get; init; } - internal HttpClient? HttpClient { get; init; } - internal OpenAIServiceType Type { get; init; } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs deleted file mode 100644 index 650503a94e5e..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/OpenAIServiceConfigurationExtensionsTests.cs +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using Microsoft.SemanticKernel.Agents.OpenAI; -using OpenAI.Files; -using OpenAI.VectorStores; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions; - -/// -/// Unit testing of . -/// -public class OpenAIServiceConfigurationExtensionsTests -{ - /// - /// Verify can produce a - /// - [Fact] - public void OpenAIServiceConfigurationExtensionsCreateFileClientTest() - { - OpenAIServiceConfiguration configOpenAI = OpenAIServiceConfiguration.ForOpenAI("key", new Uri("https://localhost")); - Assert.IsType(configOpenAI.CreateFileClient()); - - OpenAIServiceConfiguration configAzure = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); - Assert.IsType(configOpenAI.CreateFileClient()); - } - - /// - /// Verify can produce a - /// - [Fact] - public void OpenAIServiceConfigurationExtensionsCreateVectorStoreTest() - { - OpenAIServiceConfiguration configOpenAI = OpenAIServiceConfiguration.ForOpenAI("key", new Uri("https://localhost")); - Assert.IsType(configOpenAI.CreateVectorStoreClient()); - - OpenAIServiceConfiguration configAzure = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); - Assert.IsType(configOpenAI.CreateVectorStoreClient()); - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs deleted file mode 100644 index df2387c917e3..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/OpenAIClientFactoryTests.cs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Azure.Core; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.Agents.OpenAI.Internal; -using Moq; -using OpenAI; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; - -/// -/// Unit testing of . -/// -public class OpenAIClientFactoryTests -{ - /// - /// Verify that the factory can create a client for Azure OpenAI. - /// - [Fact] - public void VerifyOpenAIClientFactoryTargetAzureByKey() - { - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI("key", new Uri("https://localhost")); - OpenAIClient client = OpenAIClientFactory.CreateClient(config); - Assert.NotNull(client); - } - - /// - /// Verify that the factory can create a client for Azure OpenAI. - /// - [Fact] - public void VerifyOpenAIClientFactoryTargetAzureByCredential() - { - Mock mockCredential = new(); - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost")); - OpenAIClient client = OpenAIClientFactory.CreateClient(config); - Assert.NotNull(client); - } - - /// - /// Verify that the factory throws exception for null credential. - /// - [Fact] - public void VerifyOpenAIClientFactoryTargetAzureNullCredential() - { - OpenAIServiceConfiguration config = new() { Type = OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI }; - Assert.Throws(() => OpenAIClientFactory.CreateClient(config)); - } - - /// - /// Verify that the factory throws exception for null credential. - /// - [Fact] - public void VerifyOpenAIClientFactoryTargetUnknownTypes() - { - OpenAIServiceConfiguration config = new() { Type = (OpenAIServiceConfiguration.OpenAIServiceType)99 }; - Assert.Throws(() => OpenAIClientFactory.CreateClient(config)); - } - - /// - /// Verify that the factory can create a client for various OpenAI service configurations. - /// - [Theory] - [InlineData(null, null)] - [InlineData("key", null)] - [InlineData("key", "http://myproxy:9819")] - public void VerifyOpenAIClientFactoryTargetOpenAI(string? key, string? endpoint) - { - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null); - OpenAIClient client = OpenAIClientFactory.CreateClient(config); - Assert.NotNull(client); - } - - /// - /// Verify that the factory can create a client with http proxy. - /// - [Fact] - public void VerifyOpenAIClientFactoryWithHttpClient() - { - using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") }; - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: null, httpClient: httpClient); - OpenAIClient client = OpenAIClientFactory.CreateClient(config); - Assert.NotNull(client); - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 59073cdb1802..a1a1959e9cff 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -634,10 +634,10 @@ private Task CreateAgentAsync() definition); } - private OpenAIServiceConfiguration CreateTestConfiguration(bool targetAzure = false) + private OpenAIClientProvider CreateTestConfiguration(bool targetAzure = false) => targetAzure ? - OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : - OpenAIServiceConfiguration.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); + OpenAIClientProvider.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIClientProvider.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); private void SetupResponse(HttpStatusCode statusCode, string content) { diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs new file mode 100644 index 000000000000..dfb033f31d3c --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using Azure.Core; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIClientProviderTests +{ + /// + /// Verify that provisioning of client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByKey() + { + OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI("key", new Uri("https://localhost")); + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByCredential() + { + Mock mockCredential = new(); + OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost")); + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for OpenAI. + /// + [Theory] + [InlineData(null)] + [InlineData("http://myproxy:9819")] + public void VerifyOpenAIClientFactoryTargetOpenAINoKey(string? endpoint) + { + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(endpoint != null ? new Uri(endpoint) : null); + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for OpenAI. + /// + [Theory] + [InlineData("key", null)] + [InlineData("key", "http://myproxy:9819")] + public void VerifyOpenAIClientFactoryTargetOpenAIByKey(string key, string? endpoint) + { + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null); + Assert.NotNull(provider.Client); + } + + /// + /// Verify that the factory can create a client with http proxy. + /// + [Fact] + public void VerifyOpenAIClientFactoryWithHttpClient() + { + using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") }; + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(httpClient: httpClient); + Assert.NotNull(provider.Client); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs deleted file mode 100644 index dce5d5c9ceaf..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIServiceConfigurationTests.cs +++ /dev/null @@ -1,84 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Azure.Core; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Moq; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI; - -/// -/// Unit testing of . -/// -public class OpenAIServiceConfigurationTests -{ - /// - /// Verify Open AI service configuration. - /// - [Fact] - public void VerifyOpenAIAssistantConfiguration() - { - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey"); - - Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.OpenAI, config.Type); - Assert.Equal("testkey", config.ApiKey); - Assert.Null(config.Credential); - Assert.Null(config.Endpoint); - Assert.Null(config.HttpClient); - } - - /// - /// Verify Open AI service configuration with endpoint. - /// - [Fact] - public void VerifyOpenAIAssistantProxyConfiguration() - { - using HttpClient client = new(); - - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost"), client); - - Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.OpenAI, config.Type); - Assert.Equal("testkey", config.ApiKey); - Assert.Null(config.Credential); - Assert.NotNull(config.Endpoint); - Assert.Equal("https://localhost/", config.Endpoint.ToString()); - Assert.NotNull(config.HttpClient); - } - - /// - /// Verify Azure Open AI service configuration with API key. - /// - [Fact] - public void VerifyAzureOpenAIAssistantApiKeyConfiguration() - { - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(apiKey: "testkey", endpoint: new Uri("https://localhost")); - - Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI, config.Type); - Assert.Equal("testkey", config.ApiKey); - Assert.Null(config.Credential); - Assert.NotNull(config.Endpoint); - Assert.Equal("https://localhost/", config.Endpoint.ToString()); - Assert.Null(config.HttpClient); - } - - /// - /// Verify Azure Open AI service configuration with API key. - /// - [Fact] - public void VerifyAzureOpenAIAssistantCredentialConfiguration() - { - using HttpClient client = new(); - - Mock credential = new(); - - OpenAIServiceConfiguration config = OpenAIServiceConfiguration.ForAzureOpenAI(credential.Object, endpoint: new Uri("https://localhost"), client); - - Assert.Equal(OpenAIServiceConfiguration.OpenAIServiceType.AzureOpenAI, config.Type); - Assert.Null(config.ApiKey); - Assert.NotNull(config.Credential); - Assert.NotNull(config.Endpoint); - Assert.Equal("https://localhost/", config.Endpoint.ToString()); - Assert.NotNull(config.HttpClient); - } -} diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index f7dee91db903..a4458f6cb470 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -36,7 +36,7 @@ public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAns Assert.NotNull(openAISettings); await this.ExecuteAgentAsync( - OpenAIServiceConfiguration.ForOpenAI(openAISettings.ApiKey), + OpenAIClientProvider.ForOpenAI(openAISettings.ApiKey), openAISettings.ModelId, input, expectedAnswerContains); @@ -54,14 +54,14 @@ public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAn Assert.NotNull(azureOpenAIConfiguration); await this.ExecuteAgentAsync( - OpenAIServiceConfiguration.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), + OpenAIClientProvider.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), azureOpenAIConfiguration.ChatDeploymentName!, input, expectedAnswerContains); } private async Task ExecuteAgentAsync( - OpenAIServiceConfiguration config, + OpenAIClientProvider config, string modelName, string input, string expected) diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs index 7bfbd3fd4df0..e86c1b77f4c1 100644 --- a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs @@ -31,13 +31,13 @@ public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output }); /// - /// Provide a according to the configuration settings. + /// Provide a according to the configuration settings. /// - protected OpenAIServiceConfiguration GetOpenAIConfiguration() + protected OpenAIClientProvider GetClientProvider() => this.UseOpenAIConfig ? - OpenAIServiceConfiguration.ForOpenAI(this.ApiKey) : - OpenAIServiceConfiguration.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + OpenAIClientProvider.ForOpenAI(this.ApiKey) : + OpenAIClientProvider.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); /// /// Common method to write formatted agent chat content to the console. From a372a679d654a70ad0a7030185b84ad9e79bc69f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 09:25:38 -0700 Subject: [PATCH 159/226] Comments --- dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs index 2a9c18dfb283..8c4f4e477d64 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -14,7 +14,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// %%% +/// Provides an for use by . /// public sealed class OpenAIClientProvider { @@ -24,7 +24,7 @@ public sealed class OpenAIClientProvider private const string SingleSpaceKey = " "; /// - /// %%% + /// An active client instance. /// public OpenAIClient Client { get; } @@ -92,7 +92,7 @@ public static OpenAIClientProvider ForOpenAI(ApiKeyCredential apiKey, Uri? endpo } /// - /// %%% + /// Directly provide a client instance. /// public static OpenAIClientProvider FromClient(OpenAIClient client) { From bdaa4b61141f24f5660da0f4f3ac004a4cdfc97a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 09:36:21 -0700 Subject: [PATCH 160/226] One more comment --- dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs index 8c4f4e477d64..8cc879f2d0b4 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -28,6 +28,9 @@ public sealed class OpenAIClientProvider /// public OpenAIClient Client { get; } + /// + /// Configuration keys required for management. + /// internal IEnumerable ConfigurationKeys { get; } private OpenAIClientProvider(OpenAIClient client, IEnumerable keys) From 2b138e500b0939b129d08a8fbf3864a11c87e820 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 12:20:16 -0700 Subject: [PATCH 161/226] Include agent-base-test in solution --- dotnet/SK-dotnet.sln | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index f9a0d8f29d28..228dd14c7abd 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -277,7 +277,16 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithAgents", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{77E141BA-AF5E-4C01-A970-6C07AC3CD55A}" ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\ConfigurationNotFoundException.cs + src\InternalUtilities\samples\EnumerableExtensions.cs = src\InternalUtilities\samples\EnumerableExtensions.cs + src\InternalUtilities\samples\Env.cs = src\InternalUtilities\samples\Env.cs + src\InternalUtilities\samples\ObjectExtensions.cs = src\InternalUtilities\samples\ObjectExtensions.cs + src\InternalUtilities\samples\PlanExtensions.cs = src\InternalUtilities\samples\PlanExtensions.cs + src\InternalUtilities\samples\RepoFiles.cs = src\InternalUtilities\samples\RepoFiles.cs src\InternalUtilities\samples\SamplesInternalUtilities.props = src\InternalUtilities\samples\SamplesInternalUtilities.props + src\InternalUtilities\samples\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\TextOutputHelperExtensions.cs + src\InternalUtilities\samples\XunitLogger.cs = src\InternalUtilities\samples\XunitLogger.cs + src\InternalUtilities\samples\YourAppException.cs = src\InternalUtilities\samples\YourAppException.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Prompty", "src\Functions\Functions.Prompty\Functions.Prompty.csproj", "{12B06019-740B-466D-A9E0-F05BC123A47D}" @@ -344,20 +353,6 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{EE454832-085F-4D37-B19B-F94F7FC6984A}" - ProjectSection(SolutionItems) = preProject - src\InternalUtilities\samples\InternalUtilities\BaseTest.cs = src\InternalUtilities\samples\InternalUtilities\BaseTest.cs - src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs - src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs = src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs - src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs = src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs - src\InternalUtilities\samples\InternalUtilities\Env.cs = src\InternalUtilities\samples\InternalUtilities\Env.cs - src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs = src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs - src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs = src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs - src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs = src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs - src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs = src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs - src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs - src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs = src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs - src\InternalUtilities\samples\InternalUtilities\YourAppException.cs = src\InternalUtilities\samples\InternalUtilities\YourAppException.cs - EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30E0-7AC1-47F4-8244-57B066B43FD8}" ProjectSection(SolutionItems) = preProject @@ -987,7 +982,7 @@ Global {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} - {38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {38374C62-0263-4FE8-A18C-70FC8132912B} = {00000000-0000-0000-0000-000000000000} {E06818E3-00A5-41AC-97ED-9491070CDEA1} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} From 6855dbba077be31c0d390bc36e30be2372a75933 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 12:21:25 -0700 Subject: [PATCH 162/226] Re-link samples/internalutilities to .sln --- dotnet/SK-dotnet.sln | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 228dd14c7abd..791ff8e35185 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -277,16 +277,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithAgents", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{77E141BA-AF5E-4C01-A970-6C07AC3CD55A}" ProjectSection(SolutionItems) = preProject - src\InternalUtilities\samples\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\ConfigurationNotFoundException.cs - src\InternalUtilities\samples\EnumerableExtensions.cs = src\InternalUtilities\samples\EnumerableExtensions.cs - src\InternalUtilities\samples\Env.cs = src\InternalUtilities\samples\Env.cs - src\InternalUtilities\samples\ObjectExtensions.cs = src\InternalUtilities\samples\ObjectExtensions.cs - src\InternalUtilities\samples\PlanExtensions.cs = src\InternalUtilities\samples\PlanExtensions.cs - src\InternalUtilities\samples\RepoFiles.cs = src\InternalUtilities\samples\RepoFiles.cs src\InternalUtilities\samples\SamplesInternalUtilities.props = src\InternalUtilities\samples\SamplesInternalUtilities.props - src\InternalUtilities\samples\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\TextOutputHelperExtensions.cs - src\InternalUtilities\samples\XunitLogger.cs = src\InternalUtilities\samples\XunitLogger.cs - src\InternalUtilities\samples\YourAppException.cs = src\InternalUtilities\samples\YourAppException.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Prompty", "src\Functions\Functions.Prompty\Functions.Prompty.csproj", "{12B06019-740B-466D-A9E0-F05BC123A47D}" @@ -353,6 +344,20 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{EE454832-085F-4D37-B19B-F94F7FC6984A}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\InternalUtilities\BaseTest.cs = src\InternalUtilities\samples\InternalUtilities\BaseTest.cs + src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs + src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs = src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs + src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs = src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs + src\InternalUtilities\samples\InternalUtilities\Env.cs = src\InternalUtilities\samples\InternalUtilities\Env.cs + src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs = src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs + src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs = src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs + src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs = src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs + src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs = src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs + src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs + src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs = src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs + src\InternalUtilities\samples\InternalUtilities\YourAppException.cs = src\InternalUtilities\samples\InternalUtilities\YourAppException.cs + EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30E0-7AC1-47F4-8244-57B066B43FD8}" ProjectSection(SolutionItems) = preProject From f4d2113ad9a98d9985894f36256784f3568dde9a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 7 Aug 2024 12:28:16 -0700 Subject: [PATCH 163/226] Re-add demo project to sln (merge weirdness?) --- dotnet/SK-dotnet.sln | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 791ff8e35185..b4580b4d1146 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -331,9 +331,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CreateChatGptPlugin", "CreateChatGptPlugin", "{F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098}" EndProject @@ -364,6 +362,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30 src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs = src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs EndProjectSection EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -851,10 +851,6 @@ Global {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.Build.0 = Debug|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.Build.0 = Release|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.Build.0 = Release|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.ActiveCfg = Debug|Any CPU @@ -873,6 +869,12 @@ Global {4326A974-F027-4ABD-A220-382CC6BB0801}.Publish|Any CPU.Build.0 = Debug|Any CPU {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.Build.0 = Release|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.Build.0 = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -987,7 +989,6 @@ Global {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} - {38374C62-0263-4FE8-A18C-70FC8132912B} = {00000000-0000-0000-0000-000000000000} {E06818E3-00A5-41AC-97ED-9491070CDEA1} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} @@ -995,6 +996,7 @@ Global {4326A974-F027-4ABD-A220-382CC6BB0801} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} {EE454832-085F-4D37-B19B-F94F7FC6984A} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} {5C6C30E0-7AC1-47F4-8244-57B066B43FD8} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} From e8ace92e352aebe1566d93af428f8d7482cca23a Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:10:39 +0100 Subject: [PATCH 164/226] .Net: Allow chat history mutation from auto-function invocation filters (#7952) ### Motivation and Context Today, {Azure} OpenAI connectors don't allow auto-function invocation filters to update/mutate chat history. The mutation can be useful to reduce the number of tokens sent to LLM by removing no longer needed function-calling messages from the chat history. Partially closes: https://github.com/microsoft/semantic-kernel/issues/7590. The MistralAI connector will be updated in a separate PR. ### Description This PR updates the `ClientCore` class to generate a list of messages to send to the LLM based on the latest chat history for every function-calling loop iteration, rather than separately updating the list of LLM messages and chat history. ### Contribution Checklist - [x] The code builds clean without any errors or warnings - [x] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [x] All unit tests pass, and I have added new tests where possible - [x] I didn't break anyone :smile: --- .../Services/PlanProvider.cs | 8 +- .../MultipleHttpMessageHandlerStub.cs | 53 ----- .../AzureOpenAIChatCompletionServiceTests.cs | 164 ++++++++++++++++ .../OpenAIChatCompletionServiceTests.cs | 181 +++++++++++++++++- .../Core/ClientCore.ChatCompletion.cs | 87 ++------- 5 files changed, 364 insertions(+), 129 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs diff --git a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs index a61251f9eb49..ed5bd4f03fe1 100644 --- a/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs +++ b/dotnet/samples/Demos/StepwisePlannerMigration/Services/PlanProvider.cs @@ -2,10 +2,13 @@ using System.IO; using System.Text.Json; -using Microsoft.SemanticKernel.ChatCompletion; #pragma warning disable IDE0005 // Using directive is unnecessary +using Microsoft.SemanticKernel.ChatCompletion; + +#pragma warning restore IDE0005 // Using directive is unnecessary + namespace StepwisePlannerMigration.Services; /// @@ -19,6 +22,3 @@ public ChatHistory GetPlan(string fileName) return JsonSerializer.Deserialize(plan)!; } } - -#pragma warning restore IDE0005 // Using directive is unnecessary - diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs deleted file mode 100644 index 0af66de6a519..000000000000 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/MultipleHttpMessageHandlerStub.cs +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; - -namespace SemanticKernel.Connectors.AzureOpenAI; - -internal sealed class MultipleHttpMessageHandlerStub : DelegatingHandler -{ - private int _callIteration = 0; - - public List RequestHeaders { get; private set; } - - public List ContentHeaders { get; private set; } - - public List RequestContents { get; private set; } - - public List RequestUris { get; private set; } - - public List Methods { get; private set; } - - public List ResponsesToReturn { get; set; } - - public MultipleHttpMessageHandlerStub() - { - this.RequestHeaders = []; - this.ContentHeaders = []; - this.RequestContents = []; - this.RequestUris = []; - this.Methods = []; - this.ResponsesToReturn = []; - } - - protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - this._callIteration++; - - this.Methods.Add(request.Method); - this.RequestUris.Add(request.RequestUri); - this.RequestHeaders.Add(request.Headers); - this.ContentHeaders.Add(request.Content?.Headers); - - var content = request.Content is null ? null : await request.Content.ReadAsByteArrayAsync(cancellationToken); - - this.RequestContents.Add(content); - - return await Task.FromResult(this.ResponsesToReturn[this._callIteration - 1]); - } -} diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs index 2e639434e951..435caa3c425a 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI.UnitTests/Services/AzureOpenAIChatCompletionServiceTests.cs @@ -900,6 +900,150 @@ public async Task FunctionResultsCanBeProvidedToLLMAsManyResultsInOneChatMessage Assert.Equal("2", assistantMessage2.GetProperty("tool_call_id").GetString()); } + [Fact] + public async Task GetChatMessageContentShouldSendMutatedChatHistoryToLLM() + { + // Arrange + static void MutateChatHistory(AutoFunctionInvocationContext context, Func next) + { + // Remove the function call messages from the chat history to reduce token count. + context.ChatHistory.RemoveRange(1, 2); // Remove the `Date` function call and function result messages. + + next(context); + } + + var kernel = new Kernel(); + kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "rainy", "GetCurrentWeather")]); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(MutateChatHistory)); + + using var firstResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_single_function_call_test_response.json")) }; + this._messageHandlerStub.ResponsesToReturn.Add(firstResponse); + + using var secondResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_test_response.json")) }; + this._messageHandlerStub.ResponsesToReturn.Add(secondResponse); + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.User, "What time is it?"), + new ChatMessageContent(AuthorRole.Assistant, [ + new FunctionCallContent("Date", "TimePlugin", "2") + ]), + new ChatMessageContent(AuthorRole.Tool, [ + new FunctionResultContent("Date", "TimePlugin", "2", "rainy") + ]), + new ChatMessageContent(AuthorRole.Assistant, "08/06/2024 00:00:00"), + new ChatMessageContent(AuthorRole.User, "Given the current time of day and weather, what is the likely color of the sky in Boston?") + }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[1]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(5, messages.GetArrayLength()); + + var userFirstPrompt = messages[0]; + Assert.Equal("user", userFirstPrompt.GetProperty("role").GetString()); + Assert.Equal("What time is it?", userFirstPrompt.GetProperty("content").ToString()); + + var assistantFirstResponse = messages[1]; + Assert.Equal("assistant", assistantFirstResponse.GetProperty("role").GetString()); + Assert.Equal("08/06/2024 00:00:00", assistantFirstResponse.GetProperty("content").GetString()); + + var userSecondPrompt = messages[2]; + Assert.Equal("user", userSecondPrompt.GetProperty("role").GetString()); + Assert.Equal("Given the current time of day and weather, what is the likely color of the sky in Boston?", userSecondPrompt.GetProperty("content").ToString()); + + var assistantSecondResponse = messages[3]; + Assert.Equal("assistant", assistantSecondResponse.GetProperty("role").GetString()); + Assert.Equal("1", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("id").GetString()); + Assert.Equal("MyPlugin-GetCurrentWeather", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("function").GetProperty("name").GetString()); + + var functionResult = messages[4]; + Assert.Equal("tool", functionResult.GetProperty("role").GetString()); + Assert.Equal("rainy", functionResult.GetProperty("content").GetString()); + } + + [Fact] + public async Task GetStreamingChatMessageContentsShouldSendMutatedChatHistoryToLLM() + { + // Arrange + static void MutateChatHistory(AutoFunctionInvocationContext context, Func next) + { + // Remove the function call messages from the chat history to reduce token count. + context.ChatHistory.RemoveRange(1, 2); // Remove the `Date` function call and function result messages. + + next(context); + } + + var kernel = new Kernel(); + kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "rainy", "GetCurrentWeather")]); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(MutateChatHistory)); + + using var firstResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_single_function_call_test_response.txt")) }; + this._messageHandlerStub.ResponsesToReturn.Add(firstResponse); + + using var secondResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) }; + this._messageHandlerStub.ResponsesToReturn.Add(secondResponse); + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.User, "What time is it?"), + new ChatMessageContent(AuthorRole.Assistant, [ + new FunctionCallContent("Date", "TimePlugin", "2") + ]), + new ChatMessageContent(AuthorRole.Tool, [ + new FunctionResultContent("Date", "TimePlugin", "2", "rainy") + ]), + new ChatMessageContent(AuthorRole.Assistant, "08/06/2024 00:00:00"), + new ChatMessageContent(AuthorRole.User, "Given the current time of day and weather, what is the likely color of the sky in Boston?") + }; + + // Act + await foreach (var update in sut.GetStreamingChatMessageContentsAsync(chatHistory, new AzureOpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + } + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContents[1]!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(5, messages.GetArrayLength()); + + var userFirstPrompt = messages[0]; + Assert.Equal("user", userFirstPrompt.GetProperty("role").GetString()); + Assert.Equal("What time is it?", userFirstPrompt.GetProperty("content").ToString()); + + var assistantFirstResponse = messages[1]; + Assert.Equal("assistant", assistantFirstResponse.GetProperty("role").GetString()); + Assert.Equal("08/06/2024 00:00:00", assistantFirstResponse.GetProperty("content").GetString()); + + var userSecondPrompt = messages[2]; + Assert.Equal("user", userSecondPrompt.GetProperty("role").GetString()); + Assert.Equal("Given the current time of day and weather, what is the likely color of the sky in Boston?", userSecondPrompt.GetProperty("content").ToString()); + + var assistantSecondResponse = messages[3]; + Assert.Equal("assistant", assistantSecondResponse.GetProperty("role").GetString()); + Assert.Equal("1", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("id").GetString()); + Assert.Equal("MyPlugin-GetCurrentWeather", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("function").GetProperty("name").GetString()); + + var functionResult = messages[4]; + Assert.Equal("tool", functionResult.GetProperty("role").GetString()); + Assert.Equal("rainy", functionResult.GetProperty("content").GetString()); + } + public void Dispose() { this._httpClient.Dispose(); @@ -917,4 +1061,24 @@ public void Dispose() { "json_object", "json_object" }, { "text", "text" } }; + + private sealed class AutoFunctionInvocationFilter : IAutoFunctionInvocationFilter + { + private readonly Func, Task> _callback; + + public AutoFunctionInvocationFilter(Func, Task> callback) + { + this._callback = callback; + } + + public AutoFunctionInvocationFilter(Action> callback) + { + this._callback = (c, n) => { callback(c, n); return Task.CompletedTask; }; + } + + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + await this._callback(context, next); + } + } } diff --git a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs index 1a0145d137f2..ccda12afe6a6 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI.UnitTests/Services/OpenAIChatCompletionServiceTests.cs @@ -892,6 +892,150 @@ public async Task GetInvalidResponseThrowsExceptionAndIsCapturedByDiagnosticsAsy Assert.True(startedChatCompletionsActivity); } + [Fact] + public async Task GetChatMessageContentShouldSendMutatedChatHistoryToLLM() + { + // Arrange + static void MutateChatHistory(AutoFunctionInvocationContext context, Func next) + { + // Remove the function call messages from the chat history to reduce token count. + context.ChatHistory.RemoveRange(1, 2); // Remove the `Date` function call and function result messages. + + next(context); + } + + var kernel = new Kernel(); + kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "rainy", "GetCurrentWeather")]); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(MutateChatHistory)); + + using var firstResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_single_function_call_test_response.json")) }; + this._messageHandlerStub.ResponseQueue.Enqueue(firstResponse); + + using var secondResponse = new HttpResponseMessage(System.Net.HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_test_response.json")) }; + this._messageHandlerStub.ResponseQueue.Enqueue(secondResponse); + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.User, "What time is it?"), + new ChatMessageContent(AuthorRole.Assistant, [ + new FunctionCallContent("Date", "TimePlugin", "2") + ]), + new ChatMessageContent(AuthorRole.Tool, [ + new FunctionResultContent("Date", "TimePlugin", "2", "rainy") + ]), + new ChatMessageContent(AuthorRole.Assistant, "08/06/2024 00:00:00"), + new ChatMessageContent(AuthorRole.User, "Given the current time of day and weather, what is the likely color of the sky in Boston?") + }; + + // Act + await sut.GetChatMessageContentAsync(chatHistory, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel); + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(5, messages.GetArrayLength()); + + var userFirstPrompt = messages[0]; + Assert.Equal("user", userFirstPrompt.GetProperty("role").GetString()); + Assert.Equal("What time is it?", userFirstPrompt.GetProperty("content").ToString()); + + var assistantFirstResponse = messages[1]; + Assert.Equal("assistant", assistantFirstResponse.GetProperty("role").GetString()); + Assert.Equal("08/06/2024 00:00:00", assistantFirstResponse.GetProperty("content").GetString()); + + var userSecondPrompt = messages[2]; + Assert.Equal("user", userSecondPrompt.GetProperty("role").GetString()); + Assert.Equal("Given the current time of day and weather, what is the likely color of the sky in Boston?", userSecondPrompt.GetProperty("content").ToString()); + + var assistantSecondResponse = messages[3]; + Assert.Equal("assistant", assistantSecondResponse.GetProperty("role").GetString()); + Assert.Equal("1", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("id").GetString()); + Assert.Equal("MyPlugin-GetCurrentWeather", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("function").GetProperty("name").GetString()); + + var functionResult = messages[4]; + Assert.Equal("tool", functionResult.GetProperty("role").GetString()); + Assert.Equal("rainy", functionResult.GetProperty("content").GetString()); + } + + [Fact] + public async Task GetStreamingChatMessageContentsShouldSendMutatedChatHistoryToLLM() + { + // Arrange + static void MutateChatHistory(AutoFunctionInvocationContext context, Func next) + { + // Remove the function call messages from the chat history to reduce token count. + context.ChatHistory.RemoveRange(1, 2); // Remove the `Date` function call and function result messages. + + next(context); + } + + var kernel = new Kernel(); + kernel.ImportPluginFromFunctions("MyPlugin", [KernelFunctionFactory.CreateFromMethod(() => "rainy", "GetCurrentWeather")]); + kernel.AutoFunctionInvocationFilters.Add(new AutoFunctionInvocationFilter(MutateChatHistory)); + + using var firstResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_single_function_call_test_response.txt")) }; + this._messageHandlerStub.ResponseQueue.Enqueue(firstResponse); + + using var secondResponse = new HttpResponseMessage(HttpStatusCode.OK) { Content = new StreamContent(File.OpenRead("TestData/chat_completion_streaming_test_response.txt")) }; + this._messageHandlerStub.ResponseQueue.Enqueue(secondResponse); + + var sut = new OpenAIChatCompletionService(modelId: "gpt-3.5-turbo", apiKey: "NOKEY", httpClient: this._httpClient); + + var chatHistory = new ChatHistory + { + new ChatMessageContent(AuthorRole.User, "What time is it?"), + new ChatMessageContent(AuthorRole.Assistant, [ + new FunctionCallContent("Date", "TimePlugin", "2") + ]), + new ChatMessageContent(AuthorRole.Tool, [ + new FunctionResultContent("Date", "TimePlugin", "2", "rainy") + ]), + new ChatMessageContent(AuthorRole.Assistant, "08/06/2024 00:00:00"), + new ChatMessageContent(AuthorRole.User, "Given the current time of day and weather, what is the likely color of the sky in Boston?") + }; + + // Act + await foreach (var update in sut.GetStreamingChatMessageContentsAsync(chatHistory, new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }, kernel)) + { + } + + // Assert + var actualRequestContent = Encoding.UTF8.GetString(this._messageHandlerStub.RequestContent!); + Assert.NotNull(actualRequestContent); + + var optionsJson = JsonSerializer.Deserialize(actualRequestContent); + + var messages = optionsJson.GetProperty("messages"); + Assert.Equal(5, messages.GetArrayLength()); + + var userFirstPrompt = messages[0]; + Assert.Equal("user", userFirstPrompt.GetProperty("role").GetString()); + Assert.Equal("What time is it?", userFirstPrompt.GetProperty("content").ToString()); + + var assistantFirstResponse = messages[1]; + Assert.Equal("assistant", assistantFirstResponse.GetProperty("role").GetString()); + Assert.Equal("08/06/2024 00:00:00", assistantFirstResponse.GetProperty("content").GetString()); + + var userSecondPrompt = messages[2]; + Assert.Equal("user", userSecondPrompt.GetProperty("role").GetString()); + Assert.Equal("Given the current time of day and weather, what is the likely color of the sky in Boston?", userSecondPrompt.GetProperty("content").ToString()); + + var assistantSecondResponse = messages[3]; + Assert.Equal("assistant", assistantSecondResponse.GetProperty("role").GetString()); + Assert.Equal("1", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("id").GetString()); + Assert.Equal("MyPlugin-GetCurrentWeather", assistantSecondResponse.GetProperty("tool_calls")[0].GetProperty("function").GetProperty("name").GetString()); + + var functionResult = messages[4]; + Assert.Equal("tool", functionResult.GetProperty("role").GetString()); + Assert.Equal("rainy", functionResult.GetProperty("content").GetString()); + } + public void Dispose() { this._httpClient.Dispose(); @@ -899,6 +1043,28 @@ public void Dispose() this._multiMessageHandlerStub.Dispose(); } + private sealed class AutoFunctionInvocationFilter : IAutoFunctionInvocationFilter + { + private readonly Func, Task> _callback; + + public AutoFunctionInvocationFilter(Func, Task> callback) + { + Verify.NotNull(callback, nameof(callback)); + this._callback = callback; + } + + public AutoFunctionInvocationFilter(Action> callback) + { + Verify.NotNull(callback, nameof(callback)); + this._callback = (c, n) => { callback(c, n); return Task.CompletedTask; }; + } + + public async Task OnAutoFunctionInvocationAsync(AutoFunctionInvocationContext context, Func next) + { + await this._callback(context, next); + } + } + private const string ChatCompletionResponse = """ { "id": "chatcmpl-8IlRBQU929ym1EqAY2J4T7GGkW5Om", @@ -911,12 +1077,17 @@ public void Dispose() "message": { "role": "assistant", "content": null, - "function_call": { - "name": "TimePlugin_Date", - "arguments": "{}" - } + "tool_calls":[{ + "id": "1", + "type": "function", + "function": { + "name": "TimePlugin-Date", + "arguments": "{}" + } + } + ] }, - "finish_reason": "stop" + "finish_reason": "tool_calls" } ], "usage": { diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs index 5ad712255af5..1177fb7ec846 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.ChatCompletion.cs @@ -143,10 +143,10 @@ internal async Task> GetChatMessageContentsAsy ValidateMaxTokens(chatExecutionSettings.MaxTokens); - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - for (int requestIndex = 0; ; requestIndex++) { + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); @@ -207,11 +207,8 @@ internal async Task> GetChatMessageContentsAsy this.Logger.LogTrace("Function call requests: {Requests}", string.Join(", ", chatCompletion.ToolCalls.OfType().Select(ftc => $"{ftc.FunctionName}({ftc.FunctionArguments})"))); } - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. Also add the result message to the caller's chat - // history: if they don't want it, they can remove it, but this makes the data available, - // including metadata like usage. - chatForRequest.Add(CreateRequestMessage(chatCompletion)); + // Add the result message to the caller's chat history; + // this is required for the service to understand the tool call responses. chat.Add(chatMessageContent); // We must send back a response for every tool call, regardless of whether we successfully executed it or not. @@ -223,7 +220,7 @@ internal async Task> GetChatMessageContentsAsy // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (functionToolCall.Kind != ChatToolCallKind.Function) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Tool call was not a function call.", functionToolCall, this.Logger); continue; } @@ -235,7 +232,7 @@ internal async Task> GetChatMessageContentsAsy } catch (JsonException) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Function call arguments were invalid JSON.", functionToolCall, this.Logger); continue; } @@ -245,14 +242,14 @@ internal async Task> GetChatMessageContentsAsy if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Function call request for a function that wasn't defined.", functionToolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Requested function could not be found.", functionToolCall, this.Logger); continue; } @@ -287,7 +284,7 @@ internal async Task> GetChatMessageContentsAsy catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatForRequest, chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); + AddResponseMessage(chat, null, $"Error: Exception while invoking function. {e.Message}", functionToolCall, this.Logger); continue; } finally @@ -301,7 +298,7 @@ internal async Task> GetChatMessageContentsAsy object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, functionToolCall, this.Logger); + AddResponseMessage(chat, stringResult, errorMessage: null, functionToolCall, this.Logger); // If filter requested termination, returning latest function result. if (invocationContext.Terminate) @@ -342,10 +339,10 @@ internal async IAsyncEnumerable GetStreamingC Dictionary? functionNamesByIndex = null; Dictionary? functionArgumentBuildersByIndex = null; - var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); - for (int requestIndex = 0; ; requestIndex++) { + var chatForRequest = CreateChatCompletionMessages(chatExecutionSettings, chat); + var toolCallingConfig = this.GetToolCallingConfiguration(kernel, chatExecutionSettings, requestIndex); var chatOptions = this.CreateChatCompletionOptions(chatExecutionSettings, chat, toolCallingConfig, kernel); @@ -478,9 +475,7 @@ internal async IAsyncEnumerable GetStreamingC this.Logger.LogDebug("Function call requests: {Requests}", toolCalls.Length); } - // Add the original assistant message to the chat messages; this is required for the service - // to understand the tool call responses. - chatForRequest.Add(CreateRequestMessage(streamedRole ?? default, content, streamedName, toolCalls)); + // Add the result message to the caller's chat history; this is required for the service to understand the tool call responses. var chatMessageContent = this.CreateChatMessageContent(streamedRole ?? default, content, toolCalls, functionCallContents, metadata, streamedName); chat.Add(chatMessageContent); @@ -492,7 +487,7 @@ internal async IAsyncEnumerable GetStreamingC // We currently only know about function tool calls. If it's anything else, we'll respond with an error. if (string.IsNullOrEmpty(toolCall.FunctionName)) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Tool call was not a function call.", toolCall, this.Logger); continue; } @@ -504,7 +499,7 @@ internal async IAsyncEnumerable GetStreamingC } catch (JsonException) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Function call arguments were invalid JSON.", toolCall, this.Logger); continue; } @@ -514,14 +509,14 @@ internal async IAsyncEnumerable GetStreamingC if (chatExecutionSettings.ToolCallBehavior?.AllowAnyRequestedKernelFunction is not true && !IsRequestableTool(chatOptions, openAIFunctionToolCall)) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Function call request for a function that wasn't defined.", toolCall, this.Logger); continue; } // Find the function in the kernel and populate the arguments. if (!kernel!.Plugins.TryGetFunctionAndArguments(openAIFunctionToolCall, out KernelFunction? function, out KernelArguments? functionArgs)) { - AddResponseMessage(chatForRequest, chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); + AddResponseMessage(chat, result: null, "Error: Requested function could not be found.", toolCall, this.Logger); continue; } @@ -556,7 +551,7 @@ internal async IAsyncEnumerable GetStreamingC catch (Exception e) #pragma warning restore CA1031 // Do not catch general exception types { - AddResponseMessage(chatForRequest, chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); + AddResponseMessage(chat, result: null, $"Error: Exception while invoking function. {e.Message}", toolCall, this.Logger); continue; } finally @@ -570,7 +565,7 @@ internal async IAsyncEnumerable GetStreamingC object functionResultValue = functionResult.GetValue() ?? string.Empty; var stringResult = ProcessFunctionResult(functionResultValue, chatExecutionSettings.ToolCallBehavior); - AddResponseMessage(chatForRequest, chat, stringResult, errorMessage: null, toolCall, this.Logger); + AddResponseMessage(chat, stringResult, errorMessage: null, toolCall, this.Logger); // If filter requested termination, returning latest function result and breaking request iteration loop. if (invocationContext.Terminate) @@ -785,26 +780,6 @@ private static List CreateChatCompletionMessages(OpenAIPromptExecut return messages; } - private static ChatMessage CreateRequestMessage(ChatMessageRole chatRole, string content, string? name, ChatToolCall[]? tools) - { - if (chatRole == ChatMessageRole.User) - { - return new UserChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.System) - { - return new SystemChatMessage(content) { ParticipantName = name }; - } - - if (chatRole == ChatMessageRole.Assistant) - { - return new AssistantChatMessage(tools, content) { ParticipantName = name }; - } - - throw new NotImplementedException($"Role {chatRole} is not implemented"); - } - private static List CreateRequestMessages(ChatMessageContent message, ToolCallBehavior? toolCallBehavior) { if (message.Role == AuthorRole.System) @@ -955,26 +930,6 @@ private static ChatMessageContentPart GetImageContentItem(ImageContent imageCont throw new ArgumentException($"{nameof(ImageContent)} must have either Data or a Uri."); } - private static ChatMessage CreateRequestMessage(OpenAIChatCompletion completion) - { - if (completion.Role == ChatMessageRole.System) - { - return ChatMessage.CreateSystemMessage(completion.Content[0].Text); - } - - if (completion.Role == ChatMessageRole.Assistant) - { - return ChatMessage.CreateAssistantMessage(completion); - } - - if (completion.Role == ChatMessageRole.User) - { - return ChatMessage.CreateUserMessage(completion.Content); - } - - throw new NotSupportedException($"Role {completion.Role} is not supported."); - } - private OpenAIChatMessageContent CreateChatMessageContent(OpenAIChatCompletion completion, string targetModel) { var message = new OpenAIChatMessageContent(completion, targetModel, this.GetChatCompletionMetadata(completion)); @@ -1053,7 +1008,7 @@ private List GetFunctionCallContents(IEnumerable chatMessages, ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) + private static void AddResponseMessage(ChatHistory chat, string? result, string? errorMessage, ChatToolCall toolCall, ILogger logger) { // Log any error if (errorMessage is not null && logger.IsEnabled(LogLevel.Debug)) @@ -1062,9 +1017,7 @@ private static void AddResponseMessage(List chatMessages, ChatHisto logger.LogDebug("Failed to handle tool request ({ToolId}). {Error}", toolCall.Id, errorMessage); } - // Add the tool response message to the chat messages result ??= errorMessage ?? string.Empty; - chatMessages.Add(new ToolChatMessage(toolCall.Id, result)); // Add the tool response message to the chat history. var message = new ChatMessageContent(role: AuthorRole.Tool, content: result, metadata: new Dictionary { { OpenAIChatMessageContent.ToolIdProperty, toolCall.Id } }); From a18953fca23caf3695122cc0f98f9a20f50eba43 Mon Sep 17 00:00:00 2001 From: SergeyMenshykh <68852919+SergeyMenshykh@users.noreply.github.com> Date: Thu, 8 Aug 2024 09:46:32 +0100 Subject: [PATCH 165/226] .Net: Enable code coverage for OpenAi connectors (#7970) Code coverage enabled for the `Microsoft.SemanticKernel.Connectors.AzureOpenAI` assembly --- .github/workflows/dotnet-build-and-test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/dotnet-build-and-test.yml b/.github/workflows/dotnet-build-and-test.yml index 366934c73314..c53353de8b3f 100644 --- a/.github/workflows/dotnet-build-and-test.yml +++ b/.github/workflows/dotnet-build-and-test.yml @@ -126,7 +126,7 @@ jobs: reports: "./TestResults/Coverage/**/coverage.cobertura.xml" targetdir: "./TestResults/Reports" reporttypes: "JsonSummary" - assemblyfilters: "+Microsoft.SemanticKernel.Abstractions;+Microsoft.SemanticKernel.Core;+Microsoft.SemanticKernel.PromptTemplates.Handlebars;+Microsoft.SemanticKernel.Connectors.OpenAI;+Microsoft.SemanticKernel.Yaml;+Microsoft.SemanticKernel.Agents.Abstractions;+Microsoft.SemanticKernel.Agents.Core;+Microsoft.SemanticKernel.Agents.OpenAI" + assemblyfilters: "+Microsoft.SemanticKernel.Abstractions;+Microsoft.SemanticKernel.Core;+Microsoft.SemanticKernel.PromptTemplates.Handlebars;+Microsoft.SemanticKernel.Connectors.OpenAI;+Microsoft.SemanticKernel.Connectors.AzureOpenAI;+Microsoft.SemanticKernel.Yaml;+Microsoft.SemanticKernel.Agents.Abstractions;+Microsoft.SemanticKernel.Agents.Core;+Microsoft.SemanticKernel.Agents.OpenAI" - name: Check coverage shell: pwsh From 0d082e1594da0567c58a317ed732446d454e6ab3 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 10:11:39 -0700 Subject: [PATCH 166/226] Fix channel-keys generation --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index b13afaecf901..fb49364d86be 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -265,7 +265,16 @@ public async IAsyncEnumerable InvokeAsync( } /// - protected override IEnumerable GetChannelKeys() => this._channelKeys; + protected override IEnumerable GetChannelKeys() + { + // Distinguish from other channel types. + yield return typeof(OpenAIAssistantChannel).FullName!; + + foreach (string key in this._channelKeys) + { + yield return key; + } + } /// protected override async Task CreateChannelAsync(CancellationToken cancellationToken) From b8f1d0256b3209a0c9d56a622aea4653155a3027 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 10:12:15 -0700 Subject: [PATCH 167/226] Allocate key-enumeration --- dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs index 8cc879f2d0b4..e291de57ee52 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -31,12 +31,12 @@ public sealed class OpenAIClientProvider /// /// Configuration keys required for management. /// - internal IEnumerable ConfigurationKeys { get; } + internal IReadOnlyList ConfigurationKeys { get; } private OpenAIClientProvider(OpenAIClient client, IEnumerable keys) { this.Client = client; - this.ConfigurationKeys = keys; + this.ConfigurationKeys = keys.ToArray(); } /// From fe3b835ff2ecd1cf743f73cd101b3ecc0c539469 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 11:54:34 -0700 Subject: [PATCH 168/226] Final fixes --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 2d702675e5ba..e89257cb2fd0 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -167,7 +167,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions); - options.ToolsOverride.AddRange(tools); // %%% VERIFY + options.ToolsOverride.AddRange(tools); ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 8a86638be700..3e476bb7989b 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -56,9 +56,9 @@ public sealed class OpenAIAssistantAgent : KernelAgent public RunPollingOptions PollingOptions { get; } = new(); /// - /// Expose predefined tools merged with available kernel functions. + /// Expose predefined tools for run-processing. /// - internal IReadOnlyList Tools => [.. this._assistant.Tools, .. this.Kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))]; + internal IReadOnlyList Tools => this._assistant.Tools; /// /// Define a new . From 7cdfba6caf3e27e25c1540d2c91bf9885eb0af57 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 12:20:18 -0700 Subject: [PATCH 169/226] Test enhancement --- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 5 ++- .../ChatHistorySummarizationReducer.cs | 4 +- .../ChatHistorySummarizationReducerTests.cs | 37 +++++++++++++++++-- .../ChatHistoryTruncationReducerTests.cs | 28 +++++++++++++- 4 files changed, 67 insertions(+), 7 deletions(-) diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index 3423308325c2..2ade71b86a9a 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -65,7 +65,7 @@ await chatCompletionService.GetChatMessageContentsAsync( history.Add(message); } - foreach (ChatMessageContent message in messages ?? []) + foreach (ChatMessageContent message in messages) { message.AuthorName = this.Name; @@ -121,6 +121,9 @@ public async IAsyncEnumerable InvokeStreamingAsync( /// protected override IEnumerable GetChannelKeys() { + // Distinguish from other channel types. + yield return typeof(ChatHistoryChannel).FullName!; + // Agents with different reducers shall not share the same channel. // Agents with the same or equivalent reducer shall share the same channel. if (this.HistoryReducer != null) diff --git a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs index a45bfa57011d..4f909e7e146d 100644 --- a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs +++ b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs @@ -154,7 +154,9 @@ public override bool Equals(object? obj) ChatHistorySummarizationReducer? other = obj as ChatHistorySummarizationReducer; return other != null && this._thresholdCount == other._thresholdCount && - this._targetCount == other._targetCount; + this._targetCount == other._targetCount && + this.UseSingleSummary == other.UseSingleSummary && + string.Equals(this.SummarizationInstructions, other.SummarizationInstructions, StringComparison.Ordinal); } /// diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs index f464b6a8214a..7661cfcdf8cd 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs @@ -23,7 +23,7 @@ public class ChatHistorySummarizationReducerTests [InlineData(-1)] [InlineData(-1, int.MaxValue)] [InlineData(int.MaxValue, -1)] - public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null) + public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { Mock mockCompletionService = this.CreateMockCompletionService(); @@ -34,7 +34,7 @@ public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? /// Verify object state after initialization. /// [Fact] - public void VerifyChatHistoryInitializationState() + public void VerifyInitializationState() { Mock mockCompletionService = this.CreateMockCompletionService(); @@ -54,11 +54,42 @@ public void VerifyChatHistoryInitializationState() Assert.False(reducer.FailOnError); } + /// + /// Validate equality override. + /// + [Fact] + public void VerifyEquality() + { + Mock mockCompletionService = this.CreateMockCompletionService(); + + ChatHistorySummarizationReducer reducer1 = new(mockCompletionService.Object, 3, 3); + ChatHistorySummarizationReducer reducer2 = new(mockCompletionService.Object, 3, 3); + ChatHistorySummarizationReducer reducer3 = new(mockCompletionService.Object, 3, 3) { UseSingleSummary = false }; + ChatHistorySummarizationReducer reducer4 = new(mockCompletionService.Object, 3, 3) { SummarizationInstructions = "override" }; + ChatHistorySummarizationReducer reducer5 = new(mockCompletionService.Object, 4, 3); + ChatHistorySummarizationReducer reducer6 = new(mockCompletionService.Object, 3, 5); + ChatHistorySummarizationReducer reducer7 = new(mockCompletionService.Object, 3); + ChatHistorySummarizationReducer reducer8 = new(mockCompletionService.Object, 3); + + Assert.True(reducer1.Equals(reducer1)); + Assert.True(reducer1.Equals(reducer2)); + Assert.True(reducer7.Equals(reducer8)); + Assert.True(reducer3.Equals(reducer3)); + Assert.True(reducer4.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer5)); + Assert.False(reducer1.Equals(reducer6)); + Assert.False(reducer1.Equals(reducer7)); + Assert.False(reducer1.Equals(reducer8)); + Assert.False(reducer1.Equals(null)); + } + /// /// Validate hash-code expresses reducer equivalency. /// [Fact] - public void VerifyChatHistoryHasCode() + public void VerifyHashCode() { HashSet reducers = []; diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs index eebcf8fc6136..27675420264c 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs @@ -21,16 +21,40 @@ public class ChatHistoryTruncationReducerTests [InlineData(-1)] [InlineData(-1, int.MaxValue)] [InlineData(int.MaxValue, -1)] - public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null) + public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { Assert.Throws(() => new ChatHistoryTruncationReducer(targetCount, thresholdCount)); } + /// + /// Validate equality override. + /// + [Fact] + public void VerifyEquality() + { + ChatHistoryTruncationReducer reducer1 = new(3, 3); + ChatHistoryTruncationReducer reducer2 = new(3, 3); + ChatHistoryTruncationReducer reducer3 = new(4, 3); + ChatHistoryTruncationReducer reducer4 = new(3, 5); + ChatHistoryTruncationReducer reducer5 = new(3); + ChatHistoryTruncationReducer reducer6 = new(3); + + Assert.True(reducer1.Equals(reducer1)); + Assert.True(reducer1.Equals(reducer2)); + Assert.True(reducer5.Equals(reducer6)); + Assert.True(reducer3.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer5)); + Assert.False(reducer1.Equals(reducer6)); + Assert.False(reducer1.Equals(null)); + } + /// /// Validate hash-code expresses reducer equivalency. /// [Fact] - public void VerifyChatHistoryHasCode() + public void VerifyHashCode() { HashSet reducers = []; From 5c626564fb625fdf77307d8755913ea88196dfcb Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 13:07:41 -0700 Subject: [PATCH 170/226] Up test coverage --- dotnet/src/Agents/Abstractions/AgentChat.cs | 2 +- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 36 +++++++++++++++++++ dotnet/src/Agents/UnitTests/MockAgent.cs | 3 +- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index f4654963444e..ca6cbdaab259 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -285,7 +285,7 @@ private void ClearActivitySignal() /// The activity signal is used to manage ability and visibility for taking actions based /// on conversation history. /// - private void SetActivityOrThrow() + protected void SetActivityOrThrow() { // Note: Interlocked is the absolute lightest synchronization mechanism available in dotnet. int wasActive = Interlocked.CompareExchange(ref this._isActive, 1, 0); diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index 49c36ae73c53..0f796badd440 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -3,9 +3,12 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests; @@ -55,6 +58,32 @@ public async Task VerifyAgentChatLifecycleAsync() await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync(chat.Agent)); // Agent history } + /// + /// Verify throw exception for system message. + /// + [Fact] + public void VerifyAgentChatRejectsSystemMessge() + { + // Create chat + TestChat chat = new() { LoggerFactory = new Mock().Object }; + + // Verify system message not accepted + Assert.Throws(() => chat.AddChatMessage(new ChatMessageContent(AuthorRole.System, "hi"))); + } + + /// + /// Verify throw exception for if invoked when active. + /// + [Fact] + public async Task VerifyAgentChatThrowsWhenActiveAsync() + { + // Create chat + TestChat chat = new(); + + // Verify system message not accepted + await Assert.ThrowsAsync(() => chat.InvalidInvokeAsync().ToArrayAsync().AsTask()); + } + /// /// Verify the management of instances as they join . /// @@ -119,5 +148,12 @@ private sealed class TestChat : AgentChat public override IAsyncEnumerable InvokeAsync( CancellationToken cancellationToken = default) => this.InvokeAgentAsync(this.Agent, cancellationToken); + + public IAsyncEnumerable InvalidInvokeAsync( + CancellationToken cancellationToken = default) + { + this.SetActivityOrThrow(); + return this.InvokeAgentAsync(this.Agent, cancellationToken); + } } } diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index f3b833024001..51fbf36c6ab4 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -46,7 +47,7 @@ public IAsyncEnumerable InvokeStreamingAsync( /// protected internal override IEnumerable GetChannelKeys() { - yield return typeof(ChatHistoryChannel).FullName!; + yield return Guid.NewGuid().ToString(); } /// From 50774653b13313c4326f9e8aca0523f753c61f7a Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 13:08:36 -0700 Subject: [PATCH 171/226] Typo --- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index 0f796badd440..ed6682d101ba 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -62,7 +62,7 @@ public async Task VerifyAgentChatLifecycleAsync() /// Verify throw exception for system message. /// [Fact] - public void VerifyAgentChatRejectsSystemMessge() + public void VerifyAgentChatRejectsSystemMessage() { // Create chat TestChat chat = new() { LoggerFactory = new Mock().Object }; From bf3825ec93d3706d80a079b6de0e0f8fa9ae1b21 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 13:23:39 -0700 Subject: [PATCH 172/226] Namespace + core coverage --- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 1 - .../KernelFunctionSelectionStrategyTests.cs | 31 ++++++++++++++++--- .../KernelFunctionTerminationStrategyTests.cs | 9 +++++- 3 files changed, 35 insertions(+), 6 deletions(-) diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index ed6682d101ba..d9d6cd560e07 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs index af045e67873d..5b2453b47fe1 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs @@ -27,12 +27,16 @@ public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - ResultParser = (result) => result.GetValue() ?? string.Empty, + ResultParser = (result) => mockAgent.Object.Id, + AgentsVariableName = "agents", + HistoryVariableName = "history", }; Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); + Assert.NotEqual("agent", KernelFunctionSelectionStrategy.DefaultAgentsVariableName); + Assert.NotEqual("history", KernelFunctionSelectionStrategy.DefaultHistoryVariableName); Agent nextAgent = await strategy.NextAsync([mockAgent.Object], []); @@ -44,16 +48,35 @@ public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() /// Verify strategy mismatch. /// [Fact] - public async Task VerifyKernelFunctionSelectionStrategyParsingAsync() + public async Task VerifyKernelFunctionSelectionStrategyThrowsOnNullResultAsync() { Mock mockAgent = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(string.Empty)); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id)); + + KernelFunctionSelectionStrategy strategy = + new(plugin.Single(), new()) + { + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, + ResultParser = (result) => "larry", + }; + + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); + } + + /// + /// Verify strategy mismatch. + /// + [Fact] + public async Task VerifyKernelFunctionSelectionStrategyThrowsOnBadResultAsync() + { + Mock mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin("")); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, - ResultParser = (result) => result.GetValue() ?? string.Empty, + ResultParser = (result) => result.GetValue() ?? null!, }; await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs index 6f0b446e5e7a..36ef6e7e6e79 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs @@ -24,11 +24,18 @@ public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync() { KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin()); - KernelFunctionTerminationStrategy strategy = new(plugin.Single(), new()); + KernelFunctionTerminationStrategy strategy = + new(plugin.Single(), new()) + { + AgentVariableName = "agent", + HistoryVariableName = "history", + }; Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); + Assert.NotEqual("agent", KernelFunctionTerminationStrategy.DefaultAgentVariableName); + Assert.NotEqual("history", KernelFunctionTerminationStrategy.DefaultHistoryVariableName); Mock mockAgent = new(); From 590fbf3d96ad81d4c12ceadbf99f9957c7c97af7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 14:33:11 -0700 Subject: [PATCH 173/226] Service-selection coverage --- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 6 +++--- .../UnitTests/Core/ChatCompletionAgentTests.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index 2ade71b86a9a..91f5b864e725 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -38,7 +38,7 @@ public async IAsyncEnumerable InvokeAsync( kernel ??= this.Kernel; arguments ??= this.Arguments; - (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments); + (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); ChatHistory chat = this.SetupAgentChatHistory(history); @@ -83,7 +83,7 @@ public async IAsyncEnumerable InvokeStreamingAsync( kernel ??= this.Kernel; arguments ??= this.Arguments; - (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments); + (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); ChatHistory chat = this.SetupAgentChatHistory(history); @@ -148,7 +148,7 @@ protected override Task CreateChannelAsync(CancellationToken cance return Task.FromResult(channel); } - private (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) + internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) { // Need to provide a KernelFunction to the service selector as a container for the execution-settings. KernelFunction nullPrompt = KernelFunctionFactory.CreateFromPrompt("placeholder", arguments?.ExecutionSettings?.Values); diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs index c8a1c0578613..c99c98bdd4b9 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs @@ -115,6 +115,24 @@ public async Task VerifyChatCompletionAgentStreamingAsync() Times.Once); } + /// + /// Verify the invocation and response of . + /// + [Fact] + public void VerifyChatCompletionServiceSelection() + { + Mock mockService = new(); + Kernel kernel = CreateKernel(mockService.Object); + + (IChatCompletionService service, PromptExecutionSettings? settings) = ChatCompletionAgent.GetChatCompletionService(kernel, null); + Assert.Null(settings); + + (service, settings) = ChatCompletionAgent.GetChatCompletionService(kernel, []); + Assert.Null(settings); + + Assert.Throws(() => ChatCompletionAgent.GetChatCompletionService(kernel, new KernelArguments(new PromptExecutionSettings() { ServiceId = "anything" }))); + } + private static Kernel CreateKernel(IChatCompletionService chatCompletionService) { var builder = Kernel.CreateBuilder(); From e46a93cdb83ae60b46e8013eb5252579067deb26 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 8 Aug 2024 14:49:43 -0700 Subject: [PATCH 174/226] Even deeper coverage --- .../Core/ChatCompletionAgentTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs index c99c98bdd4b9..f19453a74058 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.History; using Microsoft.SemanticKernel.ChatCompletion; using Moq; using Xunit; @@ -133,6 +134,24 @@ public void VerifyChatCompletionServiceSelection() Assert.Throws(() => ChatCompletionAgent.GetChatCompletionService(kernel, new KernelArguments(new PromptExecutionSettings() { ServiceId = "anything" }))); } + /// + /// Verify the invocation and response of . + /// + [Fact] + public void VerifyChatCompletionChannelKeys() + { + ChatCompletionAgent agent1 = new(); + ChatCompletionAgent agent2 = new(); + ChatCompletionAgent agent3 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; + ChatCompletionAgent agent4 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; + ChatCompletionAgent agent5 = new() { HistoryReducer = new ChatHistoryTruncationReducer(100) }; + + Assert.Equal(agent1.GetChannelKeys(), agent2.GetChannelKeys()); + Assert.Equal(agent3.GetChannelKeys(), agent4.GetChannelKeys()); + Assert.NotEqual(agent1.GetChannelKeys(), agent3.GetChannelKeys()); + Assert.NotEqual(agent3.GetChannelKeys(), agent5.GetChannelKeys()); + } + private static Kernel CreateKernel(IChatCompletionService chatCompletionService) { var builder = Kernel.CreateBuilder(); From 8be28e126e3affdd0428840845d30332fde159c9 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Fri, 9 Aug 2024 11:31:25 +0100 Subject: [PATCH 175/226] .Net: OpenAI V2 - Small fix (#8015) ## Small fix - Remove remaining comments. --- .../Connectors.OpenAI/Core/ClientCore.cs | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs index 64083aa99acc..843768bc17c2 100644 --- a/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs +++ b/dotnet/src/Connectors/Connectors.OpenAI/Core/ClientCore.cs @@ -1,20 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -/* -Phase 01 : This class was created adapting and merging ClientCore and OpenAIClientCore classes. -System.ClientModel changes were added and adapted to the code as this package is now used as a dependency over OpenAI package. -All logic from original ClientCore and OpenAIClientCore were preserved. - -Phase 02 : -- Moved AddAttributes usage to the constructor, avoiding the need verify and adding it in the services. -- Added ModelId attribute to the OpenAIClient constructor. -- Added WhiteSpace instead of empty string for ApiKey to avoid exception from OpenAI Client on custom endpoints added an issue in OpenAI SDK repo. https://github.com/openai/openai-dotnet/issues/90 - -Phase 05: -- Model Id became not be required to support services like: File Service. - -*/ - using System; using System.ClientModel; using System.ClientModel.Primitives; From 9c19d597dbdb3d3b18ff539cc7b1315f2dccc9a5 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:44:38 -0700 Subject: [PATCH 176/226] Update dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 3e476bb7989b..97b493eaef22 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -64,7 +64,7 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// Define a new . /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the API service. + /// OpenAI client provider for accessing the API service. /// The assistant definition. /// The to monitor for cancellation requests. The default is . /// An instance From 176f3727a4877ac07d31f8177877095fce75d3d2 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:45:24 -0700 Subject: [PATCH 177/226] Update dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index 28b625d3eee8..c982c5b0dd81 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -4,7 +4,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// Defines agent execution options for each invocation. +/// Defines assistant execution options for each invocation. /// /// /// These options are persisted as a single entry of the agent's metadata with key: "__run_options" From 71bd060a1ed8847429704e8c16a1a9aaacc37863 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:46:24 -0700 Subject: [PATCH 178/226] Update dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs index c982c5b0dd81..074b92831c92 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -7,7 +7,7 @@ namespace Microsoft.SemanticKernel.Agents.OpenAI; /// Defines assistant execution options for each invocation. /// /// -/// These options are persisted as a single entry of the agent's metadata with key: "__run_options" +/// These options are persisted as a single entry of the assistant's metadata with key: "__run_options" /// public sealed class OpenAIAssistantExecutionOptions { From a24d151c92c1f2929c23bbcdc729c9b03608dc12 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:48:27 -0700 Subject: [PATCH 179/226] Update dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs index f624dd62152b..73adb0b2f5cd 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -24,7 +24,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsDefault() // Setup message with null metadata ChatMessageContent message = new(AuthorRole.User, "test"); - // Create options + // Act MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Assert From fd0d81e0abe6239f1d2edfc24691b0d8e78d3953 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:48:41 -0700 Subject: [PATCH 180/226] Update dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- .../UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs index 73adb0b2f5cd..5d0e01ba9926 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -21,7 +21,7 @@ public class AssistantMessageFactoryTests [Fact] public void VerifyAssistantMessageAdapterCreateOptionsDefault() { - // Setup message with null metadata + // Arrange (Setup message with null metadata) ChatMessageContent message = new(AuthorRole.User, "test"); // Act From 7162e3483bab80bc8d88dd653d7801c7c91ad5f0 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:56:08 -0700 Subject: [PATCH 181/226] Update dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 97b493eaef22..d60f01389b71 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -363,8 +363,7 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.SingleOrDefault(); bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject; - return - new() + return new() { Id = model.Id, Name = model.Name, From 96c3ac0b54625dcca4fafda097c57da254f73284 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 08:59:16 -0700 Subject: [PATCH 182/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index e89257cb2fd0..1ef87f72779f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -428,7 +428,7 @@ private static AnnotationContent GenerateAnnotationContent(TextAnnotation annota }; } - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string code) + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string pythonCode) { return new ChatMessageContent( From 58ce240bb65d3cdf5f98e105f35648372039c4a5 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:00:04 -0700 Subject: [PATCH 183/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 1ef87f72779f..c34aa5f56a0c 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -48,7 +48,7 @@ public static async Task CreateThreadAsync(AssistantClient client, OpenA ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpreterFileIds), }; - if (options?.Messages != null) + if (options?.Messages is not null) { foreach (ChatMessageContent message in options.Messages) { From 56f0fea936608cd69deea60935e21979b3c3a472 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 Aug 2024 09:03:10 -0700 Subject: [PATCH 184/226] Indenting --- .../src/Agents/OpenAI/OpenAIClientProvider.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs index e291de57ee52..3e2e395a77ea 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -104,12 +104,11 @@ public static OpenAIClientProvider FromClient(OpenAIClient client) private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, HttpClient? httpClient) { - AzureOpenAIClientOptions options = - new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = endpoint, - }; + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint, + }; ConfigureClientOptions(httpClient, options); @@ -118,12 +117,11 @@ private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient) { - OpenAIClientOptions options = - new() - { - ApplicationId = HttpHeaderConstant.Values.UserAgent, - Endpoint = endpoint ?? httpClient?.BaseAddress, - }; + OpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint ?? httpClient?.BaseAddress, + }; ConfigureClientOptions(httpClient, options); From c45c1c8baa3a2ff8402a88badf0c700b212e45b2 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Fri, 9 Aug 2024 09:13:40 -0700 Subject: [PATCH 185/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index c34aa5f56a0c..567b79fee57e 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -388,7 +388,7 @@ private static ChatMessageContent GenerateMessageContent(string? assistantName, // Process text content if (!string.IsNullOrEmpty(itemContent.Text)) { - content.Items.Add(new TextContent(itemContent.Text.Trim())); + content.Items.Add(new TextContent(itemContent.Text)); foreach (TextAnnotation annotation in itemContent.TextAnnotations) { From b0b35e7e10524c8c78622204dfce6268bea81953 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 Aug 2024 09:51:08 -0700 Subject: [PATCH 186/226] Updated based on comments --- .../Concepts/Agents/MixedChat_Agents.cs | 5 +- .../Concepts/Agents/MixedChat_Files.cs | 3 +- .../Concepts/Agents/MixedChat_Images.cs | 3 +- .../Agents/OpenAIAssistant_ChartMaker.cs | 3 +- .../OpenAIAssistant_FileManipulation.cs | 3 +- .../Step08_Assistant.cs | 5 +- .../Step09_Assistant_Vision.cs | 3 +- .../Step10_AssistantTool_CodeInterpreter.cs | 5 +- .../Step11_AssistantTool_FileSearch.cs | 5 +- .../src/Agents/Abstractions/AgentChannel.cs | 8 +++ .../Agents/Abstractions/AggregatorChannel.cs | 3 + .../OpenAI/Internal/AssistantThreadActions.cs | 4 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 41 +++++++------- .../OpenAI/OpenAIAssistantDefinition.cs | 14 ++++- .../AssistantRunOptionsFactoryTests.cs | 8 +-- .../OpenAI/OpenAIAssistantAgentTests.cs | 56 ++++++------------- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 7 +-- .../Agents/OpenAIAssistantAgentTests.cs | 3 +- 18 files changed, 84 insertions(+), 95 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index c387ff5704c2..21b19c1d342c 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -47,12 +47,11 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync() OpenAIAssistantAgent agentWriter = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - provider: this.GetClientProvider(), - definition: new() + clientProvider: this.GetClientProvider(), + definition: new(this.Model) { Instructions = CopyWriterInstructions, Name = CopyWriterName, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index 982e41417c13..0219c25f7712 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -36,11 +36,10 @@ await fileClient.UploadFileAsync( await OpenAIAssistantAgent.CreateAsync( kernel: new(), provider, - new() + new(this.Model) { EnableCodeInterpreter = true, CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant code-interpreter - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 4fae255c9b86..142706e8506c 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -31,12 +31,11 @@ public async Task AnalyzeDataAndGenerateChartAsync() await OpenAIAssistantAgent.CreateAsync( kernel: new(), provider, - new() + new(this.Model) { Instructions = AnalystInstructions, Name = AnalystName, EnableCodeInterpreter = true, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index 08aee21c8707..cd81f7c4d187 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -28,12 +28,11 @@ public async Task GenerateChartWithOpenAIAssistantAgentAsync() await OpenAIAssistantAgent.CreateAsync( kernel: new(), provider, - new() + new(this.Model) { Instructions = AgentInstructions, Name = AgentName, EnableCodeInterpreter = true, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index bca6118041e4..dc4af2ad2743 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -31,11 +31,10 @@ await fileClient.UploadFileAsync( await OpenAIAssistantAgent.CreateAsync( kernel: new(), provider, - new() + new(this.Model) { EnableCodeInterpreter = true, CodeInterpreterFileIds = [uploadFile.Id], - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs index c937c7de9c70..ba4ab065c2a6 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs @@ -23,12 +23,11 @@ public async Task UseSingleAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - provider: this.GetClientProvider(), - new() + clientProvider: this.GetClientProvider(), + new(this.Model) { Instructions = HostInstructions, Name = HostName, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs index 29a6e108df24..62845f2c4366 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs @@ -25,9 +25,8 @@ public async Task UseSingleAssistantAgentAsync() await OpenAIAssistantAgent.CreateAsync( kernel: new(), provider, - new() + new(this.Model) { - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs index d623c8a28b7b..1205771d66be 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs @@ -17,11 +17,10 @@ public async Task UseCodeInterpreterToolWithAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - provider: this.GetClientProvider(), - new() + clientProvider: this.GetClientProvider(), + new(this.Model) { EnableCodeInterpreter = true, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs index bfcd93dd3ecb..d34cadaf3707 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -21,11 +21,10 @@ public async Task UseFileSearchToolWithAssistantAgentAsync() OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - provider: this.GetClientProvider(), - new() + clientProvider: this.GetClientProvider(), + new(this.Model) { EnableFileSearch = true, - ModelId = this.Model, Metadata = AssistantSampleMetadata, }); diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 9788464a2adb..73469ed723b5 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -31,6 +31,10 @@ public abstract class AgentChannel /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. + /// + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. + /// protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( Agent agent, CancellationToken cancellationToken = default); @@ -59,6 +63,10 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. + /// + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. + /// protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( TAgent agent, CancellationToken cancellationToken = default); diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 73561a4eba8b..0c6bc252891d 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -13,11 +13,13 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken = default) { return this._chat.GetChatMessagesAsync(cancellationToken); } + /// protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(AggregatorAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ChatMessageContent? lastMessage = null; @@ -47,6 +49,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync } } + /// protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { // Always receive the initial history from the owning chat. diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index c34aa5f56a0c..5d508123cc95 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -133,6 +133,8 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// /// Invoke the assistant on the specified thread. + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. /// /// The assistant agent to interact with the thread. /// The assistant client @@ -434,7 +436,7 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam new ChatMessageContent( AuthorRole.Assistant, [ - new TextContent(code) + new TextContent(pythonCode) ]) { AuthorName = agentName, diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index d60f01389b71..f5c4a3588cf8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -70,17 +70,17 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// An instance public static async Task CreateAsync( Kernel kernel, - OpenAIClientProvider provider, + OpenAIClientProvider clientProvider, OpenAIAssistantDefinition definition, CancellationToken cancellationToken = default) { // Validate input Verify.NotNull(kernel, nameof(kernel)); - Verify.NotNull(provider, nameof(provider)); + Verify.NotNull(clientProvider, nameof(clientProvider)); Verify.NotNull(definition, nameof(definition)); // Create the client - AssistantClient client = CreateClient(provider); + AssistantClient client = CreateClient(clientProvider); // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); @@ -88,7 +88,7 @@ public static async Task CreateAsync( // Instantiate the agent return - new OpenAIAssistantAgent(model, provider, client) + new OpenAIAssistantAgent(model, clientProvider, client) { Kernel = kernel, }; @@ -363,23 +363,22 @@ private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant mod string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.SingleOrDefault(); bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject; - return new() - { - Id = model.Id, - Name = model.Name, - Description = model.Description, - Instructions = model.Instructions, - CodeInterpreterFileIds = fileIds, - EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), - EnableFileSearch = model.Tools.Any(t => t is FileSearchToolDefinition), - Metadata = model.Metadata, - ModelId = model.Model, - EnableJsonResponse = enableJsonResponse, - TopP = model.NucleusSamplingFactor, - Temperature = model.Temperature, - VectorStoreId = string.IsNullOrWhiteSpace(vectorStoreId) ? null : vectorStoreId, - ExecutionOptions = options, - }; + return new(model.Model) + { + Id = model.Id, + Name = model.Name, + Description = model.Description, + Instructions = model.Instructions, + CodeInterpreterFileIds = fileIds, + EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), + EnableFileSearch = model.Tools.Any(t => t is FileSearchToolDefinition), + Metadata = model.Metadata, + EnableJsonResponse = enableJsonResponse, + TopP = model.NucleusSamplingFactor, + Temperature = model.Temperature, + VectorStoreId = string.IsNullOrWhiteSpace(vectorStoreId) ? null : vectorStoreId, + ExecutionOptions = options, + }; } private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index f52d468c8d6f..7b7015aa3b4a 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -12,7 +12,7 @@ public sealed class OpenAIAssistantDefinition /// /// Identifies the AI model targeted by the agent. /// - public string ModelId { get; init; } = string.Empty; + public string ModelId { get; } /// /// The description of the assistant. @@ -97,4 +97,16 @@ public sealed class OpenAIAssistantDefinition /// [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public OpenAIAssistantExecutionOptions? ExecutionOptions { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The targeted model + [JsonConstructor] + public OpenAIAssistantDefinition(string modelId) + { + Verify.NotNullOrWhiteSpace(modelId); + + this.ModelId = modelId; + } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs index 325e93969f20..704cf0252852 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs @@ -19,7 +19,7 @@ public class AssistantRunOptionsFactoryTests public void AssistantRunOptionsFactoryExecutionOptionsNullTest() { OpenAIAssistantDefinition definition = - new() + new("gpt-anything") { Temperature = 0.5F, }; @@ -38,7 +38,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsNullTest() public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() { OpenAIAssistantDefinition definition = - new() + new("gpt-anything") { Temperature = 0.5F, }; @@ -62,7 +62,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() { OpenAIAssistantDefinition definition = - new() + new("gpt-anything") { Temperature = 0.5F, ExecutionOptions = @@ -95,7 +95,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest() { OpenAIAssistantDefinition definition = - new() + new("gpt-anything") { Temperature = 0.5F, ExecutionOptions = diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index a1a1959e9cff..bee75be9d80c 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -32,11 +32,7 @@ public sealed class OpenAIAssistantAgentTests : IDisposable [Fact] public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() { - OpenAIAssistantDefinition definition = - new() - { - ModelId = "testmodel", - }; + OpenAIAssistantDefinition definition = new("testmodel"); await this.VerifyAgentCreationAsync(definition); } @@ -49,9 +45,8 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", Name = "testname", Description = "testdescription", Instructions = "testinstructions", @@ -68,9 +63,8 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableCodeInterpreter = true, }; @@ -85,9 +79,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableCodeInterpreter = true, CodeInterpreterFileIds = ["file1", "file2"], }; @@ -103,9 +96,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableFileSearch = true, }; @@ -120,9 +112,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableFileSearch = true, VectorStoreId = "#vs1", }; @@ -138,9 +129,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", Metadata = new Dictionary() { { "a", "1" }, @@ -159,9 +149,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableJsonResponse = true, }; @@ -176,9 +165,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", Temperature = 2.0F, }; @@ -193,9 +181,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", TopP = 2.0F, }; @@ -210,10 +197,9 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", - ExecutionOptions = new(), + ExecutionOptions = new OpenAIAssistantExecutionOptions(), }; await this.VerifyAgentCreationAsync(definition); @@ -227,9 +213,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsy public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", ExecutionOptions = new() { @@ -249,9 +234,8 @@ public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAndMetadataAsync() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", ExecutionOptions = new(), Metadata = new Dictionary() { @@ -269,11 +253,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAnd [Fact] public async Task VerifyOpenAIAssistantAgentRetrievalAsync() { - OpenAIAssistantDefinition definition = - new() - { - ModelId = "testmodel", - }; + OpenAIAssistantDefinition definition = new("testmodel"); this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); @@ -619,11 +599,7 @@ private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAs private Task CreateAgentAsync() { - OpenAIAssistantDefinition definition = - new() - { - ModelId = "testmodel", - }; + OpenAIAssistantDefinition definition = new("testmodel"); this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index fa8d903419a5..a13261b58fdf 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -17,10 +17,10 @@ public class OpenAIAssistantDefinitionTests [Fact] public void VerifyOpenAIAssistantDefinitionInitialState() { - OpenAIAssistantDefinition definition = new(); + OpenAIAssistantDefinition definition = new("testmodel"); Assert.Equal(string.Empty, definition.Id); - Assert.Equal(string.Empty, definition.ModelId); + Assert.Equal("testmodel", definition.ModelId); Assert.Null(definition.Name); Assert.Null(definition.Instructions); Assert.Null(definition.Description); @@ -44,11 +44,10 @@ public void VerifyOpenAIAssistantDefinitionInitialState() public void VerifyOpenAIAssistantDefinitionAssignment() { OpenAIAssistantDefinition definition = - new() + new("testmodel") { Id = "testid", Name = "testname", - ModelId = "testmodel", Instructions = "testinstructions", Description = "testdescription", EnableFileSearch = true, diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index a4458f6cb470..0dc1ae952c20 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -76,10 +76,9 @@ private async Task ExecuteAgentAsync( await OpenAIAssistantAgent.CreateAsync( kernel, config, - new() + new(modelName) { Instructions = "Answer questions about the menu.", - ModelId = modelName, }); AgentGroupChat chat = new(); From 631eded8a1f916a22285a850a3da3d08027b4dbd Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 Aug 2024 11:40:49 -0700 Subject: [PATCH 187/226] Update unit-test comments --- .../Step04_KernelFunctionStrategies.cs | 2 +- .../src/Agents/UnitTests/AgentChannelTests.cs | 27 +++--- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 33 +++---- .../Agents/UnitTests/AggregatorAgentTests.cs | 24 +++++- .../UnitTests/Core/AgentGroupChatTests.cs | 30 +++++++ .../Core/Chat/AgentGroupChatSettingsTests.cs | 7 ++ .../AggregatorTerminationStrategyTests.cs | 41 +++++---- .../KernelFunctionSelectionStrategyTests.cs | 33 ++++--- .../KernelFunctionTerminationStrategyTests.cs | 14 +-- .../Chat/RegExTerminationStrategyTests.cs | 20 +++-- .../Chat/SequentialSelectionStrategyTests.cs | 38 +++++--- .../Core/ChatCompletionAgentTests.cs | 34 ++++++-- .../UnitTests/Core/ChatHistoryChannelTests.cs | 22 ++--- .../ChatHistoryReducerExtensionsTests.cs | 39 +++++++-- .../ChatHistorySummarizationReducerTests.cs | 38 ++++++-- .../ChatHistoryTruncationReducerTests.cs | 21 ++++- .../Extensions/ChatHistoryExtensionsTests.cs | 4 + .../UnitTests/Internal/BroadcastQueueTests.cs | 31 ++++--- .../UnitTests/Internal/KeyEncoderTests.cs | 5 +- dotnet/src/Agents/UnitTests/MockAgent.cs | 2 +- .../Azure/AddHeaderRequestPolicyTests.cs | 5 +- .../Extensions/AuthorRoleExtensionsTests.cs | 3 + .../Extensions/KernelExtensionsTests.cs | 6 ++ .../KernelFunctionExtensionsTests.cs | 10 +++ .../Internal/AssistantMessageFactoryTests.cs | 45 +++++++--- .../AssistantRunOptionsFactoryTests.cs | 15 ++++ .../OpenAI/OpenAIAssistantAgentTests.cs | 86 +++++++++++++++++-- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 6 ++ .../OpenAIAssistantInvocationOptionsTests.cs | 8 ++ .../OpenAI/OpenAIClientProviderTests.cs | 15 ++++ .../OpenAIThreadCreationOptionsTests.cs | 8 ++ .../OpenAI/RunPollingOptionsTests.cs | 6 ++ 32 files changed, 518 insertions(+), 160 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs index 24a4a1dc70b5..36424e6c268b 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs @@ -73,7 +73,7 @@ No participant should take more than one turn in a row. - {{{CopyWriterName}}} Always follow these rules when selecting the next participant: - - After user input, it is {{{CopyWriterName}}}'a turn. + - After user input, it is {{{CopyWriterName}}}'s turn. - After {{{CopyWriterName}}}, it is {{{ReviewerName}}}'s turn. - After {{{ReviewerName}}}, it is {{{CopyWriterName}}}'s turn. diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index 2a680614a54f..17994a12e6a0 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -23,20 +23,26 @@ public class AgentChannelTests [Fact] public async Task VerifyAgentChannelUpcastAsync() { + // Arrange TestChannel channel = new(); + // Assert Assert.Equal(0, channel.InvokeCount); - var messages = channel.InvokeAgentAsync(new TestAgent()).ToArrayAsync(); + // Act + var messages = channel.InvokeAgentAsync(new MockAgent()).ToArrayAsync(); + // Assert Assert.Equal(1, channel.InvokeCount); + // Act await Assert.ThrowsAsync(() => channel.InvokeAgentAsync(new NextAgent()).ToArrayAsync().AsTask()); + // Assert Assert.Equal(1, channel.InvokeCount); } /// /// Not using mock as the goal here is to provide entrypoint to protected method. /// - private sealed class TestChannel : AgentChannel + private sealed class TestChannel : AgentChannel { public int InvokeCount { get; private set; } @@ -44,7 +50,7 @@ private sealed class TestChannel : AgentChannel => base.InvokeAsync(agent, cancellationToken); #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(TestAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(MockAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { this.InvokeCount++; @@ -63,18 +69,5 @@ protected internal override Task ReceiveAsync(IEnumerable hi } } - private sealed class NextAgent : TestAgent; - - private class TestAgent : KernelAgent - { - protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected internal override IEnumerable GetChannelKeys() - { - throw new NotImplementedException(); - } - } + private sealed class NextAgent : MockAgent; } diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index d9d6cd560e07..cd83ab8b9f45 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -23,36 +23,36 @@ public class AgentChatTests [Fact] public async Task VerifyAgentChatLifecycleAsync() { - // Create chat + // Arrange: Create chat TestChat chat = new(); - // Verify initial state + // Assert: Verify initial state Assert.False(chat.IsActive); await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent history - // Inject history + // Act: Inject history chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "More")]); chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "And then some")]); - // Verify updated history + // Assert: Verify updated history await this.VerifyHistoryAsync(expectedCount: 2, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent hasn't joined - // Invoke with input & verify (agent joins chat) + // Act: Invoke with input & verify (agent joins chat) chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi")); await chat.InvokeAsync().ToArrayAsync(); - Assert.Equal(1, chat.Agent.InvokeCount); - // Verify updated history + // Assert: Verify updated history + Assert.Equal(1, chat.Agent.InvokeCount); await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync(chat.Agent)); // Agent history - // Invoke without input & verify + // Act: Invoke without input await chat.InvokeAsync().ToArrayAsync(); - Assert.Equal(2, chat.Agent.InvokeCount); - // Verify final history + // Assert: Verify final history + Assert.Equal(2, chat.Agent.InvokeCount); await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync(chat.Agent)); // Agent history } @@ -63,10 +63,10 @@ public async Task VerifyAgentChatLifecycleAsync() [Fact] public void VerifyAgentChatRejectsSystemMessage() { - // Create chat + // Arrange: Create chat TestChat chat = new() { LoggerFactory = new Mock().Object }; - // Verify system message not accepted + // Assert and Act: Verify system message not accepted Assert.Throws(() => chat.AddChatMessage(new ChatMessageContent(AuthorRole.System, "hi"))); } @@ -76,10 +76,10 @@ public void VerifyAgentChatRejectsSystemMessage() [Fact] public async Task VerifyAgentChatThrowsWhenActiveAsync() { - // Create chat + // Arrange: Create chat TestChat chat = new(); - // Verify system message not accepted + // Assert and Act: Verify system message not accepted await Assert.ThrowsAsync(() => chat.InvalidInvokeAsync().ToArrayAsync().AsTask()); } @@ -89,13 +89,14 @@ public async Task VerifyAgentChatThrowsWhenActiveAsync() [Fact(Skip = "Not 100% reliable for github workflows, but useful for dev testing.")] public async Task VerifyGroupAgentChatConcurrencyAsync() { + // Arrange TestChat chat = new(); Task[] tasks; int isActive = 0; - // Queue concurrent tasks + // Act: Queue concurrent tasks object syncObject = new(); lock (syncObject) { @@ -117,7 +118,7 @@ public async Task VerifyGroupAgentChatConcurrencyAsync() await Task.Yield(); - // Verify failure + // Assert: Verify failure await Assert.ThrowsAsync(() => Task.WhenAll(tasks)); async Task SynchronizedInvokeAsync() diff --git a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs index 1a607ea7e6c7..e6668c7ea568 100644 --- a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs @@ -21,6 +21,7 @@ public class AggregatorAgentTests [InlineData(AggregatorMode.Flat, 2)] public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeOffset) { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); @@ -44,38 +45,57 @@ public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeO // Add message to outer chat (no agent has joined) uberChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test uber")); + // Act var messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent hasn't joined chat, no broadcast + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent hasn't joined chat, no broadcast - // Add message to inner chat (not visible to parent) + // Arrange: Add message to inner chat (not visible to parent) groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test inner")); + // Act messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent still hasn't joined chat + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); - // Invoke outer chat (outer chat captures final inner message) + // Act: Invoke outer chat (outer chat captures final inner message) messages = await uberChat.InvokeAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Equal(1 + modeOffset, messages.Length); // New messages generated from inner chat + // Act messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(2 + modeOffset, messages.Length); // Total messages on uber chat + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized (agent equivalent) } diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs index ad7428f6f0b9..62420f90e62b 100644 --- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs @@ -23,12 +23,18 @@ public class AgentGroupChatTests [Fact] public void VerifyGroupAgentChatDefaultState() { + // Arrange AgentGroupChat chat = new(); + + // Assert Assert.Empty(chat.Agents); Assert.NotNull(chat.ExecutionSettings); Assert.False(chat.IsComplete); + // Act chat.IsComplete = true; + + // Assert Assert.True(chat.IsComplete); } @@ -38,21 +44,30 @@ public void VerifyGroupAgentChatDefaultState() [Fact] public async Task VerifyGroupAgentChatAgentMembershipAsync() { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); Agent agent4 = CreateMockAgent(); AgentGroupChat chat = new(agent1, agent2); + + // Assert Assert.Equal(2, chat.Agents.Count); + // Act chat.AddAgent(agent3); + // Assert Assert.Equal(3, chat.Agents.Count); + // Act var messages = await chat.InvokeAsync(agent4, isJoining: false).ToArrayAsync(); + // Assert Assert.Equal(3, chat.Agents.Count); + // Act messages = await chat.InvokeAsync(agent4).ToArrayAsync(); + // Assert Assert.Equal(4, chat.Agents.Count); } @@ -62,6 +77,7 @@ public async Task VerifyGroupAgentChatAgentMembershipAsync() [Fact] public async Task VerifyGroupAgentChatMultiTurnAsync() { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); @@ -81,10 +97,14 @@ public async Task VerifyGroupAgentChatMultiTurnAsync() IsComplete = true }; + // Act and Assert await Assert.ThrowsAsync(() => chat.InvokeAsync(CancellationToken.None).ToArrayAsync().AsTask()); + // Act chat.ExecutionSettings.TerminationStrategy.AutomaticReset = true; var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync(); + + // Assert Assert.Equal(9, messages.Length); Assert.False(chat.IsComplete); @@ -111,6 +131,7 @@ public async Task VerifyGroupAgentChatMultiTurnAsync() [Fact] public async Task VerifyGroupAgentChatFailedSelectionAsync() { + // Arrange AgentGroupChat chat = Create3AgentChat(); chat.ExecutionSettings = @@ -128,6 +149,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync() // Remove max-limit in order to isolate the target behavior. chat.ExecutionSettings.TerminationStrategy.MaximumIterations = int.MaxValue; + // Act and Assert await Assert.ThrowsAsync(() => chat.InvokeAsync().ToArrayAsync().AsTask()); } @@ -137,6 +159,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync() [Fact] public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() { + // Arrange AgentGroupChat chat = Create3AgentChat(); chat.ExecutionSettings = @@ -150,7 +173,10 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() } }; + // Act var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.True(chat.IsComplete); } @@ -161,6 +187,7 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() [Fact] public async Task VerifyGroupAgentChatDiscreteTerminationAsync() { + // Arrange Agent agent1 = CreateMockAgent(); AgentGroupChat chat = @@ -178,7 +205,10 @@ public async Task VerifyGroupAgentChatDiscreteTerminationAsync() } }; + // Act var messages = await chat.InvokeAsync(agent1).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.True(chat.IsComplete); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs index d17391ee24be..ecb5cd6eee33 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs @@ -16,7 +16,10 @@ public class AgentGroupChatSettingsTests [Fact] public void VerifyChatExecutionSettingsDefault() { + // Arrange AgentGroupChatSettings settings = new(); + + // Assert Assert.IsType(settings.TerminationStrategy); Assert.Equal(1, settings.TerminationStrategy.MaximumIterations); Assert.IsType(settings.SelectionStrategy); @@ -28,6 +31,7 @@ public void VerifyChatExecutionSettingsDefault() [Fact] public void VerifyChatExecutionContinuationStrategyDefault() { + // Arrange Mock strategyMock = new(); AgentGroupChatSettings settings = new() @@ -35,6 +39,7 @@ public void VerifyChatExecutionContinuationStrategyDefault() TerminationStrategy = strategyMock.Object }; + // Assert Assert.Equal(strategyMock.Object, settings.TerminationStrategy); } @@ -44,6 +49,7 @@ public void VerifyChatExecutionContinuationStrategyDefault() [Fact] public void VerifyChatExecutionSelectionStrategyDefault() { + // Arrange Mock strategyMock = new(); AgentGroupChatSettings settings = new() @@ -51,6 +57,7 @@ public void VerifyChatExecutionSelectionStrategyDefault() SelectionStrategy = strategyMock.Object }; + // Assert Assert.NotNull(settings.SelectionStrategy); Assert.Equal(strategyMock.Object, settings.SelectionStrategy); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs index 6ad6fd75b18f..5af211c6cdf1 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs @@ -6,7 +6,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -22,7 +21,10 @@ public class AggregatorTerminationStrategyTests [Fact] public void VerifyAggregateTerminationStrategyInitialState() { + // Arrange AggregatorTerminationStrategy strategy = new(); + + // Assert Assert.Equal(AggregateTerminationCondition.All, strategy.Condition); } @@ -32,14 +34,16 @@ public void VerifyAggregateTerminationStrategyInitialState() [Fact] public async Task VerifyAggregateTerminationStrategyAnyAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMock = new(); + MockAgent agentMock = new(); + // Act and Assert await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockFalse) { Condition = AggregateTerminationCondition.Any, @@ -47,7 +51,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockFalse, strategyMockFalse) { Condition = AggregateTerminationCondition.Any, @@ -55,7 +59,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockTrue) { Condition = AggregateTerminationCondition.Any, @@ -68,14 +72,16 @@ await VerifyResultAsync( [Fact] public async Task VerifyAggregateTerminationStrategyAllAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMock = new(); + MockAgent agentMock = new(); + // Act and Assert await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockFalse) { Condition = AggregateTerminationCondition.All, @@ -83,7 +89,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockFalse, strategyMockFalse) { Condition = AggregateTerminationCondition.All, @@ -91,7 +97,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockTrue) { Condition = AggregateTerminationCondition.All, @@ -104,34 +110,39 @@ await VerifyResultAsync( [Fact] public async Task VerifyAggregateTerminationStrategyAgentAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMockA = new(); - Mock agentMockB = new(); + MockAgent agentMockA = new(); + MockAgent agentMockB = new(); + // Act and Assert await VerifyResultAsync( expectedResult: false, - agentMockB.Object, + agentMockB, new(strategyMockTrue, strategyMockTrue) { - Agents = [agentMockA.Object], + Agents = [agentMockA], Condition = AggregateTerminationCondition.All, }); await VerifyResultAsync( expectedResult: true, - agentMockB.Object, + agentMockB, new(strategyMockTrue, strategyMockTrue) { - Agents = [agentMockB.Object], + Agents = [agentMockB], Condition = AggregateTerminationCondition.All, }); } private static async Task VerifyResultAsync(bool expectedResult, Agent agent, AggregatorTerminationStrategy strategyRoot) { + // Act var result = await strategyRoot.ShouldTerminateAsync(agent, []); + + // Assert Assert.Equal(expectedResult, result); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs index 5b2453b47fe1..83cb9a3ea337 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs @@ -5,7 +5,6 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -21,27 +20,31 @@ public class KernelFunctionSelectionStrategyTests [Fact] public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() { - Mock mockAgent = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id)); + // Arrange + MockAgent mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id)); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - ResultParser = (result) => mockAgent.Object.Id, + ResultParser = (result) => mockAgent.Id, AgentsVariableName = "agents", HistoryVariableName = "history", }; + // Assert Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); Assert.NotEqual("agent", KernelFunctionSelectionStrategy.DefaultAgentsVariableName); Assert.NotEqual("history", KernelFunctionSelectionStrategy.DefaultHistoryVariableName); - Agent nextAgent = await strategy.NextAsync([mockAgent.Object], []); + // Act + Agent nextAgent = await strategy.NextAsync([mockAgent], []); + // Assert Assert.NotNull(nextAgent); - Assert.Equal(mockAgent.Object, nextAgent); + Assert.Equal(mockAgent, nextAgent); } /// @@ -50,17 +53,19 @@ public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() [Fact] public async Task VerifyKernelFunctionSelectionStrategyThrowsOnNullResultAsync() { - Mock mockAgent = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id)); + // Arrange + MockAgent mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id)); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } }, ResultParser = (result) => "larry", }; - await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); + // Act and Assert + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], [])); } /// @@ -69,17 +74,19 @@ public async Task VerifyKernelFunctionSelectionStrategyThrowsOnNullResultAsync() [Fact] public async Task VerifyKernelFunctionSelectionStrategyThrowsOnBadResultAsync() { - Mock mockAgent = new(); + // Arrange + MockAgent mockAgent = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin("")); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } }, ResultParser = (result) => result.GetValue() ?? null!, }; - await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); + // Act and Assert + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], [])); } private sealed class TestPlugin(string agentName) diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs index 36ef6e7e6e79..7ee5cf838bc3 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs @@ -3,10 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -22,6 +20,7 @@ public class KernelFunctionTerminationStrategyTests [Fact] public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync() { + // Arrange KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin()); KernelFunctionTerminationStrategy strategy = @@ -31,15 +30,16 @@ public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync() HistoryVariableName = "history", }; + // Assert Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); Assert.NotEqual("agent", KernelFunctionTerminationStrategy.DefaultAgentVariableName); Assert.NotEqual("history", KernelFunctionTerminationStrategy.DefaultHistoryVariableName); - Mock mockAgent = new(); - - bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + // Act + MockAgent mockAgent = new(); + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []); Assert.True(isTerminating); } @@ -59,9 +59,9 @@ public async Task VerifyKernelFunctionTerminationStrategyParsingAsync() ResultParser = (result) => string.Equals("test", result.GetValue(), StringComparison.OrdinalIgnoreCase) }; - Mock mockAgent = new(); + MockAgent mockAgent = new(); - bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []); Assert.True(isTerminating); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs index a1b739ae1d1e..196a89ded6e3 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs @@ -2,10 +2,8 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -13,7 +11,7 @@ namespace SemanticKernel.Agents.UnitTests.Core.Chat; /// /// Unit testing of . /// -public class RegexTerminationStrategyTests +public partial class RegexTerminationStrategyTests { /// /// Verify abililty of strategy to match expression. @@ -21,10 +19,12 @@ public class RegexTerminationStrategyTests [Fact] public async Task VerifyExpressionTerminationStrategyAsync() { + // Arrange RegexTerminationStrategy strategy = new("test"); - Regex r = new("(?:^|\\W)test(?:$|\\W)"); + Regex r = MyRegex(); + // Act and Assert await VerifyResultAsync( expectedResult: false, new(r), @@ -38,9 +38,17 @@ await VerifyResultAsync( private static async Task VerifyResultAsync(bool expectedResult, RegexTerminationStrategy strategyRoot, string content) { + // Arrange ChatMessageContent message = new(AuthorRole.Assistant, content); - Mock agent = new(); - var result = await strategyRoot.ShouldTerminateAsync(agent.Object, [message]); + MockAgent agent = new(); + + // Act + var result = await strategyRoot.ShouldTerminateAsync(agent, [message]); + + // Assert Assert.Equal(expectedResult, result); } + + [GeneratedRegex("(?:^|\\W)test(?:$|\\W)")] + private static partial Regex MyRegex(); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs index 04339a8309e4..8f7ff6b29d03 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs @@ -3,7 +3,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -19,28 +18,38 @@ public class SequentialSelectionStrategyTests [Fact] public async Task VerifySequentialSelectionStrategyTurnsAsync() { - Mock agent1 = new(); - Mock agent2 = new(); + // Arrange + MockAgent agent1 = new(); + MockAgent agent2 = new(); - Agent[] agents = [agent1.Object, agent2.Object]; + Agent[] agents = [agent1, agent2]; SequentialSelectionStrategy strategy = new(); - await VerifyNextAgent(agent1.Object); - await VerifyNextAgent(agent2.Object); - await VerifyNextAgent(agent1.Object); - await VerifyNextAgent(agent2.Object); - await VerifyNextAgent(agent1.Object); + // Act and Assert + await VerifyNextAgent(agent1); + await VerifyNextAgent(agent2); + await VerifyNextAgent(agent1); + await VerifyNextAgent(agent2); + await VerifyNextAgent(agent1); + // Arrange strategy.Reset(); - await VerifyNextAgent(agent1.Object); - // Verify index does not exceed current bounds. - agents = [agent1.Object]; - await VerifyNextAgent(agent1.Object); + // Act and Assert + await VerifyNextAgent(agent1); + + // Arrange: Verify index does not exceed current bounds. + agents = [agent1]; + + // Act and Assert + await VerifyNextAgent(agent1); async Task VerifyNextAgent(Agent agent1) { + // Act Agent? nextAgent = await strategy.NextAsync(agents, []); + + // Assert Assert.NotNull(nextAgent); Assert.Equal(agent1.Id, nextAgent.Id); } @@ -52,7 +61,10 @@ async Task VerifyNextAgent(Agent agent1) [Fact] public async Task VerifySequentialSelectionStrategyEmptyAsync() { + // Arrange SequentialSelectionStrategy strategy = new(); + + // Act and Assert await Assert.ThrowsAsync(() => strategy.NextAsync([], [])); } } diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs index f19453a74058..01debd8ded5f 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs @@ -23,6 +23,7 @@ public class ChatCompletionAgentTests [Fact] public void VerifyChatCompletionAgentDefinition() { + // Arrange ChatCompletionAgent agent = new() { @@ -31,6 +32,7 @@ public void VerifyChatCompletionAgentDefinition() Name = "test name", }; + // Assert Assert.NotNull(agent.Id); Assert.Equal("test instructions", agent.Instructions); Assert.Equal("test description", agent.Description); @@ -44,7 +46,8 @@ public void VerifyChatCompletionAgentDefinition() [Fact] public async Task VerifyChatCompletionAgentInvocationAsync() { - var mockService = new Mock(); + // Arrange + Mock mockService = new(); mockService.Setup( s => s.GetChatMessageContentsAsync( It.IsAny(), @@ -52,16 +55,18 @@ public async Task VerifyChatCompletionAgentInvocationAsync() It.IsAny(), It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]); - var agent = - new ChatCompletionAgent() + ChatCompletionAgent agent = + new() { Instructions = "test instructions", Kernel = CreateKernel(mockService.Object), Arguments = [], }; - var result = await agent.InvokeAsync([]).ToArrayAsync(); + // Act + ChatMessageContent[] result = await agent.InvokeAsync([]).ToArrayAsync(); + // Assert Assert.Single(result); mockService.Verify( @@ -80,13 +85,14 @@ public async Task VerifyChatCompletionAgentInvocationAsync() [Fact] public async Task VerifyChatCompletionAgentStreamingAsync() { + // Arrange StreamingChatMessageContent[] returnContent = [ new(AuthorRole.Assistant, "wh"), new(AuthorRole.Assistant, "at?"), ]; - var mockService = new Mock(); + Mock mockService = new(); mockService.Setup( s => s.GetStreamingChatMessageContentsAsync( It.IsAny(), @@ -94,16 +100,18 @@ public async Task VerifyChatCompletionAgentStreamingAsync() It.IsAny(), It.IsAny())).Returns(returnContent.ToAsyncEnumerable()); - var agent = - new ChatCompletionAgent() + ChatCompletionAgent agent = + new() { Instructions = "test instructions", Kernel = CreateKernel(mockService.Object), Arguments = [], }; - var result = await agent.InvokeStreamingAsync([]).ToArrayAsync(); + // Act + StreamingChatMessageContent[] result = await agent.InvokeStreamingAsync([]).ToArrayAsync(); + // Assert Assert.Equal(2, result.Length); mockService.Verify( @@ -122,15 +130,23 @@ public async Task VerifyChatCompletionAgentStreamingAsync() [Fact] public void VerifyChatCompletionServiceSelection() { + // Arrange Mock mockService = new(); Kernel kernel = CreateKernel(mockService.Object); + // Act (IChatCompletionService service, PromptExecutionSettings? settings) = ChatCompletionAgent.GetChatCompletionService(kernel, null); + // Assert + Assert.Equal(mockService.Object, service); Assert.Null(settings); + // Act (service, settings) = ChatCompletionAgent.GetChatCompletionService(kernel, []); + // Assert + Assert.Equal(mockService.Object, service); Assert.Null(settings); + // Act and Assert Assert.Throws(() => ChatCompletionAgent.GetChatCompletionService(kernel, new KernelArguments(new PromptExecutionSettings() { ServiceId = "anything" }))); } @@ -140,12 +156,14 @@ public void VerifyChatCompletionServiceSelection() [Fact] public void VerifyChatCompletionChannelKeys() { + // Arrange ChatCompletionAgent agent1 = new(); ChatCompletionAgent agent2 = new(); ChatCompletionAgent agent3 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; ChatCompletionAgent agent4 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; ChatCompletionAgent agent5 = new() { HistoryReducer = new ChatHistoryTruncationReducer(100) }; + // Act ans Assert Assert.Equal(agent1.GetChannelKeys(), agent2.GetChannelKeys()); Assert.Equal(agent3.GetChannelKeys(), agent4.GetChannelKeys()); Assert.NotEqual(agent1.GetChannelKeys(), agent3.GetChannelKeys()); diff --git a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs index 43aae918ad52..dfa9f59032c1 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core; @@ -22,21 +20,11 @@ public class ChatHistoryChannelTests [Fact] public async Task VerifyAgentWithoutIChatHistoryHandlerAsync() { - TestAgent agent = new(); // Not a IChatHistoryHandler + // Arrange + Mock agent = new(); // Not a IChatHistoryHandler ChatHistoryChannel channel = new(); // Requires IChatHistoryHandler - await Assert.ThrowsAsync(() => channel.InvokeAsync(agent).ToArrayAsync().AsTask()); - } - - private sealed class TestAgent : KernelAgent - { - protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - protected internal override IEnumerable GetChannelKeys() - { - throw new NotImplementedException(); - } + // Act & Assert + await Assert.ThrowsAsync(() => channel.InvokeAsync(agent.Object).ToArrayAsync().AsTask()); } } diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs index a75533474147..d9042305d9fa 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs @@ -30,8 +30,10 @@ public class ChatHistoryReducerExtensionsTests [InlineData(100, 0, int.MaxValue, 100)] public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? endIndex = null, int? expectedCount = null) { + // Arrange ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)]; + // Act ChatMessageContent[] extractedHistory = history.Extract(startIndex, endIndex).ToArray(); int finalIndex = endIndex ?? messageCount - 1; @@ -39,6 +41,7 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e expectedCount ??= finalIndex - startIndex + 1; + // Assert Assert.Equal(expectedCount, extractedHistory.Length); if (extractedHistory.Length > 0) @@ -58,16 +61,19 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e [InlineData(100, 0)] public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount) { + // Arrange ChatHistory summaries = [.. MockHistoryGenerator.CreateSimpleHistory(summaryCount)]; foreach (ChatMessageContent summary in summaries) { summary.Metadata = new Dictionary() { { "summary", true } }; } + // Act ChatHistory history = [.. summaries, .. MockHistoryGenerator.CreateSimpleHistory(regularCount)]; int finalSummaryIndex = history.LocateSummarizationBoundary("summary"); + // Assert Assert.Equal(summaryCount, finalSummaryIndex); } @@ -77,17 +83,22 @@ public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount) [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange ChatHistory history = []; + Mock mockReducer = new(); + mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null); + // Act bool isReduced = await history.ReduceAsync(null, default); + // Assert Assert.False(isReduced); Assert.Empty(history); - Mock mockReducer = new(); - mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null); + // Act isReduced = await history.ReduceAsync(mockReducer.Object, default); + // Assert Assert.False(isReduced); Assert.Empty(history); } @@ -98,13 +109,16 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange Mock mockReducer = new(); mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)[]); ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(10)]; + // Act bool isReduced = await history.ReduceAsync(mockReducer.Object, default); + // Assert Assert.True(isReduced); Assert.Empty(history); } @@ -124,11 +138,13 @@ public async Task VerifyChatHistoryReducedAsync() [InlineData(900, 500, int.MaxValue)] public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount, int? thresholdCount = null) { - // Shape of history doesn't matter since reduction is not expected + // Arrange: Shape of history doesn't matter since reduction is not expected ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.Equal(0, reductionIndex); } @@ -146,11 +162,13 @@ public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount [InlineData(1000, 500, 499)] public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCount, int? thresholdCount = null) { - // Generate history with only assistant messages + // Arrange: Generate history with only assistant messages ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); Assert.Equal(targetCount, messageCount - reductionIndex); } @@ -170,17 +188,20 @@ public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCoun [InlineData(1000, 500, 499)] public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int targetCount, int? thresholdCount = null) { - // Generate history with alternating user and assistant messages + // Arrange: Generate history with alternating user and assistant messages ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); - // The reduction length should align with a user message, if threshold is specified + // Act: The reduction length should align with a user message, if threshold is specified bool hasThreshold = thresholdCount > 0; int expectedCount = targetCount + (hasThreshold && sourceHistory[^targetCount].Role != AuthorRole.User ? 1 : 0); + // Assert Assert.Equal(expectedCount, messageCount - reductionIndex); } @@ -201,14 +222,16 @@ public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int ta [InlineData(9)] public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, int? thresholdCount = null) { - // Generate a history with function call on index 5 and 9 and + // Arrange: Generate a history with function call on index 5 and 9 and // function result on index 6 and 10 (total length: 14) ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithFunctionContent()]; ChatHistoryTruncationReducer reducer = new(targetCount, thresholdCount); + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); // The reduction length avoid splitting function call and result, regardless of threshold @@ -216,7 +239,7 @@ public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, i if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionCallContent)) { - expectedCount += 1; + expectedCount++; } else if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionResultContent)) { diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs index 7661cfcdf8cd..53e93d0026c3 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs @@ -25,8 +25,10 @@ public class ChatHistorySummarizationReducerTests [InlineData(int.MaxValue, -1)] public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); + // Act & Assert Assert.Throws(() => new ChatHistorySummarizationReducer(mockCompletionService.Object, targetCount, thresholdCount)); } @@ -36,13 +38,15 @@ public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdC [Fact] public void VerifyInitializationState() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + // Assert Assert.Equal(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions); Assert.True(reducer.FailOnError); + // Act reducer = new(mockCompletionService.Object, 10) { @@ -50,6 +54,7 @@ public void VerifyInitializationState() SummarizationInstructions = "instructions", }; + // Assert Assert.NotEqual(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions); Assert.False(reducer.FailOnError); } @@ -60,6 +65,7 @@ public void VerifyInitializationState() [Fact] public void VerifyEquality() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); ChatHistorySummarizationReducer reducer1 = new(mockCompletionService.Object, 3, 3); @@ -71,6 +77,7 @@ public void VerifyEquality() ChatHistorySummarizationReducer reducer7 = new(mockCompletionService.Object, 3); ChatHistorySummarizationReducer reducer8 = new(mockCompletionService.Object, 3); + // Assert Assert.True(reducer1.Equals(reducer1)); Assert.True(reducer1.Equals(reducer2)); Assert.True(reducer7.Equals(reducer8)); @@ -91,15 +98,18 @@ public void VerifyEquality() [Fact] public void VerifyHashCode() { + // Arrange HashSet reducers = []; Mock mockCompletionService = this.CreateMockCompletionService(); + // Act int hashCode1 = GenerateHashCode(3, 4); int hashCode2 = GenerateHashCode(33, 44); int hashCode3 = GenerateHashCode(3000, 4000); int hashCode4 = GenerateHashCode(3000, 4000); + // Assert Assert.NotEqual(hashCode1, hashCode2); Assert.NotEqual(hashCode2, hashCode3); Assert.Equal(hashCode3, hashCode4); @@ -121,12 +131,15 @@ int GenerateHashCode(int targetCount, int thresholdCount) [Fact] public async Task VerifyChatHistoryReductionSilentFailureAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(throwException: true); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10) { FailOnError = false }; + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -136,10 +149,12 @@ public async Task VerifyChatHistoryReductionSilentFailureAsync() [Fact] public async Task VerifyChatHistoryReductionThrowsOnFailureAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(throwException: true); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act and Assert await Assert.ThrowsAsync(() => reducer.ReduceAsync(sourceHistory)); } @@ -149,12 +164,15 @@ public async Task VerifyChatHistoryReductionThrowsOnFailureAsync() [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 20); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -164,12 +182,15 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11); VerifySummarization(messages[0]); } @@ -180,19 +201,24 @@ public async Task VerifyChatHistoryReducedAsync() [Fact] public async Task VerifyChatHistoryRereducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11); VerifySummarization(messages[0]); + // Act reducer = new(mockCompletionService.Object, 10) { UseSingleSummary = false }; reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert messages = VerifyReducedHistory(reducedHistory, 12); VerifySummarization(messages[0]); VerifySummarization(messages[1]); diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs index 27675420264c..9d8b2e721fdf 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs @@ -23,6 +23,7 @@ public class ChatHistoryTruncationReducerTests [InlineData(int.MaxValue, -1)] public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { + // Act and Assert Assert.Throws(() => new ChatHistoryTruncationReducer(targetCount, thresholdCount)); } @@ -32,6 +33,7 @@ public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdC [Fact] public void VerifyEquality() { + // Arrange ChatHistoryTruncationReducer reducer1 = new(3, 3); ChatHistoryTruncationReducer reducer2 = new(3, 3); ChatHistoryTruncationReducer reducer3 = new(4, 3); @@ -39,6 +41,7 @@ public void VerifyEquality() ChatHistoryTruncationReducer reducer5 = new(3); ChatHistoryTruncationReducer reducer6 = new(3); + // Assert Assert.True(reducer1.Equals(reducer1)); Assert.True(reducer1.Equals(reducer2)); Assert.True(reducer5.Equals(reducer6)); @@ -56,13 +59,16 @@ public void VerifyEquality() [Fact] public void VerifyHashCode() { + // Arrange HashSet reducers = []; + // Act int hashCode1 = GenerateHashCode(3, 4); int hashCode2 = GenerateHashCode(33, 44); int hashCode3 = GenerateHashCode(3000, 4000); int hashCode4 = GenerateHashCode(3000, 4000); + // Assert Assert.NotEqual(hashCode1, hashCode2); Assert.NotEqual(hashCode2, hashCode3); Assert.Equal(hashCode3, hashCode4); @@ -84,11 +90,14 @@ int GenerateHashCode(int targetCount, int thresholdCount) [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(10).ToArray(); - ChatHistoryTruncationReducer reducer = new(20); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -98,11 +107,14 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistoryTruncationReducer reducer = new(10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert VerifyReducedHistory(reducedHistory, 10); } @@ -112,12 +124,15 @@ public async Task VerifyChatHistoryReducedAsync() [Fact] public async Task VerifyChatHistoryRereducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistoryTruncationReducer reducer = new(10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert VerifyReducedHistory(reducedHistory, 10); } diff --git a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs index 14a938a7b169..d7f370e3734c 100644 --- a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs @@ -19,10 +19,12 @@ public class ChatHistoryExtensionsTests [Fact] public void VerifyChatHistoryOrdering() { + // Arrange ChatHistory history = []; history.AddUserMessage("Hi"); history.AddAssistantMessage("Hi"); + // Act and Assert VerifyRole(AuthorRole.User, history.First()); VerifyRole(AuthorRole.Assistant, history.Last()); @@ -36,10 +38,12 @@ public void VerifyChatHistoryOrdering() [Fact] public async Task VerifyChatHistoryOrderingAsync() { + // Arrange ChatHistory history = []; history.AddUserMessage("Hi"); history.AddAssistantMessage("Hi"); + // Act and Assert VerifyRole(AuthorRole.User, history.First()); VerifyRole(AuthorRole.Assistant, history.Last()); diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 452a0566e11f..96ed232fb109 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -22,8 +22,10 @@ public class BroadcastQueueTests [Fact] public void VerifyBroadcastQueueDefaultConfiguration() { + // Arrange BroadcastQueue queue = new(); + // Assert Assert.True(queue.BlockDuration.TotalSeconds > 0); } @@ -33,7 +35,7 @@ public void VerifyBroadcastQueueDefaultConfiguration() [Fact] public async Task VerifyBroadcastQueueReceiveAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -42,23 +44,31 @@ public async Task VerifyBroadcastQueueReceiveAsync() TestChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Verify initial state + // Act: Verify initial state await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify empty invocation with no channels. + // Act: Verify empty invocation with no channels. queue.Enqueue([], []); await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify empty invocation of channel. + // Act: Verify empty invocation of channel. queue.Enqueue([reference], []); await VerifyReceivingStateAsync(receiveCount: 1, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify expected invocation of channel. + // Act: Verify expected invocation of channel. queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]); await VerifyReceivingStateAsync(receiveCount: 2, queue, channel, "test"); + + // Assert Assert.NotEmpty(channel.ReceivedMessages); } @@ -68,7 +78,7 @@ public async Task VerifyBroadcastQueueReceiveAsync() [Fact] public async Task VerifyBroadcastQueueFailureAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -77,9 +87,10 @@ public async Task VerifyBroadcastQueueFailureAsync() BadChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Verify expected invocation of channel. + // Act: Verify expected invocation of channel. queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]); + // Assert await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); @@ -91,7 +102,7 @@ public async Task VerifyBroadcastQueueFailureAsync() [Fact] public async Task VerifyBroadcastQueueConcurrencyAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -100,7 +111,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() TestChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Enqueue multiple channels + // Act: Enqueue multiple channels for (int count = 0; count < 10; ++count) { queue.Enqueue([new(channel, $"test{count}")], [new ChatMessageContent(AuthorRole.User, "hi")]); @@ -112,7 +123,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() await queue.EnsureSynchronizedAsync(new ChannelReference(channel, $"test{count}")); } - // Verify result + // Assert Assert.NotEmpty(channel.ReceivedMessages); Assert.Equal(10, channel.ReceivedMessages.Count); } diff --git a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs index 0a9715f25115..13cc3203d58c 100644 --- a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs @@ -17,21 +17,24 @@ public class KeyEncoderTests [Fact] public void VerifyKeyEncoderUniqueness() { + // Act this.VerifyHashEquivalancy([]); this.VerifyHashEquivalancy(nameof(KeyEncoderTests)); this.VerifyHashEquivalancy(nameof(KeyEncoderTests), "http://localhost", "zoo"); - // Verify "well-known" value + // Assert: Verify "well-known" value string localHash = KeyEncoder.GenerateHash([typeof(ChatHistoryChannel).FullName!]); Assert.Equal("Vdx37EnWT9BS+kkCkEgFCg9uHvHNw1+hXMA4sgNMKs4=", localHash); } private void VerifyHashEquivalancy(params string[] keys) { + // Act string hash1 = KeyEncoder.GenerateHash(keys); string hash2 = KeyEncoder.GenerateHash(keys); string hash3 = KeyEncoder.GenerateHash(keys.Concat(["another"])); + // Assert Assert.Equal(hash1, hash2); Assert.NotEqual(hash1, hash3); } diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index 51fbf36c6ab4..2535446dae7b 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -15,7 +15,7 @@ namespace SemanticKernel.Agents.UnitTests; /// /// Mock definition of with a contract. /// -internal sealed class MockAgent : KernelAgent, IChatHistoryHandler +internal class MockAgent : KernelAgent, IChatHistoryHandler { public int InvokeCount { get; private set; } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs index 3c2945ad0fb9..6288c6a5aed8 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs @@ -18,14 +18,17 @@ public class AddHeaderRequestPolicyTests [Fact] public void VerifyAddHeaderRequestPolicyExecution() { + // Arrange using HttpClientTransport clientTransport = new(); HttpPipeline pipeline = new(clientTransport); HttpMessage message = pipeline.CreateMessage(); - AddHeaderRequestPolicy policy = new(headerName: "testname", headerValue: "testvalue"); + + // Act policy.OnSendingRequest(message); + // Assert Assert.Single(message.Request.Headers); HttpHeader header = message.Request.Headers.Single(); Assert.Equal("testname", header.Name); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs index 997596796be1..97dbf32903d6 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs @@ -29,7 +29,10 @@ public void VerifyToMessageRole() private void VerifyRoleConversion(AuthorRole inputRole, MessageRole expectedRole) { + // Arrange MessageRole convertedRole = inputRole.ToMessageRole(); + + // Assert Assert.Equal(expectedRole, convertedRole); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs index 3f982f3a7b47..70c27ccb2152 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs @@ -17,11 +17,15 @@ public class KernelExtensionsTests [Fact] public void VerifyGetKernelFunctionLookup() { + // Arrange Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); + // Act KernelFunction function = kernel.GetKernelFunction($"{nameof(TestPlugin)}-{nameof(TestPlugin.TestFunction)}", '-'); + + // Assert Assert.NotNull(function); Assert.Equal(nameof(TestPlugin.TestFunction), function.Name); } @@ -32,10 +36,12 @@ public void VerifyGetKernelFunctionLookup() [Fact] public void VerifyGetKernelFunctionInvalid() { + // Arrange Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); + // Act and Assert Assert.Throws(() => kernel.GetKernelFunction("a", '-')); Assert.Throws(() => kernel.GetKernelFunction("a-b", ':')); Assert.Throws(() => kernel.GetKernelFunction("a-b-c", '-')); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs index 6d690e909457..acf195840366 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs @@ -19,17 +19,27 @@ public class KernelFunctionExtensionsTests [Fact] public void VerifyKernelFunctionToFunctionTool() { + // Arrange KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + + // Assert Assert.Equal(2, plugin.FunctionCount); + // Arrange KernelFunction f1 = plugin[nameof(TestPlugin.TestFunction1)]; KernelFunction f2 = plugin[nameof(TestPlugin.TestFunction2)]; + // Act FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin"); + + // Assert Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition1.Description); + // Act FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin"); + + // Assert Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition2.Description); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs index 5d0e01ba9926..50dec2cb95ae 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -24,7 +24,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsDefault() // Arrange (Setup message with null metadata) ChatMessageContent message = new(AuthorRole.User, "test"); - // Act + // Act: Create options MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); // Assert @@ -38,17 +38,17 @@ public void VerifyAssistantMessageAdapterCreateOptionsDefault() [Fact] public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty() { - // Setup message with empty metadata + // Arrange Setup message with empty metadata ChatMessageContent message = new(AuthorRole.User, "test") { Metadata = new Dictionary() }; - // Create options + // Act: Create options MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); - // Validate + // Assert Assert.NotNull(options); Assert.Empty(options.Metadata); } @@ -59,7 +59,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty() [Fact] public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() { - // Setup message with metadata + // Arrange: Setup message with metadata ChatMessageContent message = new(AuthorRole.User, "test") { @@ -71,10 +71,10 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() } }; - // Create options + // Act: Create options MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); - // Validate + // Assert Assert.NotNull(options); Assert.NotEmpty(options.Metadata); Assert.Equal(2, options.Metadata.Count); @@ -88,7 +88,7 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() [Fact] public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() { - // Setup message with null metadata value + // Arrange: Setup message with null metadata value ChatMessageContent message = new(AuthorRole.User, "test") { @@ -100,10 +100,10 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() } }; - // Create options + // Act: Create options MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); - // Validate + // Assert Assert.NotNull(options); Assert.NotEmpty(options.Metadata); Assert.Equal(2, options.Metadata.Count); @@ -117,8 +117,13 @@ public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() [Fact] public void VerifyAssistantMessageAdapterGetMessageContentsWithText() { + // Arrange ChatMessageContent message = new(AuthorRole.User, items: [new TextContent("test")]); + + // Act MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().Text); @@ -130,8 +135,13 @@ public void VerifyAssistantMessageAdapterGetMessageContentsWithText() [Fact] public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() { + // Arrange ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new Uri("https://localhost/myimage.png"))]); + + // Act MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageUrl); @@ -143,8 +153,13 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() [Fact(Skip = "API bug with data Uri construction")] public void VerifyAssistantMessageAdapterGetMessageWithImageData() { + // Arrange ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]); + + // Act MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageUrl); @@ -156,8 +171,13 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageData() [Fact] public void VerifyAssistantMessageAdapterGetMessageWithImageFile() { + // Arrange ChatMessageContent message = new(AuthorRole.User, items: [new FileReferenceContent("file-id")]); + + // Act MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert Assert.NotNull(contents); Assert.Single(contents); Assert.NotNull(contents.Single().ImageFileId); @@ -169,6 +189,7 @@ public void VerifyAssistantMessageAdapterGetMessageWithImageFile() [Fact] public void VerifyAssistantMessageAdapterGetMessageWithAll() { + // Arrange ChatMessageContent message = new( AuthorRole.User, @@ -178,7 +199,11 @@ public void VerifyAssistantMessageAdapterGetMessageWithAll() new ImageContent(new Uri("https://localhost/myimage.png")), new FileReferenceContent("file-id") ]); + + // Act MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert Assert.NotNull(contents); Assert.Equal(3, contents.Length); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs index 704cf0252852..d6bcf91b8a94 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs @@ -18,13 +18,17 @@ public class AssistantRunOptionsFactoryTests [Fact] public void AssistantRunOptionsFactoryExecutionOptionsNullTest() { + // Arrange OpenAIAssistantDefinition definition = new("gpt-anything") { Temperature = 0.5F, }; + // Act RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, null); + + // Assert Assert.NotNull(options); Assert.Null(options.Temperature); Assert.Null(options.NucleusSamplingFactor); @@ -37,6 +41,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsNullTest() [Fact] public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() { + // Arrange OpenAIAssistantDefinition definition = new("gpt-anything") { @@ -49,7 +54,10 @@ public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() Temperature = 0.5F, }; + // Act RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + // Assert Assert.NotNull(options); Assert.Null(options.Temperature); Assert.Null(options.NucleusSamplingFactor); @@ -61,6 +69,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() [Fact] public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() { + // Arrange OpenAIAssistantDefinition definition = new("gpt-anything") { @@ -80,7 +89,10 @@ public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() EnableJsonResponse = true, }; + // Act RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + // Assert Assert.NotNull(options); Assert.Equal(0.9F, options.Temperature); Assert.Equal(8, options.TruncationStrategy.LastMessages); @@ -94,6 +106,7 @@ public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() [Fact] public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest() { + // Arrange OpenAIAssistantDefinition definition = new("gpt-anything") { @@ -115,8 +128,10 @@ public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest() }, }; + // Act RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + // Assert Assert.Equal(2, options.Metadata.Count); Assert.Equal("value", options.Metadata["key1"]); Assert.Equal(string.Empty, options.Metadata["key2"]); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index bee75be9d80c..ef67c48f1473 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -32,8 +32,10 @@ public sealed class OpenAIAssistantAgentTests : IDisposable [Fact] public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel"); + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -44,6 +46,7 @@ public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -52,6 +55,7 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() Instructions = "testinstructions", }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -62,12 +66,14 @@ public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { EnableCodeInterpreter = true, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -78,6 +84,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -85,6 +92,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn CodeInterpreterFileIds = ["file1", "file2"], }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -95,12 +103,14 @@ public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsyn [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { EnableFileSearch = true, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -111,6 +121,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -118,6 +129,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() VectorStoreId = "#vs1", }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -128,6 +140,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -138,6 +151,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() }, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -148,12 +162,14 @@ public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { EnableJsonResponse = true, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -164,12 +180,14 @@ public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { Temperature = 2.0F, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -180,12 +198,14 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { TopP = 2.0F, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -196,12 +216,14 @@ public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { ExecutionOptions = new OpenAIAssistantExecutionOptions(), }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -212,6 +234,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsy [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -223,6 +246,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() } }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -233,6 +257,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAndMetadataAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -244,6 +269,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAnd }, }; + // Act and Assert await this.VerifyAgentCreationAsync(definition); } @@ -253,6 +279,7 @@ public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAnd [Fact] public async Task VerifyOpenAIAssistantAgentRetrievalAsync() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel"); this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); @@ -263,6 +290,7 @@ await OpenAIAssistantAgent.RetrieveAsync( this.CreateTestConfiguration(), "#id"); + // Act and Assert ValidateAgentDefinition(agent, definition); } @@ -272,17 +300,23 @@ await OpenAIAssistantAgent.RetrieveAsync( [Fact] public async Task VerifyOpenAIAssistantAgentDeleteAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); + // Assert Assert.False(agent.IsDeleted); + // Arrange this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteAgent); + // Act await agent.DeleteAsync(); + // Assert Assert.True(agent.IsDeleted); + // Act await agent.DeleteAsync(); // Doesn't throw + // Assert Assert.True(agent.IsDeleted); - await Assert.ThrowsAsync(() => agent.AddChatMessageAsync("threadid", new(AuthorRole.User, "test"))); await Assert.ThrowsAsync(() => agent.InvokeAsync("threadid").ToArrayAsync().AsTask()); } @@ -293,16 +327,22 @@ public async Task VerifyOpenAIAssistantAgentDeleteAsync() [Fact] public async Task VerifyOpenAIAssistantAgentCreateThreadAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + // Act string threadId = await agent.CreateThreadAsync(); + // Assert Assert.NotNull(threadId); + // Arrange this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + // Act threadId = await agent.CreateThreadAsync(new()); + // Assert Assert.NotNull(threadId); } @@ -312,6 +352,7 @@ public async Task VerifyOpenAIAssistantAgentCreateThreadAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -323,7 +364,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); @@ -335,6 +380,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -346,7 +392,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() ResponseContent.GetTextMessageWithAnnotation); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Equal(2, messages[0].Items.Count); Assert.NotNull(messages[0].Items.SingleOrDefault(c => c is TextContent)); @@ -359,6 +409,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -370,7 +421,11 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() ResponseContent.GetImageMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); @@ -382,7 +437,7 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() [Fact] public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() { - // Create agent + // Arrange: Create agent OpenAIAssistantAgent agent = await this.CreateAgentAsync(); // Initialize agent channel @@ -395,18 +450,22 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + // Assert Assert.Single(messages); - // Setup messages + // Arrange: Setup messages this.SetupResponses( HttpStatusCode.OK, ResponseContent.ListMessagesPageMore, ResponseContent.ListMessagesPageMore, ResponseContent.ListMessagesPageFinal); - // Get messages and verify + // Act: Get messages messages = await chat.GetChatMessagesAsync(agent).ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); } @@ -416,7 +475,7 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() [Fact] public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() { - // Create agent + // Arrange: Create agent OpenAIAssistantAgent agent = await this.CreateAgentAsync(); // Initialize agent channel @@ -428,12 +487,18 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() ResponseContent.MessageSteps, ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + // Assert Assert.Single(messages); + // Arrange chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi")); + // Act messages = await chat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(2, messages.Length); } @@ -443,6 +508,7 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() [Fact] public async Task VerifyOpenAIAssistantAgentListDefinitionAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -451,19 +517,24 @@ public async Task VerifyOpenAIAssistantAgentListDefinitionAsync() ResponseContent.ListAgentsPageMore, ResponseContent.ListAgentsPageFinal); + // Act var messages = await OpenAIAssistantAgent.ListDefinitionsAsync( this.CreateTestConfiguration()).ToArrayAsync(); + // Assert Assert.Equal(7, messages.Length); + // Arrange this.SetupResponses( HttpStatusCode.OK, ResponseContent.ListAgentsPageMore, ResponseContent.ListAgentsPageFinal); + // Act messages = await OpenAIAssistantAgent.ListDefinitionsAsync( this.CreateTestConfiguration()).ToArrayAsync(); + // Assert Assert.Equal(4, messages.Length); } @@ -473,6 +544,7 @@ await OpenAIAssistantAgent.ListDefinitionsAsync( [Fact] public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); @@ -490,7 +562,11 @@ public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index a13261b58fdf..f8547f375f13 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -17,8 +17,10 @@ public class OpenAIAssistantDefinitionTests [Fact] public void VerifyOpenAIAssistantDefinitionInitialState() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel"); + // Assert Assert.Equal(string.Empty, definition.Id); Assert.Equal("testmodel", definition.ModelId); Assert.Null(definition.Name); @@ -34,6 +36,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() Assert.False(definition.EnableCodeInterpreter); Assert.False(definition.EnableJsonResponse); + // Act and Assert ValidateSerialization(definition); } @@ -43,6 +46,7 @@ public void VerifyOpenAIAssistantDefinitionInitialState() [Fact] public void VerifyOpenAIAssistantDefinitionAssignment() { + // Arrange OpenAIAssistantDefinition definition = new("testmodel") { @@ -68,6 +72,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() EnableJsonResponse = true, }; + // Assert Assert.Equal("testid", definition.Id); Assert.Equal("testname", definition.Name); Assert.Equal("testmodel", definition.ModelId); @@ -87,6 +92,7 @@ public void VerifyOpenAIAssistantDefinitionAssignment() Assert.True(definition.EnableCodeInterpreter); Assert.True(definition.EnableJsonResponse); + // Act and Assert ValidateSerialization(definition); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs index 692dee85f1aa..99cbe012f183 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs @@ -17,8 +17,10 @@ public class OpenAIAssistantInvocationOptionsTests [Fact] public void OpenAIAssistantInvocationOptionsInitialState() { + // Arrange OpenAIAssistantInvocationOptions options = new(); + // Assert Assert.Null(options.ModelName); Assert.Null(options.Metadata); Assert.Null(options.Temperature); @@ -31,6 +33,7 @@ public void OpenAIAssistantInvocationOptionsInitialState() Assert.False(options.EnableCodeInterpreter); Assert.False(options.EnableFileSearch); + // Act and Assert ValidateSerialization(options); } @@ -40,6 +43,7 @@ public void OpenAIAssistantInvocationOptionsInitialState() [Fact] public void OpenAIAssistantInvocationOptionsAssignment() { + // Arrange OpenAIAssistantInvocationOptions options = new() { @@ -56,6 +60,7 @@ public void OpenAIAssistantInvocationOptionsAssignment() EnableFileSearch = true, }; + // Assert Assert.Equal("testmodel", options.ModelName); Assert.Equal(2, options.Temperature); Assert.Equal(0, options.TopP); @@ -68,15 +73,18 @@ public void OpenAIAssistantInvocationOptionsAssignment() Assert.True(options.EnableJsonResponse); Assert.True(options.EnableFileSearch); + // Act and Assert ValidateSerialization(options); } private static void ValidateSerialization(OpenAIAssistantInvocationOptions source) { + // Act string json = JsonSerializer.Serialize(source); OpenAIAssistantInvocationOptions? target = JsonSerializer.Deserialize(json); + // Assert Assert.NotNull(target); Assert.Equal(source.ModelName, target.ModelName); Assert.Equal(source.Temperature, target.Temperature); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs index dfb033f31d3c..7799eb26c305 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs @@ -19,7 +19,10 @@ public class OpenAIClientProviderTests [Fact] public void VerifyOpenAIClientFactoryTargetAzureByKey() { + // Arrange OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI("key", new Uri("https://localhost")); + + // Assert Assert.NotNull(provider.Client); } @@ -29,8 +32,11 @@ public void VerifyOpenAIClientFactoryTargetAzureByKey() [Fact] public void VerifyOpenAIClientFactoryTargetAzureByCredential() { + // Arrange Mock mockCredential = new(); OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost")); + + // Assert Assert.NotNull(provider.Client); } @@ -42,7 +48,10 @@ public void VerifyOpenAIClientFactoryTargetAzureByCredential() [InlineData("http://myproxy:9819")] public void VerifyOpenAIClientFactoryTargetOpenAINoKey(string? endpoint) { + // Arrange OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(endpoint != null ? new Uri(endpoint) : null); + + // Assert Assert.NotNull(provider.Client); } @@ -54,7 +63,10 @@ public void VerifyOpenAIClientFactoryTargetOpenAINoKey(string? endpoint) [InlineData("key", "http://myproxy:9819")] public void VerifyOpenAIClientFactoryTargetOpenAIByKey(string key, string? endpoint) { + // Arrange OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null); + + // Assert Assert.NotNull(provider.Client); } @@ -64,8 +76,11 @@ public void VerifyOpenAIClientFactoryTargetOpenAIByKey(string key, string? endpo [Fact] public void VerifyOpenAIClientFactoryWithHttpClient() { + // Arrange using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") }; OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(httpClient: httpClient); + + // Assert Assert.NotNull(provider.Client); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs index 496f429f0793..1689bec1f828 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -19,13 +19,16 @@ public class OpenAIThreadCreationOptionsTests [Fact] public void OpenAIThreadCreationOptionsInitialState() { + // Arrange OpenAIThreadCreationOptions options = new(); + // Assert Assert.Null(options.Messages); Assert.Null(options.Metadata); Assert.Null(options.VectorStoreId); Assert.Null(options.CodeInterpreterFileIds); + // Act and Assert ValidateSerialization(options); } @@ -35,6 +38,7 @@ public void OpenAIThreadCreationOptionsInitialState() [Fact] public void OpenAIThreadCreationOptionsAssignment() { + // Arrange OpenAIThreadCreationOptions options = new() { @@ -44,20 +48,24 @@ public void OpenAIThreadCreationOptionsAssignment() CodeInterpreterFileIds = ["file1"], }; + // Assert Assert.Single(options.Messages); Assert.Single(options.Metadata); Assert.Equal("#vs", options.VectorStoreId); Assert.Single(options.CodeInterpreterFileIds); + // Act and Assert ValidateSerialization(options); } private static void ValidateSerialization(OpenAIThreadCreationOptions source) { + // Act string json = JsonSerializer.Serialize(source); OpenAIThreadCreationOptions? target = JsonSerializer.Deserialize(json); + // Assert Assert.NotNull(target); Assert.Equal(source.VectorStoreId, target.VectorStoreId); AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs index 9ec3567c0987..e75a962dfc5e 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs @@ -16,8 +16,10 @@ public class RunPollingOptionsTests [Fact] public void RunPollingOptionsInitialStateTest() { + // Arrange RunPollingOptions options = new(); + // Assert Assert.Equal(RunPollingOptions.DefaultPollingInterval, options.RunPollingInterval); Assert.Equal(RunPollingOptions.DefaultPollingBackoff, options.RunPollingBackoff); Assert.Equal(RunPollingOptions.DefaultMessageSynchronizationDelay, options.MessageSynchronizationDelay); @@ -30,6 +32,7 @@ public void RunPollingOptionsInitialStateTest() [Fact] public void RunPollingOptionsAssignmentTest() { + // Arrange RunPollingOptions options = new() { @@ -39,6 +42,7 @@ public void RunPollingOptionsAssignmentTest() MessageSynchronizationDelay = TimeSpan.FromSeconds(5), }; + // Assert Assert.Equal(3, options.RunPollingInterval.TotalSeconds); Assert.Equal(4, options.RunPollingBackoff.TotalSeconds); Assert.Equal(5, options.MessageSynchronizationDelay.TotalSeconds); @@ -51,6 +55,7 @@ public void RunPollingOptionsAssignmentTest() [Fact] public void RunPollingOptionsGetIntervalTest() { + // Arrange RunPollingOptions options = new() { @@ -59,6 +64,7 @@ public void RunPollingOptionsGetIntervalTest() RunPollingBackoffThreshold = 8, }; + // Assert Assert.Equal(options.RunPollingInterval, options.GetPollingInterval(8)); Assert.Equal(options.RunPollingBackoff, options.GetPollingInterval(9)); } From 5463d1b654ff41533c4ce7ed38ee0e93b649ebdc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 Aug 2024 12:29:48 -0700 Subject: [PATCH 188/226] Summary reducer optimization --- .../src/Agents/Core/History/ChatHistorySummarizationReducer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs index 4f909e7e146d..8c2f022830d1 100644 --- a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs +++ b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs @@ -80,7 +80,7 @@ Provide a concise and complete summarizion of the entire dialog that does not ex IEnumerable summarizedHistory = history.Extract( this.UseSingleSummary ? 0 : insertionPoint, - truncationIndex, + truncationIndex - 1, (m) => m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)); try From 2cfadc737a907619addfa847761e9594202a0ae6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Fri, 9 Aug 2024 17:36:36 -0700 Subject: [PATCH 189/226] Checkpoint --- dotnet/SK-dotnet.sln | 9 + .../AssistantStreaming.csproj | 24 ++ .../Demos/AssistantStreaming/Program.cs | 58 ++++ .../src/Agents/Abstractions/AgentChannel.cs | 42 +++ dotnet/src/Agents/Abstractions/AgentChat.cs | 90 ++++-- .../Agents/Abstractions/AggregatorChannel.cs | 7 + dotnet/src/Agents/Core/AgentGroupChat.cs | 51 ++-- dotnet/src/Agents/Core/ChatHistoryChannel.cs | 19 ++ .../OpenAI/Internal/AssistantThreadActions.cs | 273 +++++++++++++++--- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 50 ++++ .../Agents/OpenAI/OpenAIAssistantChannel.cs | 7 + .../src/Agents/UnitTests/AgentChannelTests.cs | 44 +-- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 5 + .../UnitTests/Core/AgentGroupChatTests.cs | 7 +- .../UnitTests/Internal/BroadcastQueueTests.cs | 57 +--- dotnet/src/Agents/UnitTests/MockAgent.cs | 2 +- dotnet/src/Agents/UnitTests/MockChannel.cs | 64 ++++ 17 files changed, 632 insertions(+), 177 deletions(-) create mode 100644 dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj create mode 100644 dotnet/samples/Demos/AssistantStreaming/Program.cs create mode 100644 dotnet/src/Agents/UnitTests/MockChannel.cs diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index b4580b4d1146..f95684827d42 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -364,6 +364,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AssistantStreaming", "samples\Demos\AssistantStreaming\AssistantStreaming.csproj", "{47938892-EB61-409D-AC4B-FBAC1BE0EE32}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -875,6 +877,12 @@ Global {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.Build.0 = Debug|Any CPU {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.ActiveCfg = Release|Any CPU {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.Build.0 = Release|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Debug|Any CPU.Build.0 = Debug|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Publish|Any CPU.Build.0 = Debug|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Release|Any CPU.ActiveCfg = Release|Any CPU + {47938892-EB61-409D-AC4B-FBAC1BE0EE32}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -997,6 +1005,7 @@ Global {EE454832-085F-4D37-B19B-F94F7FC6984A} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} {5C6C30E0-7AC1-47F4-8244-57B066B43FD8} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} + {47938892-EB61-409D-AC4B-FBAC1BE0EE32} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj b/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj new file mode 100644 index 000000000000..e4926c923e6e --- /dev/null +++ b/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj @@ -0,0 +1,24 @@ + + + + Exe + net8.0 + enable + enable + 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 + + + + + + + + + + + + + + + diff --git a/dotnet/samples/Demos/AssistantStreaming/Program.cs b/dotnet/samples/Demos/AssistantStreaming/Program.cs new file mode 100644 index 000000000000..c6953894f751 --- /dev/null +++ b/dotnet/samples/Demos/AssistantStreaming/Program.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft. All rights reserved. + +using System.Text; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +var configuration = new ConfigurationBuilder() + .AddUserSecrets() + .AddEnvironmentVariables() + .Build(); + +string? apiKey = configuration["OpenAI:ApiKey"]; +string? modelId = configuration["OpenAI:ChatModelId"]; + +// Logger for program scope +ILogger logger = NullLogger.Instance; + +ArgumentNullException.ThrowIfNull(apiKey); +ArgumentNullException.ThrowIfNull(modelId); + +OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + OpenAIClientProvider.ForOpenAI(apiKey), + new(modelId)); + +string threadId = await agent.CreateThreadAsync(); + +try +{ + ChatHistory messages = []; + while (true) + { + Console.Write("\nUser: "); + var input = Console.ReadLine(); + if (string.IsNullOrWhiteSpace(input)) { break; } + + await agent.AddChatMessageAsync(threadId, new(AuthorRole.User, input)); + + Console.Write("\nAssistant: "); + + await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(threadId, messages)) + { + Console.Write(content.Content); + } + + Console.WriteLine(); + } +} +finally +{ + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); +} diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 73469ed723b5..721b1bd11231 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents; @@ -39,6 +40,18 @@ public abstract class AgentChannel Agent agent, CancellationToken cancellationToken = default); + /// + /// Perform a discrete incremental interaction between a single and with streaming results. + /// + /// The agent actively interacting with the chat. + /// The reciever for the completed messages generated + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of streaming messages. + protected internal abstract IAsyncEnumerable InvokeStreamingAsync( + Agent agent, + ChatHistory messages, // %%% IList ??? + CancellationToken cancellationToken = default); + /// /// Retrieve the message history specific to this channel. /// @@ -83,4 +96,33 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent return this.InvokeAsync((TAgent)agent, cancellationToken); } + /// + /// Process a discrete incremental interaction between a single an a . + /// + /// The agent actively interacting with the chat. + /// The reciever for the completed messages generated + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. + /// + protected internal abstract IAsyncEnumerable InvokeStreamingAsync( + TAgent agent, + ChatHistory messages, + CancellationToken cancellationToken = default); + + /// + protected internal override IAsyncEnumerable InvokeStreamingAsync( + Agent agent, + ChatHistory messages, + CancellationToken cancellationToken = default) + { + if (agent.GetType() != typeof(TAgent)) + { + throw new KernelException($"Invalid agent channel: {typeof(TAgent).Name}/{agent.GetType().Name}"); + } + + return this.InvokeStreamingAsync((TAgent)agent, messages, cancellationToken); + } } diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index ca6cbdaab259..78a8d6470920 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -56,6 +56,13 @@ public abstract class AgentChat /// Asynchronous enumeration of messages. public abstract IAsyncEnumerable InvokeAsync(CancellationToken cancellationToken = default); + /// + /// Process a series of interactions between the agents participating in this chat. + /// + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + public abstract IAsyncEnumerable InvokeStreamingAsync(CancellationToken cancellationToken = default); + /// /// Retrieve the chat history. /// @@ -205,7 +212,7 @@ protected async IAsyncEnumerable InvokeAgentAsync( { // Get or create the required channel and block until channel is synchronized. // Will throw exception when propagating a processing failure. - AgentChannel channel = await GetOrCreateChannelAsync().ConfigureAwait(false); + AgentChannel channel = await this.GetOrCreateChannelAsync(agent, cancellationToken).ConfigureAwait(false); // Invoke agent & process response List messages = []; @@ -240,29 +247,54 @@ protected async IAsyncEnumerable InvokeAgentAsync( { this.ClearActivitySignal(); // Signal activity hash completed } + } - async Task GetOrCreateChannelAsync() - { - string channelKey = this.GetAgentHash(agent); - AgentChannel? channel = await this.SynchronizeChannelAsync(channelKey, cancellationToken).ConfigureAwait(false); - if (channel is null) - { - this.Logger.LogAgentChatCreatingChannel(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); + /// + /// Process a discrete incremental interaction between a single an a . + /// + /// The agent actively interacting with the chat. + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// Any instance does not support concurrent invocation and + /// will throw exception if concurrent activity is attempted. + /// + protected async IAsyncEnumerable InvokeStreamingAgentAsync( + Agent agent, + [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this.SetActivityOrThrow(); // Disallow concurrent access to chat history - channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false); + this.Logger.LogAgentChatInvokingAgent(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); - this._agentChannels.Add(channelKey, channel); + try + { + // Get or create the required channel and block until channel is synchronized. + // Will throw exception when propagating a processing failure. + AgentChannel channel = await this.GetOrCreateChannelAsync(agent, cancellationToken).ConfigureAwait(false); - if (this.History.Count > 0) - { - // Sync channel with existing history - await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false); - } + // Invoke agent & process response + ChatHistory messages = []; - this.Logger.LogAgentChatCreatedChannel(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); + await foreach (StreamingChatMessageContent streamingContent in channel.InvokeStreamingAsync(agent, messages, cancellationToken).ConfigureAwait(false)) + { + //this.Logger.LogAgentChatInvokedAgentMessage(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, message); // %%% LOGGING + yield return streamingContent; } - return channel; + // Broadcast message to other channels (in parallel) + // Note: Able to queue messages without synchronizing channels. + var channelRefs = + this._agentChannels + .Where(kvp => kvp.Value != channel) + .Select(kvp => new ChannelReference(kvp.Value, kvp.Key)); + this._broadcastQueue.Enqueue(channelRefs, messages); + + this.Logger.LogAgentChatInvokedAgent(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); + } + finally + { + this.ClearActivitySignal(); // Signal activity hash completed } } @@ -308,6 +340,30 @@ private string GetAgentHash(Agent agent) return hash; } + private async Task GetOrCreateChannelAsync(Agent agent, CancellationToken cancellationToken) + { + string channelKey = this.GetAgentHash(agent); + AgentChannel? channel = await this.SynchronizeChannelAsync(channelKey, cancellationToken).ConfigureAwait(false); + if (channel is null) + { + this.Logger.LogAgentChatCreatingChannel(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); + + channel = await agent.CreateChannelAsync(cancellationToken).ConfigureAwait(false); + + this._agentChannels.Add(channelKey, channel); + + if (this.History.Count > 0) + { + // Sync channel with existing history + await channel.ReceiveAsync(this.History, cancellationToken).ConfigureAwait(false); + } + + this.Logger.LogAgentChatCreatedChannel(nameof(InvokeAgentAsync), agent.GetType(), agent.Id); + } + + return channel; + } + private async Task SynchronizeChannelAsync(string channelKey, CancellationToken cancellationToken) { if (this._agentChannels.TryGetValue(channelKey, out AgentChannel? channel)) diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 0c6bc252891d..7b7662416124 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -3,6 +3,7 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; +using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents; @@ -49,6 +50,12 @@ protected internal override IAsyncEnumerable GetHistoryAsync } } + /// + protected internal override IAsyncEnumerable InvokeStreamingAsync(AggregatorAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); // %%% TODO + } + /// protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs index 928326745b97..33ca3f25ad0b 100644 --- a/dotnet/src/Agents/Core/AgentGroupChat.cs +++ b/dotnet/src/Agents/Core/AgentGroupChat.cs @@ -53,7 +53,7 @@ public void AddAgent(Agent agent) /// The interactions will proceed according to the and the /// defined via . /// In the absence of an , this method will not invoke any agents. - /// Any agent may be explicitly selected by calling . + /// Any agent may be explicitly selected by calling . /// /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. @@ -113,6 +113,17 @@ public override async IAsyncEnumerable InvokeAsync([Enumerat this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } + /// + /// %%% + /// + /// + /// + /// + public override IAsyncEnumerable InvokeStreamingAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); // %%% TODO + } + /// /// Process a single interaction between a given an a . /// @@ -122,35 +133,17 @@ public override async IAsyncEnumerable InvokeAsync([Enumerat /// /// Specified agent joins the chat. /// > - public IAsyncEnumerable InvokeAsync( - Agent agent, - CancellationToken cancellationToken = default) => - this.InvokeAsync(agent, isJoining: true, cancellationToken); - - /// - /// Process a single interaction between a given an a irregardless of - /// the defined via . Likewise, this does - /// not regard as it only takes a single turn for the specified agent. - /// - /// The agent actively interacting with the chat. - /// Optional flag to control if agent is joining the chat. - /// The to monitor for cancellation requests. The default is . - /// Asynchronous enumeration of messages. public async IAsyncEnumerable InvokeAsync( Agent agent, - bool isJoining, [EnumeratorCancellation] CancellationToken cancellationToken = default) { this.EnsureStrategyLoggerAssignment(); this.Logger.LogAgentGroupChatInvokingAgent(nameof(InvokeAsync), agent.GetType(), agent.Id); - if (isJoining) - { - this.AddAgent(agent); - } + this.AddAgent(agent); // %%% RECTIFY WITH SERIALIATION - await foreach (var message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false)) + await foreach (ChatMessageContent message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false)) { if (message.Role == AuthorRole.Assistant) { @@ -164,6 +157,22 @@ public async IAsyncEnumerable InvokeAsync( this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } + /// + /// Process a single interaction between a given an a . + /// + /// The agent actively interacting with the chat. + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// Specified agent joins the chat. + /// > + public IAsyncEnumerable InvokeStreamingAsync( + Agent agent, + CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); // %%% TODO + } + /// /// Initializes a new instance of the class. /// diff --git a/dotnet/src/Agents/Core/ChatHistoryChannel.cs b/dotnet/src/Agents/Core/ChatHistoryChannel.cs index c8f143fe55c5..bbba6f89daef 100644 --- a/dotnet/src/Agents/Core/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Core/ChatHistoryChannel.cs @@ -77,6 +77,25 @@ bool IsMessageVisible(ChatMessageContent message) => messageQueue.Count == 0); } + /// + protected override async IAsyncEnumerable InvokeStreamingAsync(Agent agent, ChatHistory messages, [EnumeratorCancellation] CancellationToken cancellationToken = default) + { + if (agent is not IChatHistoryHandler historyHandler) + { + throw new KernelException($"Invalid channel binding for agent: {agent.Id} ({agent.GetType().FullName})"); + } + + // Pre-process history reduction. + await this._history.ReduceAsync(historyHandler.HistoryReducer, cancellationToken).ConfigureAwait(false); + + await foreach (StreamingChatMessageContent streamingMessage in historyHandler.InvokeStreamingAsync(this._history, null, null, cancellationToken).ConfigureAwait(false)) + { + yield return streamingMessage; + } + + // %%% VERIFY this._history + } + /// protected override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken) { diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index d66f54917d3f..8e58a61a2e2c 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Net; @@ -261,7 +262,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist else if (completedStep.Type == RunStepType.MessageCreation) { // Retrieve the message - ThreadMessage? message = await RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false); + ThreadMessage? message = await RetrieveMessageAsync(client, threadId, completedStep.Details.CreatedMessageId, agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); if (message is not null) { @@ -321,19 +322,9 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R { foreach (RunStepToolCall toolCall in step.Details.ToolCalls) { - var nameParts = FunctionName.Parse(toolCall.FunctionName); + (FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(toolCall.FunctionName, toolCall.FunctionArguments); - KernelArguments functionArguments = []; - if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments)) - { - Dictionary arguments = JsonSerializer.Deserialize>(toolCall.FunctionArguments)!; - foreach (var argumentKvp in arguments) - { - functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); - } - } - - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); + FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); functionSteps.Add(toolCall.ToolCallId, content); @@ -341,38 +332,134 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R } } } + } - async Task RetrieveMessageAsync(string messageId, CancellationToken cancellationToken) + /// + /// Invoke the assistant on the specified thread using streaming. + /// + /// The assistant agent to interact with the thread. + /// The assistant client + /// The thread identifier + /// The reciever for the completed messages generated + /// Options to utilize for the invocation + /// The logger to utilize (might be agent or channel scoped) + /// The plugins and other state. + /// Optional arguments to pass to the agents's invocation, including any . + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. + /// + public static async IAsyncEnumerable InvokeStreamingAsync( + OpenAIAssistantAgent agent, + AssistantClient client, + string threadId, + ChatHistory messages, + OpenAIAssistantInvocationOptions? invocationOptions, + ILogger logger, + Kernel kernel, + KernelArguments? arguments, + [EnumeratorCancellation] CancellationToken cancellationToken) + { + if (agent.IsDeleted) { - ThreadMessage? message = null; + throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); + } - bool retry = false; - int count = 0; - do + logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId); + + ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))]; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions); + + options.ToolsOverride.AddRange(tools); + + // Evaluate status and process steps and messages, as encountered. + HashSet processedStepIds = []; + List functionCalls = []; + List> functionResultTasks = []; + HashSet messageIds = []; + + ThreadRun? run = null; + IAsyncEnumerable asyncUpdates = client.CreateRunStreamingAsync(threadId, agent.Id, options, cancellationToken); + + do + { + functionCalls.Clear(); + functionResultTasks.Clear(); + messageIds.Clear(); + + await foreach (StreamingUpdate update in asyncUpdates.ConfigureAwait(false)) { - try + //logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); // %%% LOGGING + + if (update is RunUpdate runUpdate) + { + run = runUpdate; + + logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId); + } + else if (update is RequiredActionUpdate actionUpdate) { - message = await client.GetMessageAsync(threadId, messageId, cancellationToken).ConfigureAwait(false); + (FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(actionUpdate.FunctionName, actionUpdate.FunctionArguments); + FunctionCallContent functionCall = new(nameParts.Name, nameParts.PluginName, actionUpdate.ToolCallId, functionArguments); + functionCalls.Add(functionCall); + + Task functionResultTask = ExecuteFunctionStep(agent, functionCall, cancellationToken); + functionResultTasks.Add(functionResultTask); } - catch (RequestFailedException exception) + else if (update is MessageContentUpdate contentUpdate) { - // Step has provided the message-id. Retry on of NotFound/404 exists. - // Extremely rarely there might be a synchronization issue between the - // assistant response and message-service. - retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; + messageIds.Add(contentUpdate.MessageId); + yield return GenerateStreamingMessageContent(agent.GetName(), contentUpdate); } - if (retry) + //logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), activeFunctionSteps.Length, run.Id, threadId); // %%% LOGGING + } + + if (run != null) + { + // Is in terminal state? + if (s_terminalStatuses.Contains(run.Status)) { - await Task.Delay(agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } + } - ++count; + if (functionCalls.Count > 0) + { + messages.Add(GenerateFunctionCallContent(agent.GetName(), functionCalls)); + + // Block for function results + FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); + + // Process tool output + ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); + asyncUpdates = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); + + messages.Add(GenerateFunctionResultsContent(agent.GetName(), functionResults)); } - while (retry); - return message; + if (messageIds.Count > 0) + { + foreach (string messageId in messageIds) + { + ThreadMessage? message = await RetrieveMessageAsync(client, threadId, messageId, agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + + if (message != null) + { + ChatMessageContent content = GenerateMessageContent(agent.GetName(), message); + messages.Add(content); + } + } + } + + //logger.LogOpenAIAssistantProcessingRunMessages(nameof(InvokeAsync), run.Id, threadId); // %%% LOGGING + //logger.LogOpenAIAssistantProcessedRunMessages(nameof(InvokeAsync), messageCount, run.Id, threadId); // %%% LOGGING } + while (run?.Status.IsTerminal == false); + + logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run?.Id ?? "Unknown", threadId); // %%% LOGGING } private static ChatMessageContent GenerateMessageContent(string? assistantName, ThreadMessage message) @@ -407,6 +494,38 @@ private static ChatMessageContent GenerateMessageContent(string? assistantName, return content; } + private static StreamingChatMessageContent GenerateStreamingMessageContent(string? assistantName, MessageContentUpdate update) + { + StreamingChatMessageContent content = + new(AuthorRole.Assistant, content: null) + { + AuthorName = assistantName, + }; + + // Process text content + if (!string.IsNullOrEmpty(update.Text)) + { + content.Items.Add(new StreamingTextContent(update.Text)); + } + // Process image content + else if (update.ImageFileId != null) + { + //content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); // %%% CONTENT + } + // Process annotations + else if (update.TextAnnotation != null) + { + //content.Items.Add(GenerateAnnotationContent(annotation)); // %%% CONTENT + } + + if (update.Role.HasValue) + { + content.Role = new(update.Role.Value.ToString()); + } + + return content; + } + private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation) { string? fileId = null; @@ -444,19 +563,36 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam }; } - private static ChatMessageContent GenerateFunctionCallContent(string agentName, FunctionCallContent[] functionSteps) + private static (FunctionName functionName, KernelArguments arguments) ParseFunctionCall(string functionName, string? functionArguments) + { + FunctionName nameParts = FunctionName.Parse(functionName); + + KernelArguments arguments = []; + + if (!string.IsNullOrWhiteSpace(functionArguments)) + { + foreach (var argumentKvp in JsonSerializer.Deserialize>(functionArguments!)!) + { + arguments[argumentKvp.Key] = argumentKvp.Value.ToString(); + } + } + + return (nameParts, arguments); + } + + private static ChatMessageContent GenerateFunctionCallContent(string agentName, IList functionCalls) { ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) { AuthorName = agentName }; - functionCallContent.Items.AddRange(functionSteps); + functionCallContent.Items.AddRange(functionCalls); return functionCallContent; } - private static ChatMessageContent GenerateFunctionResultContent(string agentName, FunctionCallContent functionStep, string result) + private static ChatMessageContent GenerateFunctionResultContent(string agentName, FunctionCallContent functionCall, string result) { ChatMessageContent functionCallContent = new(AuthorRole.Tool, content: null) { @@ -465,26 +601,51 @@ private static ChatMessageContent GenerateFunctionResultContent(string agentName functionCallContent.Items.Add( new FunctionResultContent( - functionStep.FunctionName, - functionStep.PluginName, - functionStep.Id, + functionCall.FunctionName, + functionCall.PluginName, + functionCall.Id, result)); return functionCallContent; } - private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionSteps, CancellationToken cancellationToken) + private static ChatMessageContent GenerateFunctionResultsContent(string agentName, IList functionResults) + { + ChatMessageContent functionResultContent = new(AuthorRole.Tool, content: null) + { + AuthorName = agentName + }; + + foreach (FunctionResultContent functionResult in functionResults) + { + functionResultContent.Items.Add( + new FunctionResultContent( + functionResult.FunctionName, + functionResult.PluginName, + functionResult.CallId, + functionResult.Result)); + } + + return functionResultContent; + } + + private static Task[] ExecuteFunctionSteps(OpenAIAssistantAgent agent, FunctionCallContent[] functionCalls, CancellationToken cancellationToken) { - Task[] functionTasks = new Task[functionSteps.Length]; + Task[] functionTasks = new Task[functionCalls.Length]; - for (int index = 0; index < functionSteps.Length; ++index) + for (int index = 0; index < functionCalls.Length; ++index) { - functionTasks[index] = functionSteps[index].InvokeAsync(agent.Kernel, cancellationToken); + functionTasks[index] = functionCalls[index].InvokeAsync(agent.Kernel, cancellationToken); } return functionTasks; } + private static Task ExecuteFunctionStep(OpenAIAssistantAgent agent, FunctionCallContent functionCall, CancellationToken cancellationToken) + { + return functionCall.InvokeAsync(agent.Kernel, cancellationToken); + } + private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] functionResults) { ToolOutput[] toolOutputs = new ToolOutput[functionResults.Length]; @@ -505,4 +666,36 @@ private static ToolOutput[] GenerateToolOutputs(FunctionResultContent[] function return toolOutputs; } + + private static async Task RetrieveMessageAsync(AssistantClient client, string threadId, string messageId, TimeSpan syncDelay, CancellationToken cancellationToken) + { + ThreadMessage? message = null; + + bool retry = false; + int count = 0; + do + { + try + { + message = await client.GetMessageAsync(threadId, messageId, cancellationToken).ConfigureAwait(false); + } + catch (RequestFailedException exception) + { + // Step has provided the message-id. Retry on of NotFound/404 exists. + // Extremely rarely there might be a synchronization issue between the + // assistant response and message-service. + retry = exception.Status == (int)HttpStatusCode.NotFound && count < 3; + } + + if (retry) + { + await Task.Delay(syncDelay, cancellationToken).ConfigureAwait(false); + } + + ++count; + } + while (retry); + + return message; + } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index f5c4a3588cf8..5a93861c4267 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; using OpenAI.Files; @@ -289,6 +290,55 @@ public async IAsyncEnumerable InvokeAsync( } } + /// + /// Invoke the assistant on the specified thread. + /// + /// The thread identifier + /// The reciever for the completed messages generated + /// Optional arguments to pass to the agents's invocation, including any . + /// The containing services, plugins, and other state for use by the agent. + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. + /// + public IAsyncEnumerable InvokeStreamingAsync( + string threadId, + ChatHistory messages, + KernelArguments? arguments = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this.InvokeStreamingAsync(threadId, messages, options: null, arguments, kernel, cancellationToken); + + /// + /// Invoke the assistant on the specified thread. + /// + /// The thread identifier + /// The reciever for the completed messages generated + /// Optional invocation options + /// Optional arguments to pass to the agents's invocation, including any . + /// The containing services, plugins, and other state for use by the agent. + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. + /// + public IAsyncEnumerable InvokeStreamingAsync( + string threadId, + ChatHistory messages, + OpenAIAssistantInvocationOptions? options, + KernelArguments? arguments = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + { + this.ThrowIfDeleted(); + + kernel ??= this.Kernel; + arguments ??= this.Arguments; + + return AssistantThreadActions.InvokeStreamingAsync(this, this._client, threadId, messages, options, this.Logger, kernel, arguments, cancellationToken); + } + /// protected override IEnumerable GetChannelKeys() { diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 77e8de748653..c514e2cd1bcb 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -35,6 +36,12 @@ protected override async Task ReceiveAsync(IEnumerable histo return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken); } + /// + protected override IAsyncEnumerable InvokeStreamingAsync(OpenAIAssistantAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + { + return AssistantThreadActions.InvokeStreamingAsync(agent, this._client, this._threadId, messages, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken); + } + /// protected override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) { diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index 17994a12e6a0..a54b049795d8 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -1,12 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; -using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests; @@ -24,50 +21,19 @@ public class AgentChannelTests public async Task VerifyAgentChannelUpcastAsync() { // Arrange - TestChannel channel = new(); + MockChannel channel = new(); // Assert Assert.Equal(0, channel.InvokeCount); // Act - var messages = channel.InvokeAgentAsync(new MockAgent()).ToArrayAsync(); + var messages = await channel.InvokeAgentAsync(new MockAgent()).ToArrayAsync(); // Assert Assert.Equal(1, channel.InvokeCount); // Act - await Assert.ThrowsAsync(() => channel.InvokeAgentAsync(new NextAgent()).ToArrayAsync().AsTask()); + Mock mockAgent = new(); + await Assert.ThrowsAsync(() => channel.InvokeAgentAsync(mockAgent.Object).ToArrayAsync().AsTask()); // Assert Assert.Equal(1, channel.InvokeCount); } - - /// - /// Not using mock as the goal here is to provide entrypoint to protected method. - /// - private sealed class TestChannel : AgentChannel - { - public int InvokeCount { get; private set; } - - public IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAgentAsync(Agent agent, CancellationToken cancellationToken = default) - => base.InvokeAsync(agent, cancellationToken); - -#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(MockAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) -#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously - { - this.InvokeCount++; - - yield break; - } - - protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - } - - private sealed class NextAgent : MockAgent; } diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index cd83ab8b9f45..8aec1853185e 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -155,5 +155,10 @@ public IAsyncEnumerable InvalidInvokeAsync( this.SetActivityOrThrow(); return this.InvokeAgentAsync(this.Agent, cancellationToken); } + + public override IAsyncEnumerable InvokeStreamingAsync(CancellationToken cancellationToken = default) + { + throw new System.NotImplementedException(); // %%% TODO + } } } diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs index 62420f90e62b..95275b661f31 100644 --- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs @@ -61,12 +61,7 @@ public async Task VerifyGroupAgentChatAgentMembershipAsync() Assert.Equal(3, chat.Agents.Count); // Act - var messages = await chat.InvokeAsync(agent4, isJoining: false).ToArrayAsync(); - // Assert - Assert.Equal(3, chat.Agents.Count); - - // Act - messages = await chat.InvokeAsync(agent4).ToArrayAsync(); + ChatMessageContent[] messages = await chat.InvokeAsync(agent4).ToArrayAsync(); // Assert Assert.Equal(4, chat.Agents.Count); } diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 96ed232fb109..f93cabf82b8e 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -41,7 +41,7 @@ public async Task VerifyBroadcastQueueReceiveAsync() { BlockDuration = TimeSpan.FromSeconds(0.08), }; - TestChannel channel = new(); + MockChannel channel = new(); ChannelReference reference = new(channel, "test"); // Act: Verify initial state @@ -84,7 +84,7 @@ public async Task VerifyBroadcastQueueFailureAsync() { BlockDuration = TimeSpan.FromSeconds(0.08), }; - BadChannel channel = new(); + MockChannel channel = new() { MockException = new InvalidOperationException("Test") }; ChannelReference reference = new(channel, "test"); // Act: Verify expected invocation of channel. @@ -108,7 +108,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() { BlockDuration = TimeSpan.FromSeconds(0.08), }; - TestChannel channel = new(); + MockChannel channel = new(); ChannelReference reference = new(channel, "test"); // Act: Enqueue multiple channels @@ -128,58 +128,9 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() Assert.Equal(10, channel.ReceivedMessages.Count); } - private static async Task VerifyReceivingStateAsync(int receiveCount, BroadcastQueue queue, TestChannel channel, string hash) + private static async Task VerifyReceivingStateAsync(int receiveCount, BroadcastQueue queue, MockChannel channel, string hash) { await queue.EnsureSynchronizedAsync(new ChannelReference(channel, hash)); Assert.Equal(receiveCount, channel.ReceiveCount); } - - private sealed class TestChannel : AgentChannel - { - public TimeSpan ReceiveDuration { get; set; } = TimeSpan.FromSeconds(0.3); - - public int ReceiveCount { get; private set; } - - public List ReceivedMessages { get; } = []; - - protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(Agent agent, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - protected internal override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) - { - this.ReceivedMessages.AddRange(history); - this.ReceiveCount++; - - await Task.Delay(this.ReceiveDuration, cancellationToken); - } - } - - private sealed class BadChannel : AgentChannel - { - public TimeSpan ReceiveDuration { get; set; } = TimeSpan.FromSeconds(0.1); - - protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected internal override IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(Agent agent, CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } - - protected internal override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) - { - await Task.Delay(this.ReceiveDuration, cancellationToken); - - throw new InvalidOperationException("Test"); - } - } } diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index 2535446dae7b..51fbf36c6ab4 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -15,7 +15,7 @@ namespace SemanticKernel.Agents.UnitTests; /// /// Mock definition of with a contract. /// -internal class MockAgent : KernelAgent, IChatHistoryHandler +internal sealed class MockAgent : KernelAgent, IChatHistoryHandler { public int InvokeCount { get; private set; } diff --git a/dotnet/src/Agents/UnitTests/MockChannel.cs b/dotnet/src/Agents/UnitTests/MockChannel.cs new file mode 100644 index 000000000000..4ed34b93ca97 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/MockChannel.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace SemanticKernel.Agents.UnitTests; + +internal sealed class MockChannel : AgentChannel +{ + public Exception? MockException { get; set; } + + public int InvokeCount { get; private set; } + + public int ReceiveCount { get; private set; } + + public TimeSpan ReceiveDuration { get; set; } = TimeSpan.FromSeconds(0.3); + + public List ReceivedMessages { get; } = []; + + protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAgentAsync(Agent agent, CancellationToken cancellationToken = default) + => base.InvokeAsync(agent, cancellationToken); + +#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously + protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(MockAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) +#pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously + { + this.InvokeCount++; + + if (this.MockException is not null) + { + throw this.MockException; + } + + yield break; + } + + protected internal override IAsyncEnumerable InvokeStreamingAsync(MockAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + protected internal override async Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) + { + this.ReceivedMessages.AddRange(history); + this.ReceiveCount++; + + await Task.Delay(this.ReceiveDuration, cancellationToken); + + if (this.MockException is not null) + { + throw this.MockException; + } + } +} From 9e59698d0d72f1aebfdff5c1ad046d9d6c864b93 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Mon, 12 Aug 2024 07:42:22 -0700 Subject: [PATCH 190/226] .Net Agents - Assistant V2 Migration (#7126) ### Motivation and Context Support Assistant V2 features according to [ADR](https://github.com/microsoft/semantic-kernel/blob/adr_assistant_v2/docs/decisions/0049-agents-assistantsV2.md) (based on V2 AI connector migration) ### Description - Refactored `OpenAIAssistantAgent` to support all V2 options except: streaming, message-attachment, tool_choice - Streaming to be addressed as a separate change - Extensive enhancement of unit-tests - Migrated samples to use `FileClient` - Deep pass to enhance and improve samples - Reviewed and updated test-coverage, generally agentcov3 ### Contribution Checklist - [X] The code builds clean without any errors or warnings - [X] The PR follows the [SK Contribution Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md) and the [pre-submission formatting script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts) raises no violations - [X] All unit tests pass, and I have added new tests where possible - [X] I didn't break anyone :smile: --------- Co-authored-by: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> --- dotnet/Directory.Packages.props | 1 - dotnet/SK-dotnet.sln | 50 +- .../ChatCompletion_FunctionTermination.cs | 35 +- .../Agents/ChatCompletion_Streaming.cs | 23 +- .../Agents/ComplexChat_NestedShopper.cs | 18 +- .../Concepts/Agents/Legacy_AgentAuthoring.cs | 12 +- .../Concepts/Agents/Legacy_AgentCharts.cs | 48 +- .../Agents/Legacy_AgentCollaboration.cs | 25 +- .../Concepts/Agents/Legacy_AgentDelegation.cs | 16 +- .../Concepts/Agents/Legacy_AgentTools.cs | 59 +- .../samples/Concepts/Agents/Legacy_Agents.cs | 29 +- .../Concepts/Agents/MixedChat_Agents.cs | 20 +- .../Concepts/Agents/MixedChat_Files.cs | 53 +- .../Concepts/Agents/MixedChat_Images.cs | 42 +- .../Agents/OpenAIAssistant_ChartMaker.cs | 38 +- .../OpenAIAssistant_FileManipulation.cs | 57 +- .../Agents/OpenAIAssistant_FileService.cs | 4 +- .../Agents/OpenAIAssistant_Retrieval.cs | 71 -- dotnet/samples/Concepts/Concepts.csproj | 10 +- .../Resources/Plugins/LegacyMenuPlugin.cs | 25 - .../Concepts/Resources/Plugins/MenuPlugin.cs | 34 - .../GettingStartedWithAgents.csproj | 18 +- .../GettingStartedWithAgents/README.md | 18 +- .../Resources/cat.jpg | Bin 0 -> 37831 bytes .../Resources/employees.pdf | Bin 0 -> 43422 bytes .../{Step1_Agent.cs => Step01_Agent.cs} | 14 +- .../{Step2_Plugins.cs => Step02_Plugins.cs} | 35 +- .../{Step3_Chat.cs => Step03_Chat.cs} | 14 +- ....cs => Step04_KernelFunctionStrategies.cs} | 23 +- ...ep5_JsonResult.cs => Step05_JsonResult.cs} | 21 +- ...ction.cs => Step06_DependencyInjection.cs} | 49 +- .../{Step7_Logging.cs => Step07_Logging.cs} | 16 +- ...OpenAIAssistant.cs => Step08_Assistant.cs} | 58 +- .../Step09_Assistant_Vision.cs | 74 +++ .../Step10_AssistantTool_CodeInterpreter.cs} | 32 +- .../Step11_AssistantTool_FileSearch.cs | 83 +++ .../src/Agents/Abstractions/AgentChannel.cs | 8 + dotnet/src/Agents/Abstractions/AgentChat.cs | 2 +- .../Agents/Abstractions/AggregatorChannel.cs | 3 + .../Logging/AgentChatLogMessages.cs | 2 +- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 11 +- .../ChatHistorySummarizationReducer.cs | 6 +- dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj | 4 +- .../OpenAI/Extensions/AuthorRoleExtensions.cs | 2 +- .../Extensions/KernelFunctionExtensions.cs | 9 +- .../AddHeaderRequestPolicy.cs | 2 +- .../Internal/AssistantMessageFactory.cs | 64 ++ .../Internal/AssistantRunOptionsFactory.cs | 53 ++ .../{ => Internal}/AssistantThreadActions.cs | 203 +++--- .../Internal/AssistantToolResourcesFactory.cs | 51 ++ .../AssistantThreadActionsLogMessages.cs | 3 +- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 300 +++++---- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 9 +- .../OpenAI/OpenAIAssistantConfiguration.cs | 91 --- .../OpenAI/OpenAIAssistantDefinition.cs | 71 +- .../OpenAI/OpenAIAssistantExecutionOptions.cs | 38 ++ .../OpenAIAssistantInvocationOptions.cs | 88 +++ .../src/Agents/OpenAI/OpenAIClientProvider.cs | 173 +++++ .../OpenAI/OpenAIThreadCreationOptions.cs | 37 ++ dotnet/src/Agents/OpenAI/RunPollingOptions.cs | 57 ++ .../src/Agents/UnitTests/AgentChannelTests.cs | 27 +- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 60 +- .../Agents/UnitTests/Agents.UnitTests.csproj | 3 +- .../Agents/UnitTests/AggregatorAgentTests.cs | 24 +- .../UnitTests/Core/AgentGroupChatTests.cs | 30 + .../Core/Chat/AgentGroupChatSettingsTests.cs | 7 + .../AggregatorTerminationStrategyTests.cs | 41 +- .../KernelFunctionSelectionStrategyTests.cs | 54 +- .../KernelFunctionTerminationStrategyTests.cs | 23 +- .../Chat/RegExTerminationStrategyTests.cs | 20 +- .../Chat/SequentialSelectionStrategyTests.cs | 38 +- .../Core/ChatCompletionAgentTests.cs | 71 +- .../UnitTests/Core/ChatHistoryChannelTests.cs | 22 +- .../ChatHistoryReducerExtensionsTests.cs | 39 +- .../ChatHistorySummarizationReducerTests.cs | 75 ++- .../ChatHistoryTruncationReducerTests.cs | 49 +- .../Extensions/ChatHistoryExtensionsTests.cs | 4 + .../UnitTests/Internal/BroadcastQueueTests.cs | 31 +- .../UnitTests/Internal/KeyEncoderTests.cs | 5 +- dotnet/src/Agents/UnitTests/MockAgent.cs | 5 +- .../UnitTests/OpenAI/AssertCollection.cs | 46 ++ .../Azure/AddHeaderRequestPolicyTests.cs | 7 +- .../Extensions/AuthorRoleExtensionsTests.cs | 5 +- .../Extensions/KernelExtensionsTests.cs | 6 + .../KernelFunctionExtensionsTests.cs | 20 +- .../Internal/AssistantMessageFactoryTests.cs | 210 ++++++ .../AssistantRunOptionsFactoryTests.cs | 139 ++++ .../OpenAI/OpenAIAssistantAgentTests.cs | 610 ++++++++++++++---- .../OpenAIAssistantConfigurationTests.cs | 61 -- .../OpenAI/OpenAIAssistantDefinitionTests.cs | 85 ++- .../OpenAIAssistantInvocationOptionsTests.cs | 100 +++ .../OpenAI/OpenAIClientProviderTests.cs | 86 +++ .../OpenAIThreadCreationOptionsTests.cs | 75 +++ .../OpenAI/RunPollingOptionsTests.cs | 71 ++ .../Agents/Extensions/OpenAIRestExtensions.cs | 3 +- .../Experimental/Agents/Internal/ChatRun.cs | 18 +- .../Agents/ChatCompletionAgentTests.cs | 18 +- .../Agents/OpenAIAssistantAgentTests.cs | 38 +- .../samples/AgentUtilities/BaseAgentsTest.cs | 129 ++++ .../samples/SamplesInternalUtilities.props | 5 +- .../Contents/AnnotationContent.cs | 2 +- .../Contents/FileReferenceContent.cs | 2 +- 102 files changed, 3412 insertions(+), 1364 deletions(-) delete mode 100644 dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs delete mode 100644 dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs create mode 100644 dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg create mode 100644 dotnet/samples/GettingStartedWithAgents/Resources/employees.pdf rename dotnet/samples/GettingStartedWithAgents/{Step1_Agent.cs => Step01_Agent.cs} (76%) rename dotnet/samples/GettingStartedWithAgents/{Step2_Plugins.cs => Step02_Plugins.cs} (76%) rename dotnet/samples/GettingStartedWithAgents/{Step3_Chat.cs => Step03_Chat.cs} (86%) rename dotnet/samples/GettingStartedWithAgents/{Step4_KernelFunctionStrategies.cs => Step04_KernelFunctionStrategies.cs} (84%) rename dotnet/samples/GettingStartedWithAgents/{Step5_JsonResult.cs => Step05_JsonResult.cs} (79%) rename dotnet/samples/GettingStartedWithAgents/{Step6_DependencyInjection.cs => Step06_DependencyInjection.cs} (65%) rename dotnet/samples/GettingStartedWithAgents/{Step7_Logging.cs => Step07_Logging.cs} (86%) rename dotnet/samples/GettingStartedWithAgents/{Step8_OpenAIAssistant.cs => Step08_Assistant.cs} (57%) create mode 100644 dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs rename dotnet/samples/{Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs => GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs} (50%) create mode 100644 dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs rename dotnet/src/Agents/OpenAI/{Azure => Internal}/AddHeaderRequestPolicy.cs (87%) create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs rename dotnet/src/Agents/OpenAI/{ => Internal}/AssistantThreadActions.cs (68%) create mode 100644 dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs delete mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs create mode 100644 dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs create mode 100644 dotnet/src/Agents/OpenAI/RunPollingOptions.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs delete mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs create mode 100644 dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs create mode 100644 dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs diff --git a/dotnet/Directory.Packages.props b/dotnet/Directory.Packages.props index 1b55be45d37d..2e15ff89460f 100644 --- a/dotnet/Directory.Packages.props +++ b/dotnet/Directory.Packages.props @@ -9,7 +9,6 @@ - diff --git a/dotnet/SK-dotnet.sln b/dotnet/SK-dotnet.sln index 4c4ed6c4df5a..b4580b4d1146 100644 --- a/dotnet/SK-dotnet.sln +++ b/dotnet/SK-dotnet.sln @@ -277,16 +277,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GettingStartedWithAgents", EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{77E141BA-AF5E-4C01-A970-6C07AC3CD55A}" ProjectSection(SolutionItems) = preProject - src\InternalUtilities\samples\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\ConfigurationNotFoundException.cs - src\InternalUtilities\samples\EnumerableExtensions.cs = src\InternalUtilities\samples\EnumerableExtensions.cs - src\InternalUtilities\samples\Env.cs = src\InternalUtilities\samples\Env.cs - src\InternalUtilities\samples\ObjectExtensions.cs = src\InternalUtilities\samples\ObjectExtensions.cs - src\InternalUtilities\samples\PlanExtensions.cs = src\InternalUtilities\samples\PlanExtensions.cs - src\InternalUtilities\samples\RepoFiles.cs = src\InternalUtilities\samples\RepoFiles.cs src\InternalUtilities\samples\SamplesInternalUtilities.props = src\InternalUtilities\samples\SamplesInternalUtilities.props - src\InternalUtilities\samples\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\TextOutputHelperExtensions.cs - src\InternalUtilities\samples\XunitLogger.cs = src\InternalUtilities\samples\XunitLogger.cs - src\InternalUtilities\samples\YourAppException.cs = src\InternalUtilities\samples\YourAppException.cs EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Functions.Prompty", "src\Functions\Functions.Prompty\Functions.Prompty.csproj", "{12B06019-740B-466D-A9E0-F05BC123A47D}" @@ -340,9 +331,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Qdrant.UnitTests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Connectors.Redis.UnitTests", "src\Connectors\Connectors.Redis.UnitTests\Connectors.Redis.UnitTests.csproj", "{ACD8C464-AEC9-45F6-A458-50A84F353DB7}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{38374C62-0263-4FE8-A18C-70FC8132912B}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AIModelRouter", "samples\Demos\AIModelRouter\AIModelRouter.csproj", "{E06818E3-00A5-41AC-97ED-9491070CDEA1}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "CreateChatGptPlugin", "CreateChatGptPlugin", "{F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098}" EndProject @@ -352,6 +341,29 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "MathPlugin", "MathPlugin", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "kernel-functions-generator", "samples\Demos\CreateChatGptPlugin\MathPlugin\kernel-functions-generator\kernel-functions-generator.csproj", "{4326A974-F027-4ABD-A220-382CC6BB0801}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{EE454832-085F-4D37-B19B-F94F7FC6984A}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\InternalUtilities\BaseTest.cs = src\InternalUtilities\samples\InternalUtilities\BaseTest.cs + src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs = src\InternalUtilities\samples\InternalUtilities\ConfigurationNotFoundException.cs + src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs = src\InternalUtilities\samples\InternalUtilities\EmbeddedResource.cs + src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs = src\InternalUtilities\samples\InternalUtilities\EnumerableExtensions.cs + src\InternalUtilities\samples\InternalUtilities\Env.cs = src\InternalUtilities\samples\InternalUtilities\Env.cs + src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs = src\InternalUtilities\samples\InternalUtilities\JsonResultTranslator.cs + src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs = src\InternalUtilities\samples\InternalUtilities\ObjectExtensions.cs + src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs = src\InternalUtilities\samples\InternalUtilities\RepoFiles.cs + src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs = src\InternalUtilities\samples\InternalUtilities\TestConfiguration.cs + src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs = src\InternalUtilities\samples\InternalUtilities\TextOutputHelperExtensions.cs + src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs = src\InternalUtilities\samples\InternalUtilities\XunitLogger.cs + src\InternalUtilities\samples\InternalUtilities\YourAppException.cs = src\InternalUtilities\samples\InternalUtilities\YourAppException.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Agents", "Agents", "{5C6C30E0-7AC1-47F4-8244-57B066B43FD8}" + ProjectSection(SolutionItems) = preProject + src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs = src\InternalUtilities\samples\AgentUtilities\BaseAgentsTest.cs + EndProjectSection +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "StepwisePlannerMigration", "samples\Demos\StepwisePlannerMigration\StepwisePlannerMigration.csproj", "{2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -839,10 +851,6 @@ Global {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Publish|Any CPU.Build.0 = Debug|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACD8C464-AEC9-45F6-A458-50A84F353DB7}.Release|Any CPU.Build.0 = Release|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Publish|Any CPU.ActiveCfg = Debug|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {38374C62-0263-4FE8-A18C-70FC8132912B}.Release|Any CPU.Build.0 = Release|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E06818E3-00A5-41AC-97ED-9491070CDEA1}.Publish|Any CPU.ActiveCfg = Debug|Any CPU @@ -861,6 +869,12 @@ Global {4326A974-F027-4ABD-A220-382CC6BB0801}.Publish|Any CPU.Build.0 = Debug|Any CPU {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.ActiveCfg = Release|Any CPU {4326A974-F027-4ABD-A220-382CC6BB0801}.Release|Any CPU.Build.0 = Release|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.ActiveCfg = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Publish|Any CPU.Build.0 = Debug|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -975,12 +989,14 @@ Global {738DCDB1-EFA8-4913-AD4C-6FC3F09B0A0C} = {2E79AD99-632F-411F-B3A5-1BAF3F5F89AB} {8642A03F-D840-4B2E-B092-478300000F83} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} {ACD8C464-AEC9-45F6-A458-50A84F353DB7} = {0247C2C9-86C3-45BA-8873-28B0948EDC0C} - {38374C62-0263-4FE8-A18C-70FC8132912B} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {E06818E3-00A5-41AC-97ED-9491070CDEA1} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} {6B268108-2AB5-4607-B246-06AD8410E60E} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} = {F8B82F6B-B16A-4F8C-9C41-E9CD8D79A098} {4326A974-F027-4ABD-A220-382CC6BB0801} = {4BB70FB3-1EC0-4F4A-863B-273D6C6D4D9A} + {EE454832-085F-4D37-B19B-F94F7FC6984A} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} + {5C6C30E0-7AC1-47F4-8244-57B066B43FD8} = {77E141BA-AF5E-4C01-A970-6C07AC3CD55A} + {2A6B056D-B35A-4CCE-80EE-0307EA9C3A57} = {5D4C0700-BBB5-418F-A7B2-F392B9A18263} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FBDC56A3-86AD-4323-AA0F-201E59123B83} diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs index 16c019aebbfd..d0b8e92d39d7 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_FunctionTermination.cs @@ -12,7 +12,7 @@ namespace Agents; /// Demonstrate usage of for both direction invocation /// of and via . /// -public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseTest(output) +public class ChatCompletion_FunctionTermination(ITestOutputHelper output) : BaseAgentsTest(output) { [Fact] public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() @@ -44,25 +44,25 @@ public async Task UseAutoFunctionInvocationFilterWithAgentInvocationAsync() Console.WriteLine("================================"); foreach (ChatMessageContent message in chat) { - this.WriteContent(message); + this.WriteAgentChatMessage(message); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.Add(userContent); - this.WriteContent(userContent); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { // Do not add a message implicitly added to the history. - if (!content.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) + if (!response.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)) { - chat.Add(content); + chat.Add(response); } - this.WriteContent(content); + this.WriteAgentChatMessage(response); } } } @@ -98,28 +98,23 @@ public async Task UseAutoFunctionInvocationFilterWithAgentChatAsync() ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync(); for (int index = history.Length; index > 0; --index) { - this.WriteContent(history[index - 1]); + this.WriteAgentChatMessage(history[index - 1]); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - ChatMessageContent userContent = new(AuthorRole.User, input); - chat.AddChatMessage(userContent); - this.WriteContent(userContent); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - this.WriteContent(content); + this.WriteAgentChatMessage(response); } } } - private void WriteContent(ChatMessageContent content) - { - Console.WriteLine($"[{content.Items.LastOrDefault()?.GetType().Name ?? "(empty)"}] {content.Role} : '{content.Content}'"); - } - private Kernel CreateKernelWithFilter() { IKernelBuilder builder = Kernel.CreateBuilder(); diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index d3e94386af96..575db7f7f288 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -12,7 +12,7 @@ namespace Agents; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseTest(output) +public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ParrotName = "Parrot"; private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; @@ -66,32 +66,33 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync() // Local function to invoke agent and display the conversation messages. private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); StringBuilder builder = new(); - await foreach (StreamingChatMessageContent message in agent.InvokeStreamingAsync(chat)) + await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat)) { - if (string.IsNullOrEmpty(message.Content)) + if (string.IsNullOrEmpty(response.Content)) { continue; } if (builder.Length == 0) { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}:"); + Console.WriteLine($"# {response.Role} - {response.AuthorName ?? "*"}:"); } - Console.WriteLine($"\t > streamed: '{message.Content}'"); - builder.Append(message.Content); + Console.WriteLine($"\t > streamed: '{response.Content}'"); + builder.Append(response.Content); } if (builder.Length > 0) { // Display full response and capture in chat history - Console.WriteLine($"\t > complete: '{builder}'"); - chat.Add(new ChatMessageContent(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }); + ChatMessageContent response = new(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }; + chat.Add(response); + this.WriteAgentChatMessage(response); } } diff --git a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs index 81b2914ade3b..0d7b27917d78 100644 --- a/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs +++ b/dotnet/samples/Concepts/Agents/ComplexChat_NestedShopper.cs @@ -13,10 +13,8 @@ namespace Agents; /// Demonstrate usage of and /// to manage execution. /// -public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseTest(output) +public class ComplexChat_NestedShopper(ITestOutputHelper output) : BaseAgentsTest(output) { - protected override bool ForceOpenAI => true; - private const string InternalLeaderName = "InternalLeader"; private const string InternalLeaderInstructions = """ @@ -154,20 +152,20 @@ public async Task NestedChatWithAggregatorAgentAsync() Console.WriteLine(">>>> AGGREGATED CHAT"); Console.WriteLine(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>"); - await foreach (ChatMessageContent content in chat.GetChatMessagesAsync(personalShopperAgent).Reverse()) + await foreach (ChatMessageContent message in chat.GetChatMessagesAsync(personalShopperAgent).Reverse()) { - Console.WriteLine($">>>> {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(message); } async Task InvokeChatAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync(personalShopperAgent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(personalShopperAgent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } Console.WriteLine($"\n# IS COMPLETE: {chat.IsComplete}"); diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs index 062262fe8a8c..53276c75a24d 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentAuthoring.cs @@ -9,12 +9,6 @@ namespace Agents; /// public class Legacy_AgentAuthoring(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - // Track agents for clean-up private static readonly List s_agents = []; @@ -72,7 +66,7 @@ private static async Task CreateArticleGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("You write concise opinionated articles that are published online. Use an outline to generate an article with one section of prose for each top-level outline element. Each section is based on research with a maximum of 120 words.") .WithName("Article Author") .WithDescription("Author an article on a given topic.") @@ -87,7 +81,7 @@ private static async Task CreateOutlineGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("Produce an single-level outline (no child elements) based on the given topic with at most 3 sections.") .WithName("Outline Generator") .WithDescription("Generate an outline.") @@ -100,7 +94,7 @@ private static async Task CreateResearchGeneratorAsync() return Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .WithInstructions("Provide insightful research that supports the given topic based on your knowledge of the outline topic.") .WithName("Researcher") .WithDescription("Author research summary.") diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs index b64f183adbc8..d40755101309 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCharts.cs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents; +using OpenAI; +using OpenAI.Files; namespace Agents; @@ -12,28 +14,15 @@ namespace Agents; /// public sealed class Legacy_AgentCharts(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - private new const bool ForceOpenAI = false; - /// /// Create a chart and retrieve by file_id. /// - [Fact(Skip = "Launches external processes")] + [Fact] public async Task CreateChartAsync() { Console.WriteLine("======== Using CodeInterpreter tool ========"); - var fileService = CreateFileService(); + FileClient fileClient = CreateFileClient(); var agent = await CreateAgentBuilder().WithCodeInterpreter().BuildAsync(); @@ -69,11 +58,11 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi { var filename = $"{imageName}.jpg"; var path = Path.Combine(Environment.CurrentDirectory, filename); - Console.WriteLine($"# {message.Role}: {message.Content}"); + var fileId = message.Content; + Console.WriteLine($"# {message.Role}: {fileId}"); Console.WriteLine($"# {message.Role}: {path}"); - var content = await fileService.GetFileContentAsync(message.Content); - await using var outputStream = File.OpenWrite(filename); - await outputStream.WriteAsync(content.Data!.Value); + BinaryData content = await fileClient.DownloadFileAsync(fileId); + File.WriteAllBytes(filename, content.ToArray()); Process.Start( new ProcessStartInfo { @@ -91,22 +80,23 @@ async Task InvokeAgentAsync(IAgentThread thread, string imageName, string questi } } -#pragma warning disable CS0618 // Type or member is obsolete - private static OpenAIFileService CreateFileService() + private FileClient CreateFileClient() { - return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new OpenAIFileService(TestConfiguration.OpenAI.ApiKey) : - new OpenAIFileService(new Uri(TestConfiguration.AzureOpenAI.Endpoint), apiKey: TestConfiguration.AzureOpenAI.ApiKey); + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); } #pragma warning restore CS0618 // Type or member is obsolete - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } } diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs index 53ae0c07662a..fa257d2764b3 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentCollaboration.cs @@ -9,17 +9,6 @@ namespace Agents; /// public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-turbo-preview"; - - /// - /// Set this to 'true' to target OpenAI instead of Azure OpenAI. - /// - private const bool UseOpenAI = false; - // Track agents for clean-up private static readonly List s_agents = []; @@ -29,8 +18,6 @@ public class Legacy_AgentCollaboration(ITestOutputHelper output) : BaseTest(outp [Fact(Skip = "This test take more than 5 minutes to execute")] public async Task RunCollaborationAsync() { - Console.WriteLine($"======== Example72:Collaboration:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========"); - IAgentThread? thread = null; try { @@ -82,8 +69,6 @@ public async Task RunCollaborationAsync() [Fact(Skip = "This test take more than 2 minutes to execute")] public async Task RunAsPluginsAsync() { - Console.WriteLine($"======== Example72:AsPlugins:{(UseOpenAI ? "OpenAI" : "AzureAI")} ========"); - try { // Create copy-writer agent to generate ideas @@ -113,7 +98,7 @@ await CreateAgentBuilder() } } - private static async Task CreateCopyWriterAsync(IAgent? agent = null) + private async Task CreateCopyWriterAsync(IAgent? agent = null) { return Track( @@ -125,7 +110,7 @@ await CreateAgentBuilder() .BuildAsync()); } - private static async Task CreateArtDirectorAsync() + private async Task CreateArtDirectorAsync() { return Track( @@ -136,13 +121,13 @@ await CreateAgentBuilder() .BuildAsync()); } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { var builder = new AgentBuilder(); return - UseOpenAI ? - builder.WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + builder.WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : builder.WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs index 86dacb9c256d..b4b0ed93199f 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentDelegation.cs @@ -12,12 +12,6 @@ namespace Agents; /// public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106"; - // Track agents for clean-up private static readonly List s_agents = []; @@ -27,8 +21,6 @@ public class Legacy_AgentDelegation(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task RunAsync() { - Console.WriteLine("======== Example71_AgentDelegation ========"); - if (TestConfiguration.OpenAI.ApiKey is null) { Console.WriteLine("OpenAI apiKey not found. Skipping example."); @@ -39,11 +31,11 @@ public async Task RunAsync() try { - var plugin = KernelPluginFactory.CreateFromType(); + var plugin = KernelPluginFactory.CreateFromType(); var menuAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml")) .WithDescription("Answer questions about how the menu uses the tool.") .WithPlugin(plugin) @@ -52,14 +44,14 @@ public async Task RunAsync() var parrotAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml")) .BuildAsync()); var toolAgent = Track( await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ToolAgent.yaml")) .WithPlugin(parrotAgent.AsPlugin()) .WithPlugin(menuAgent.AsPlugin()) diff --git a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs index c75a5e403cea..00af8faab617 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_AgentTools.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using Azure.AI.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents; +using OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -13,21 +14,8 @@ namespace Agents; /// public sealed class Legacy_AgentTools(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and parallel function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-4-1106-preview"; - - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - /// - /// NOTE: Retrieval tools is not currently available on Azure. - /// - private new const bool ForceOpenAI = true; + /// + protected override bool ForceOpenAI => true; // Track agents for clean-up private readonly List _agents = []; @@ -79,14 +67,13 @@ public async Task RunRetrievalToolAsync() return; } - Kernel kernel = CreateFileEnabledKernel(); -#pragma warning disable CS0618 // Type or member is obsolete - var fileService = kernel.GetRequiredService(); - var result = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); -#pragma warning restore CS0618 // Type or member is obsolete + FileClient fileClient = CreateFileClient(); + + OpenAIFileInfo result = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!), + "travelinfo.txt", + FileUploadPurpose.Assistants); var fileId = result.Id; Console.WriteLine($"! {fileId}"); @@ -112,7 +99,7 @@ await ChatAsync( } finally { - await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileService.DeleteFileAsync(fileId))); + await Task.WhenAll(this._agents.Select(a => a.DeleteAsync()).Append(fileClient.DeleteFileAsync(fileId))); } } @@ -167,21 +154,21 @@ async Task InvokeAgentAsync(IAgent agent, string question) } } - private static Kernel CreateFileEnabledKernel() + private FileClient CreateFileClient() { -#pragma warning disable CS0618 // Type or member is obsolete - return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - Kernel.CreateBuilder().AddOpenAIFiles(TestConfiguration.OpenAI.ApiKey).Build() : - throw new NotImplementedException("The file service is being deprecated and was not moved to AzureOpenAI connector."); -#pragma warning restore CS0618 // Type or member is obsolete + OpenAIClient client = + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new OpenAIClient(TestConfiguration.OpenAI.ApiKey) : + new AzureOpenAIClient(new Uri(TestConfiguration.AzureOpenAI.Endpoint), TestConfiguration.AzureOpenAI.ApiKey); + + return client.GetFileClient(); } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } diff --git a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs index 5af10987bb3a..31cc4926392b 100644 --- a/dotnet/samples/Concepts/Agents/Legacy_Agents.cs +++ b/dotnet/samples/Concepts/Agents/Legacy_Agents.cs @@ -13,19 +13,6 @@ namespace Agents; /// public class Legacy_Agents(ITestOutputHelper output) : BaseTest(output) { - /// - /// Specific model is required that supports agents and function calling. - /// Currently this is limited to Open AI hosted services. - /// - private const string OpenAIFunctionEnabledModel = "gpt-3.5-turbo-1106"; - - /// - /// Flag to force usage of OpenAI configuration if both - /// and are defined. - /// If 'false', Azure takes precedence. - /// - private new const bool ForceOpenAI = false; - /// /// Chat using the "Parrot" agent. /// Tools/functions: None @@ -61,18 +48,12 @@ public async Task RunWithMethodFunctionsAsync() await ChatAsync( "Agents.ToolAgent.yaml", // Defined under ./Resources/Agents plugin, - arguments: new() { { LegacyMenuPlugin.CorrelationIdArgument, 3.141592653 } }, + arguments: null, "Hello", "What is the special soup?", "What is the special drink?", "Do you have enough soup for 5 orders?", "Thank you!"); - - Console.WriteLine("\nCorrelation Ids:"); - foreach (string correlationId in menuApi.CorrelationIds) - { - Console.WriteLine($"- {correlationId}"); - } } /// @@ -114,7 +95,7 @@ public async Task RunAsFunctionAsync() // Create parrot agent, same as the other cases. var agent = await new AgentBuilder() - .WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) + .WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) .FromTemplate(EmbeddedResource.Read("Agents.ParrotAgent.yaml")) .BuildAsync(); @@ -187,11 +168,11 @@ await Task.WhenAll( } } - private static AgentBuilder CreateAgentBuilder() + private AgentBuilder CreateAgentBuilder() { return - ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? - new AgentBuilder().WithOpenAIChatCompletion(OpenAIFunctionEnabledModel, TestConfiguration.OpenAI.ApiKey) : + this.ForceOpenAI || string.IsNullOrEmpty(TestConfiguration.AzureOpenAI.Endpoint) ? + new AgentBuilder().WithOpenAIChatCompletion(TestConfiguration.OpenAI.ChatModelId, TestConfiguration.OpenAI.ApiKey) : new AgentBuilder().WithAzureOpenAIChatCompletion(TestConfiguration.AzureOpenAI.Endpoint, TestConfiguration.AzureOpenAI.ChatDeploymentName, TestConfiguration.AzureOpenAI.ApiKey); } } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs index d3a894dd6c8e..21b19c1d342c 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Agents.cs @@ -10,7 +10,7 @@ namespace Agents; /// Demonstrate that two different agent types are able to participate in the same conversation. /// In this case a and participate. /// -public class MixedChat_Agents(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Agents(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -47,12 +47,12 @@ public async Task ChatWithOpenAIAssistantAgentAndChatCompletionAgentAsync() OpenAIAssistantAgent agentWriter = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - definition: new() + clientProvider: this.GetClientProvider(), + definition: new(this.Model) { Instructions = CopyWriterInstructions, Name = CopyWriterName, - ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -76,16 +76,16 @@ await OpenAIAssistantAgent.CreateAsync( }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs index b95c6efca36d..0219c25f7712 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Files.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Files.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -13,25 +12,22 @@ namespace Agents; /// Demonstrate agent interacts with /// when it produces file output. /// -public class MixedChat_Files(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Files(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - private const string SummaryInstructions = "Summarize the entire conversation for the user in natural language."; [Fact] public async Task AnalyzeFileAndGenerateReportAsync() { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIClientProvider provider = this.GetClientProvider(); + + FileClient fileClient = provider.Client.GetFileClient(); - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("30-user-context.txt"), mimeType: "text/plain"), - new OpenAIFileUploadExecutionSettings("30-user-context.txt", OpenAIFilePurpose.Assistants)); + OpenAIFileInfo uploadFile = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("30-user-context.txt")), + "30-user-context.txt", + FileUploadPurpose.Assistants); Console.WriteLine(this.ApiKey); @@ -39,12 +35,12 @@ await fileService.UploadContentAsync( OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + provider, + new(this.Model) { - EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file with assistant + EnableCodeInterpreter = true, + CodeInterpreterFileIds = [uploadFile.Id], // Associate uploaded file with assistant code-interpreter + Metadata = AssistantSampleMetadata, }); ChatCompletionAgent summaryAgent = @@ -71,7 +67,7 @@ Create a tab delimited file report of the ordered (descending) frequency distrib finally { await analystAgent.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); + await fileClient.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. @@ -79,23 +75,16 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) { if (!string.IsNullOrWhiteSpace(input)) { + ChatMessageContent message = new(AuthorRole.User, input); chat.AddChatMessage(new(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + this.WriteAgentChatMessage(message); } - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"\n# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - - foreach (AnnotationContent annotation in content.Items.OfType()) - { - Console.WriteLine($"\t* '{annotation.Quote}' => {annotation.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - Console.WriteLine($"\n{Encoding.Default.GetString(byteContent)}"); - } + this.WriteAgentChatMessage(response); + await this.DownloadResponseContentAsync(fileClient, response); } } -#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 36b96fc4be54..142706e8506c 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -3,7 +3,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Files; namespace Agents; @@ -11,13 +11,8 @@ namespace Agents; /// Demonstrate agent interacts with /// when it produces image output. /// -public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Images(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - private const string AnalystName = "Analyst"; private const string AnalystInstructions = "Create charts as requested without explanation."; @@ -27,20 +22,21 @@ public class MixedChat_Images(ITestOutputHelper output) : BaseTest(output) [Fact] public async Task AnalyzeDataAndGenerateChartAsync() { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIClientProvider provider = this.GetClientProvider(); + + FileClient fileClient = provider.Client.GetFileClient(); // Define the agents OpenAIAssistantAgent analystAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + provider, + new(this.Model) { Instructions = AnalystInstructions, Name = AnalystName, EnableCodeInterpreter = true, - ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); ChatCompletionAgent summaryAgent = @@ -87,28 +83,16 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) { if (!string.IsNullOrWhiteSpace(input)) { + ChatMessageContent message = new(AuthorRole.User, input); chat.AddChatMessage(new(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + this.WriteAgentChatMessage(message); } - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"\n# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } - - foreach (FileReferenceContent fileReference in message.Items.OfType()) - { - Console.WriteLine($"\t* Generated image - @{fileReference.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(fileReference.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - string filePath = Path.ChangeExtension(Path.GetTempFileName(), ".png"); - await File.WriteAllBytesAsync($"{filePath}.png", byteContent); - Console.WriteLine($"\t* Local path - {filePath}"); - } + this.WriteAgentChatMessage(response); + await this.DownloadResponseImageAsync(fileClient, response); } } -#pragma warning restore CS0618 // Type or member is obsolete } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs index ef5ba80154fa..cd81f7c4d187 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_ChartMaker.cs @@ -3,6 +3,7 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; namespace Agents; @@ -10,30 +11,29 @@ namespace Agents; /// Demonstrate using code-interpreter with to /// produce image content displays the requested charts. /// -public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_ChartMaker(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target Open AI services. - /// - protected override bool ForceOpenAI => true; - private const string AgentName = "ChartMaker"; private const string AgentInstructions = "Create charts as requested without explanation."; [Fact] public async Task GenerateChartWithOpenAIAssistantAgentAsync() { + OpenAIClientProvider provider = this.GetClientProvider(); + + FileClient fileClient = provider.Client.GetFileClient(); + // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + provider, + new(this.Model) { Instructions = AgentInstructions, Name = AgentName, EnableCodeInterpreter = true, - ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -55,6 +55,7 @@ Sum 426 1622 856 2904 """); await InvokeAgentAsync("Can you regenerate this same chart using the category names as the bar colors?"); + await InvokeAgentAsync("Perfect, can you regenerate this as a line chart?"); } finally { @@ -64,21 +65,14 @@ Sum 426 1622 856 2904 // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(new(AuthorRole.User, input)); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } - - foreach (FileReferenceContent fileReference in message.Items.OfType()) - { - Console.WriteLine($"# {message.Role} - {message.AuthorName ?? "*"}: @{fileReference.FileId}"); - } + this.WriteAgentChatMessage(response); + await this.DownloadResponseImageAsync(fileClient, response); } } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs index f99130790eef..dc4af2ad2743 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileManipulation.cs @@ -1,10 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; +using OpenAI.Files; using Resources; namespace Agents; @@ -12,38 +11,31 @@ namespace Agents; /// /// Demonstrate using code-interpreter to manipulate and generate csv files with . /// -public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseTest(output) +public class OpenAIAssistant_FileManipulation(ITestOutputHelper output) : BaseAgentsTest(output) { - /// - /// Target OpenAI services. - /// - protected override bool ForceOpenAI => true; - [Fact] public async Task AnalyzeCSVFileUsingOpenAIAssistantAgentAsync() { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync( - new BinaryContent(await EmbeddedResource.ReadAllAsync("sales.csv"), mimeType: "text/plain"), - new OpenAIFileUploadExecutionSettings("sales.csv", OpenAIFilePurpose.Assistants)); + OpenAIClientProvider provider = this.GetClientProvider(); -#pragma warning restore CS0618 // Type or member is obsolete + FileClient fileClient = provider.Client.GetFileClient(); - Console.WriteLine(this.ApiKey); + OpenAIFileInfo uploadFile = + await fileClient.UploadFileAsync( + new BinaryData(await EmbeddedResource.ReadAllAsync("sales.csv")!), + "sales.csv", + FileUploadPurpose.Assistants); // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + provider, + new(this.Model) { - EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file + EnableCodeInterpreter = true, + CodeInterpreterFileIds = [uploadFile.Id], + Metadata = AssistantSampleMetadata, }); // Create a chat for agent interaction. @@ -59,27 +51,20 @@ await OpenAIAssistantAgent.CreateAsync( finally { await agent.DeleteAsync(); - await fileService.DeleteFileAsync(uploadFile.Id); + await fileClient.DeleteFileAsync(uploadFile.Id); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(new(AuthorRole.User, input)); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - - foreach (AnnotationContent annotation in content.Items.OfType()) - { - Console.WriteLine($"\n* '{annotation.Quote}' => {annotation.FileId}"); - BinaryContent fileContent = await fileService.GetFileContentAsync(annotation.FileId!); - byte[] byteContent = fileContent.Data?.ToArray() ?? []; - Console.WriteLine(Encoding.Default.GetString(byteContent)); - } + this.WriteAgentChatMessage(response); + await this.DownloadResponseContentAsync(fileClient, response); } } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs index 38bac46f648a..a8f31622c753 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_FileService.cs @@ -28,7 +28,7 @@ public async Task UploadAndRetrieveFilesAsync() new BinaryContent(data: await EmbeddedResource.ReadAllAsync("travelinfo.txt"), mimeType: "text/plain") { InnerContent = "travelinfo.txt" } ]; - var fileContents = new Dictionary(); + Dictionary fileContents = new(); foreach (BinaryContent file in files) { OpenAIFileReference result = await fileService.UploadContentAsync(file, new(file.InnerContent!.ToString()!, OpenAIFilePurpose.FineTune)); @@ -49,7 +49,7 @@ public async Task UploadAndRetrieveFilesAsync() string? fileName = fileContents[fileReference.Id].InnerContent!.ToString(); ReadOnlyMemory data = content.Data ?? new(); - var typedContent = mimeType switch + BinaryContent typedContent = mimeType switch { "image/jpeg" => new ImageContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata }, "audio/wav" => new AudioContent(data, mimeType) { Uri = content.Uri, InnerContent = fileName, Metadata = content.Metadata }, diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs deleted file mode 100644 index 71acf3db0e85..000000000000 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Retrieval.cs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; -using Resources; - -namespace Agents; - -/// -/// Demonstrate using retrieval on . -/// -public class OpenAIAssistant_Retrieval(ITestOutputHelper output) : BaseTest(output) -{ - /// - /// Retrieval tool not supported on Azure OpenAI. - /// - protected override bool ForceOpenAI => true; - - [Fact] - public async Task UseRetrievalToolWithOpenAIAssistantAgentAsync() - { -#pragma warning disable CS0618 // Type or member is obsolete - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); - - OpenAIFileReference uploadFile = - await fileService.UploadContentAsync(new BinaryContent(await EmbeddedResource.ReadAllAsync("travelinfo.txt")!, "text/plain"), - new OpenAIFileUploadExecutionSettings("travelinfo.txt", OpenAIFilePurpose.Assistants)); -#pragma warning restore CS0618 // Type or member is obsolete - // Define the agent - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() - { - EnableRetrieval = true, // Enable retrieval - ModelId = this.Model, - FileIds = [uploadFile.Id] // Associate uploaded file - }); - - // Create a chat for agent interaction. - AgentGroupChat chat = new(); - - // Respond to user input - try - { - await InvokeAgentAsync("Where did sam go?"); - await InvokeAgentAsync("When does the flight leave Seattle?"); - await InvokeAgentAsync("What is the hotel contact info at the destination?"); - } - finally - { - await agent.DeleteAsync(); - } - - // Local function to invoke agent and display the conversation messages. - async Task InvokeAgentAsync(string input) - { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } - } - } -} diff --git a/dotnet/samples/Concepts/Concepts.csproj b/dotnet/samples/Concepts/Concepts.csproj index 89ac1452713a..aa303046bd36 100644 --- a/dotnet/samples/Concepts/Concepts.csproj +++ b/dotnet/samples/Concepts/Concepts.csproj @@ -8,7 +8,7 @@ false true - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -41,7 +41,10 @@ - + + + true + @@ -109,5 +112,8 @@ Always + + Always + diff --git a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs index 7111e873cf4c..c383ea9025f1 100644 --- a/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs +++ b/dotnet/samples/Concepts/Resources/Plugins/LegacyMenuPlugin.cs @@ -7,12 +7,6 @@ namespace Plugins; public sealed class LegacyMenuPlugin { - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - /// /// Returns a mock item menu. /// @@ -20,8 +14,6 @@ public sealed class LegacyMenuPlugin [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] public string[] GetSpecials(KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(GetSpecials)); - return [ "Special Soup: Clam Chowder", @@ -39,8 +31,6 @@ public string GetItemPrice( string menuItem, KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(GetItemPrice)); - return "$9.99"; } @@ -55,21 +45,6 @@ public bool IsItem86d( int count, KernelArguments? arguments) { - CaptureCorrelationId(arguments, nameof(IsItem86d)); - return count < 3; } - - private void CaptureCorrelationId(KernelArguments? arguments, string scope) - { - if (arguments?.TryGetValue(CorrelationIdArgument, out object? correlationId) ?? false) - { - string? correlationText = correlationId?.ToString(); - - if (!string.IsNullOrWhiteSpace(correlationText)) - { - this._correlationIds.Add($"{scope}:{correlationText}"); - } - } - } } diff --git a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs b/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs deleted file mode 100644 index be82177eda5d..000000000000 --- a/dotnet/samples/Concepts/Resources/Plugins/MenuPlugin.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using System.ComponentModel; -using Microsoft.SemanticKernel; - -namespace Plugins; - -public sealed class MenuPlugin -{ - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - - [KernelFunction, Description("Provides a list of specials from the menu.")] - [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } - - [KernelFunction, Description("Provides the price of the requested menu item.")] - public string GetItemPrice( - [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } -} diff --git a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj index decbe920b28b..df9e025b678f 100644 --- a/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj +++ b/dotnet/samples/GettingStartedWithAgents/GettingStartedWithAgents.csproj @@ -9,7 +9,7 @@ true - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110 + $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 Library 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 @@ -32,12 +32,16 @@ - + + + true + + @@ -47,4 +51,14 @@ + + + Always + + + + + + + diff --git a/dotnet/samples/GettingStartedWithAgents/README.md b/dotnet/samples/GettingStartedWithAgents/README.md index 39952506548c..ed0e68802994 100644 --- a/dotnet/samples/GettingStartedWithAgents/README.md +++ b/dotnet/samples/GettingStartedWithAgents/README.md @@ -19,13 +19,17 @@ The getting started with agents examples include: Example|Description ---|--- -[Step1_Agent](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs)|How to create and use an agent. -[Step2_Plugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs)|How to associate plug-ins with an agent. -[Step3_Chat](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs)|How to create a conversation between agents. -[Step4_KernelFunctionStrategies](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs)|How to utilize a `KernelFunction` as a _chat strategy_. -[Step5_JsonResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs)|How to have an agent produce JSON. -[Step6_DependencyInjection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs)|How to define dependency injection patterns for agents. -[Step7_OpenAIAssistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step7_OpenAIAssistant.cs)|How to create an Open AI Assistant agent. +[Step01_Agent](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs)|How to create and use an agent. +[Step02_Plugins](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs)|How to associate plug-ins with an agent. +[Step03_Chat](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs)|How to create a conversation between agents. +[Step04_KernelFunctionStrategies](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs)|How to utilize a `KernelFunction` as a _chat strategy_. +[Step05_JsonResult](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs)|How to have an agent produce JSON. +[Step06_DependencyInjection](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs)|How to define dependency injection patterns for agents. +[Step07_Logging](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs)|How to enable logging for agents. +[Step08_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs)|How to create an Open AI Assistant agent. +[Step09_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs)|How to provide an image as input to an Open AI Assistant agent. +[Step10_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter_.cs)|How to use the code-interpreter tool for an Open AI Assistant agent. +[Step11_Assistant](https://github.com/microsoft/semantic-kernel/blob/main/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs)|How to use the file-search tool for an Open AI Assistant agent. ## Legacy Agents diff --git a/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg b/dotnet/samples/GettingStartedWithAgents/Resources/cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..1e9f26de48fc542676a7461020206fab297c0314 GIT binary patch literal 37831 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 X-`J9C)u%;8=#JWSR9(_<@Uj2d>mT>!jGU~c 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 tt_Qna&aFsUb~*nK+dRE#YYMVVf=Qm-$*ez@Z! literal 0 HcmV?d00001 diff --git a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs similarity index 76% rename from dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs rename to dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs index d7d4a0471b01..bc5bee5249e5 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step1_Agent.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step01_Agent.cs @@ -9,7 +9,7 @@ namespace GettingStarted; /// Demonstrate creation of and /// eliciting its response to three explicit user messages. /// -public class Step1_Agent(ITestOutputHelper output) : BaseTest(output) +public class Step01_Agent(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ParrotName = "Parrot"; private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; @@ -37,15 +37,15 @@ public async Task UseSingleChatCompletionAgentAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { - chat.Add(content); + chat.Add(response); - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs similarity index 76% rename from dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs rename to dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs index 7946adc7f687..29394991dcc4 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step2_Plugins.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step02_Plugins.cs @@ -11,7 +11,7 @@ namespace GettingStarted; /// Demonstrate creation of with a , /// and then eliciting its response to explicit user messages. /// -public class Step2_Plugins(ITestOutputHelper output) : BaseTest(output) +public class Step02_Plugins(ITestOutputHelper output) : BaseAgentsTest(output) { private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; @@ -45,37 +45,34 @@ public async Task UseChatCompletionWithPluginAgentAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.Add(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agent.InvokeAsync(chat)) + await foreach (ChatMessageContent response in agent.InvokeAsync(chat)) { - chat.Add(content); + chat.Add(response); - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } - public sealed class MenuPlugin + private sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } + public string GetSpecials() => + """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """; [KernelFunction, Description("Provides the price of the requested menu item.")] public string GetItemPrice( [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } + string menuItem) => + "$9.99"; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs similarity index 86% rename from dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs rename to dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs index 5d0c185f95f5..1ada85d512f3 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step3_Chat.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step03_Chat.cs @@ -11,7 +11,7 @@ namespace GettingStarted; /// that inform how chat proceeds with regards to: Agent selection, chat continuation, and maximum /// number of agent interactions. /// -public class Step3_Chat(ITestOutputHelper output) : BaseTest(output) +public class Step03_Chat(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -74,16 +74,16 @@ public async Task UseAgentGroupChatWithTwoAgentsAsync() }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy diff --git a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs similarity index 84% rename from dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs rename to dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs index 9cabe0193d3e..36424e6c268b 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step4_KernelFunctionStrategies.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step04_KernelFunctionStrategies.cs @@ -10,7 +10,7 @@ namespace GettingStarted; /// Demonstrate usage of and /// to manage execution. /// -public class Step4_KernelFunctionStrategies(ITestOutputHelper output) : BaseTest(output) +public class Step04_KernelFunctionStrategies(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -64,17 +64,18 @@ public async Task UseKernelFunctionStrategiesWithAgentGroupChatAsync() KernelFunction selectionFunction = KernelFunctionFactory.CreateFromPrompt( $$$""" - Your job is to determine which participant takes the next turn in a conversation according to the action of the most recent participant. + Determine which participant takes the next turn in a conversation based on the the most recent participant. State only the name of the participant to take the next turn. + No participant should take more than one turn in a row. Choose only from these participants: - {{{ReviewerName}}} - {{{CopyWriterName}}} Always follow these rules when selecting the next participant: - - After user input, it is {{{CopyWriterName}}}'a turn. - - After {{{CopyWriterName}}} replies, it is {{{ReviewerName}}}'s turn. - - After {{{ReviewerName}}} provides feedback, it is {{{CopyWriterName}}}'s turn. + - After user input, it is {{{CopyWriterName}}}'s turn. + - After {{{CopyWriterName}}}, it is {{{ReviewerName}}}'s turn. + - After {{{ReviewerName}}}, it is {{{CopyWriterName}}}'s turn. History: {{$history}} @@ -116,15 +117,15 @@ State only the name of the participant to take the next turn. }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent responese in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(responese); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs similarity index 79% rename from dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs rename to dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs index 20ad4c2096d4..8806c7d3b62d 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step5_JsonResult.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step05_JsonResult.cs @@ -10,14 +10,14 @@ namespace GettingStarted; /// /// Demonstrate parsing JSON response. /// -public class Step5_JsonResult(ITestOutputHelper output) : BaseTest(output) +public class Step05_JsonResult(ITestOutputHelper output) : BaseAgentsTest(output) { private const int ScoreCompletionThreshold = 70; private const string TutorName = "Tutor"; private const string TutorInstructions = """ - Think step-by-step and rate the user input on creativity and expressivness from 1-100. + Think step-by-step and rate the user input on creativity and expressiveness from 1-100. Respond in JSON format with the following JSON schema: @@ -60,19 +60,20 @@ public async Task UseKernelFunctionStrategiesWithJsonResultAsync() // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + this.WriteAgentChatMessage(response); + + Console.WriteLine($"[IS COMPLETED: {chat.IsComplete}]"); } } } - private record struct InputScore(int score, string notes); + private record struct WritingScore(int score, string notes); private sealed class ThresholdTerminationStrategy : TerminationStrategy { @@ -80,7 +81,7 @@ protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyLi { string lastMessageContent = history[history.Count - 1].Content ?? string.Empty; - InputScore? result = JsonResultTranslator.Translate(lastMessageContent); + WritingScore? result = JsonResultTranslator.Translate(lastMessageContent); return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs similarity index 65% rename from dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs rename to dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs index 21af5db70dce..a0d32f8cefba 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step6_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs @@ -3,23 +3,19 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; -using Resources; namespace GettingStarted; /// /// Demonstrate creation of an agent via dependency injection. /// -public class Step6_DependencyInjection(ITestOutputHelper output) : BaseTest(output) +public class Step06_DependencyInjection(ITestOutputHelper output) : BaseAgentsTest(output) { - private const int ScoreCompletionThreshold = 70; - private const string TutorName = "Tutor"; private const string TutorInstructions = """ - Think step-by-step and rate the user input on creativity and expressivness from 1-100. + Think step-by-step and rate the user input on creativity and expressiveness from 1-100. Respond in JSON format with the following JSON schema: @@ -80,50 +76,27 @@ public async Task UseDependencyInjectionToCreateAgentAsync() // Local function to invoke agent and display the conversation messages. async Task WriteAgentResponse(string input) { - Console.WriteLine($"# {AuthorRole.User}: {input}"); + ChatMessageContent message = new(AuthorRole.User, input); + this.WriteAgentChatMessage(message); - await foreach (ChatMessageContent content in agentClient.RunDemoAsync(input)) + await foreach (ChatMessageContent response in agentClient.RunDemoAsync(message)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } private sealed class AgentClient([FromKeyedServices(TutorName)] ChatCompletionAgent agent) { - private readonly AgentGroupChat _chat = - new() - { - ExecutionSettings = - new() - { - // Here a TerminationStrategy subclass is used that will terminate when - // the response includes a score that is greater than or equal to 70. - TerminationStrategy = new ThresholdTerminationStrategy() - } - }; - - public IAsyncEnumerable RunDemoAsync(string input) - { - // Create a chat for agent interaction. + private readonly AgentGroupChat _chat = new(); - this._chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + public IAsyncEnumerable RunDemoAsync(ChatMessageContent input) + { + this._chat.AddChatMessage(input); return this._chat.InvokeAsync(agent); } } - private record struct InputScore(int score, string notes); - - private sealed class ThresholdTerminationStrategy : TerminationStrategy - { - protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) - { - string lastMessageContent = history[history.Count - 1].Content ?? string.Empty; - - InputScore? result = JsonResultTranslator.Translate(lastMessageContent); - - return Task.FromResult((result?.score ?? 0) >= ScoreCompletionThreshold); - } - } + private record struct WritingScore(int score, string notes); } diff --git a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs similarity index 86% rename from dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs rename to dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs index 1ab559e668fb..3a48d407dea9 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step7_Logging.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step07_Logging.cs @@ -8,13 +8,13 @@ namespace GettingStarted; /// -/// A repeat of with logging enabled via assignment +/// A repeat of with logging enabled via assignment /// of a to . /// /// /// Samples become super noisy with logging always enabled. /// -public class Step7_Logging(ITestOutputHelper output) : BaseTest(output) +public class Step07_Logging(ITestOutputHelper output) : BaseAgentsTest(output) { private const string ReviewerName = "ArtDirector"; private const string ReviewerInstructions = @@ -81,16 +81,16 @@ public async Task UseLoggerFactoryWithAgentGroupChatAsync() }; // Invoke chat and display messages. - string input = "concept: maps made out of egg cartons."; - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); - await foreach (ChatMessageContent content in chat.InvokeAsync()) + await foreach (ChatMessageContent response in chat.InvokeAsync()) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } - Console.WriteLine($"# IS COMPLETE: {chat.IsComplete}"); + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } private sealed class ApprovalTerminationStrategy : TerminationStrategy diff --git a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs similarity index 57% rename from dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs rename to dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs index d9e9760e3fa6..ba4ab065c2a6 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step8_OpenAIAssistant.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step08_Assistant.cs @@ -8,36 +8,35 @@ namespace GettingStarted; /// -/// This example demonstrates that outside of initialization (and cleanup), using -/// is no different from -/// even with with a . +/// This example demonstrates similarity between using +/// and (see: Step 2). /// -public class Step8_OpenAIAssistant(ITestOutputHelper output) : BaseTest(output) +public class Step08_Assistant(ITestOutputHelper output) : BaseAgentsTest(output) { private const string HostName = "Host"; private const string HostInstructions = "Answer questions about the menu."; [Fact] - public async Task UseSingleOpenAIAssistantAgentAsync() + public async Task UseSingleAssistantAgentAsync() { // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + clientProvider: this.GetClientProvider(), + new(this.Model) { Instructions = HostInstructions, Name = HostName, - ModelId = this.Model, + Metadata = AssistantSampleMetadata, }); // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). KernelPlugin plugin = KernelPluginFactory.CreateFromType(); agent.Kernel.Plugins.Add(plugin); - // Create a thread for the agent interaction. - string threadId = await agent.CreateThreadAsync(); + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); // Respond to user input try @@ -56,45 +55,32 @@ await OpenAIAssistantAgent.CreateAsync( // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - await agent.AddChatMessageAsync(threadId, new ChatMessageContent(AuthorRole.User, input)); + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); - - await foreach (ChatMessageContent content in agent.InvokeAsync(threadId)) + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) { - if (content.Role != AuthorRole.Tool) - { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); - } + this.WriteAgentChatMessage(response); } } } private sealed class MenuPlugin { - public const string CorrelationIdArgument = "correlationId"; - - private readonly List _correlationIds = []; - - public IReadOnlyList CorrelationIds => this._correlationIds; - [KernelFunction, Description("Provides a list of specials from the menu.")] [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] - public string GetSpecials() - { - return @" -Special Soup: Clam Chowder -Special Salad: Cobb Salad -Special Drink: Chai Tea -"; - } + public string GetSpecials() => + """ + Special Soup: Clam Chowder + Special Salad: Cobb Salad + Special Drink: Chai Tea + """; [KernelFunction, Description("Provides the price of the requested menu item.")] public string GetItemPrice( [Description("The name of the menu item.")] - string menuItem) - { - return "$9.99"; - } + string menuItem) => + "$9.99"; } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs new file mode 100644 index 000000000000..62845f2c4366 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step09_Assistant_Vision.cs @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Resources; + +namespace GettingStarted; + +/// +/// Demonstrate providing image input to . +/// +public class Step09_Assistant_Vision(ITestOutputHelper output) : BaseAgentsTest(output) +{ + /// + /// Azure currently only supports message of type=text. + /// + protected override bool ForceOpenAI => true; + + [Fact] + public async Task UseSingleAssistantAgentAsync() + { + // Define the agent + OpenAIClientProvider provider = this.GetClientProvider(); + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + provider, + new(this.Model) + { + Metadata = AssistantSampleMetadata, + }); + + // Upload an image + await using Stream imageStream = EmbeddedResource.ReadStream("cat.jpg")!; + string fileId = await agent.UploadFileAsync(imageStream, "cat.jpg"); + + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); + + // Respond to user input + try + { + // Refer to public image by url + await InvokeAgentAsync(CreateMessageWithImageUrl("Describe this image.", "https://upload.wikimedia.org/wikipedia/commons/thumb/4/47/New_york_times_square-terabass.jpg/1200px-New_york_times_square-terabass.jpg")); + await InvokeAgentAsync(CreateMessageWithImageUrl("What are is the main color in this image?", "https://upload.wikimedia.org/wikipedia/commons/5/56/White_shark.jpg")); + // Refer to uploaded image by file-id. + await InvokeAgentAsync(CreateMessageWithImageReference("Is there an animal in this image?", fileId)); + } + finally + { + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); + await provider.Client.GetFileClient().DeleteFileAsync(fileId); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(ChatMessageContent message) + { + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + this.WriteAgentChatMessage(response); + } + } + } + + private ChatMessageContent CreateMessageWithImageUrl(string input, string url) + => new(AuthorRole.User, [new TextContent(input), new ImageContent(new Uri(url))]); + + private ChatMessageContent CreateMessageWithImageReference(string input, string fileId) + => new(AuthorRole.User, [new TextContent(input), new FileReferenceContent(fileId)]); +} diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs similarity index 50% rename from dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs rename to dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs index 75b237489025..1205771d66be 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_CodeInterpreter.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step10_AssistantTool_CodeInterpreter.cs @@ -1,34 +1,31 @@ // Copyright (c) Microsoft. All rights reserved. using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -namespace Agents; +namespace GettingStarted; /// /// Demonstrate using code-interpreter on . /// -public class OpenAIAssistant_CodeInterpreter(ITestOutputHelper output) : BaseTest(output) +public class Step10_AssistantTool_CodeInterpreter(ITestOutputHelper output) : BaseAgentsTest(output) { - protected override bool ForceOpenAI => true; - [Fact] - public async Task UseCodeInterpreterToolWithOpenAIAssistantAgentAsync() + public async Task UseCodeInterpreterToolWithAssistantAgentAsync() { // Define the agent OpenAIAssistantAgent agent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + clientProvider: this.GetClientProvider(), + new(this.Model) { - EnableCodeInterpreter = true, // Enable code-interpreter - ModelId = this.Model, + EnableCodeInterpreter = true, + Metadata = AssistantSampleMetadata, }); - // Create a chat for agent interaction. - AgentGroupChat chat = new(); + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); // Respond to user input try @@ -37,19 +34,20 @@ await OpenAIAssistantAgent.CreateAsync( } finally { + await agent.DeleteThreadAsync(threadId); await agent.DeleteAsync(); } // Local function to invoke agent and display the conversation messages. async Task InvokeAgentAsync(string input) { - chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); - - Console.WriteLine($"# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); - await foreach (var content in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) { - Console.WriteLine($"# {content.Role} - {content.AuthorName ?? "*"}: '{content.Content}'"); + this.WriteAgentChatMessage(response); } } } diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs new file mode 100644 index 000000000000..d34cadaf3707 --- /dev/null +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -0,0 +1,83 @@ +// Copyright (c) Microsoft. All rights reserved. +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; +using OpenAI.VectorStores; +using Resources; + +namespace GettingStarted; + +/// +/// Demonstrate using code-interpreter on . +/// +public class Step11_AssistantTool_FileSearch(ITestOutputHelper output) : BaseAgentsTest(output) +{ + [Fact] + public async Task UseFileSearchToolWithAssistantAgentAsync() + { + // Define the agent + OpenAIClientProvider provider = this.GetClientProvider(); + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + clientProvider: this.GetClientProvider(), + new(this.Model) + { + EnableFileSearch = true, + Metadata = AssistantSampleMetadata, + }); + + // Upload file - Using a table of fictional employees. + FileClient fileClient = provider.Client.GetFileClient(); + await using Stream stream = EmbeddedResource.ReadStream("employees.pdf")!; + OpenAIFileInfo fileInfo = await fileClient.UploadFileAsync(stream, "employees.pdf", FileUploadPurpose.Assistants); + + // Create a vector-store + VectorStoreClient vectorStoreClient = provider.Client.GetVectorStoreClient(); + VectorStore vectorStore = + await vectorStoreClient.CreateVectorStoreAsync( + new VectorStoreCreationOptions() + { + FileIds = [fileInfo.Id], + Metadata = { { AssistantSampleMetadataKey, bool.TrueString } } + }); + + // Create a thread associated with a vector-store for the agent conversation. + string threadId = + await agent.CreateThreadAsync( + new OpenAIThreadCreationOptions + { + VectorStoreId = vectorStore.Id, + Metadata = AssistantSampleMetadata, + }); + + // Respond to user input + try + { + await InvokeAgentAsync("Who is the youngest employee?"); + await InvokeAgentAsync("Who works in sales?"); + await InvokeAgentAsync("I have a customer request, who can help me?"); + } + finally + { + await agent.DeleteThreadAsync(threadId); + await agent.DeleteAsync(); + await vectorStoreClient.DeleteVectorStoreAsync(vectorStore); + await fileClient.DeleteFileAsync(fileInfo); + } + + // Local function to invoke agent and display the conversation messages. + async Task InvokeAgentAsync(string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + await foreach (ChatMessageContent response in agent.InvokeAsync(threadId)) + { + this.WriteAgentChatMessage(response); + } + } + } +} diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 9788464a2adb..73469ed723b5 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -31,6 +31,10 @@ public abstract class AgentChannel /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. + /// + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. + /// protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( Agent agent, CancellationToken cancellationToken = default); @@ -59,6 +63,10 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent /// The agent actively interacting with the chat. /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. + /// + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. + /// protected internal abstract IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( TAgent agent, CancellationToken cancellationToken = default); diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index f4654963444e..ca6cbdaab259 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -285,7 +285,7 @@ private void ClearActivitySignal() /// The activity signal is used to manage ability and visibility for taking actions based /// on conversation history. /// - private void SetActivityOrThrow() + protected void SetActivityOrThrow() { // Note: Interlocked is the absolute lightest synchronization mechanism available in dotnet. int wasActive = Interlocked.CompareExchange(ref this._isActive, 1, 0); diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 73561a4eba8b..0c6bc252891d 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -13,11 +13,13 @@ internal sealed class AggregatorChannel(AgentChat chat) : AgentChannel protected internal override IAsyncEnumerable GetHistoryAsync(CancellationToken cancellationToken = default) { return this._chat.GetChatMessagesAsync(cancellationToken); } + /// protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(AggregatorAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) { ChatMessageContent? lastMessage = null; @@ -47,6 +49,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync } } + /// protected internal override Task ReceiveAsync(IEnumerable history, CancellationToken cancellationToken = default) { // Always receive the initial history from the owning chat. diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs index 314d68ce8cd8..b971fe2ce8d4 100644 --- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs +++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs @@ -61,7 +61,7 @@ public static partial void LogAgentChatAddingMessages( [LoggerMessage( EventId = 0, Level = LogLevel.Information, - Message = "[{MethodName}] Adding Messages: {MessageCount}.")] + Message = "[{MethodName}] Added Messages: {MessageCount}.")] public static partial void LogAgentChatAddedMessages( this ILogger logger, string methodName, diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index 3423308325c2..91f5b864e725 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -38,7 +38,7 @@ public async IAsyncEnumerable InvokeAsync( kernel ??= this.Kernel; arguments ??= this.Arguments; - (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments); + (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); ChatHistory chat = this.SetupAgentChatHistory(history); @@ -65,7 +65,7 @@ await chatCompletionService.GetChatMessageContentsAsync( history.Add(message); } - foreach (ChatMessageContent message in messages ?? []) + foreach (ChatMessageContent message in messages) { message.AuthorName = this.Name; @@ -83,7 +83,7 @@ public async IAsyncEnumerable InvokeStreamingAsync( kernel ??= this.Kernel; arguments ??= this.Arguments; - (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = this.GetChatCompletionService(kernel, arguments); + (IChatCompletionService chatCompletionService, PromptExecutionSettings? executionSettings) = GetChatCompletionService(kernel, arguments); ChatHistory chat = this.SetupAgentChatHistory(history); @@ -121,6 +121,9 @@ public async IAsyncEnumerable InvokeStreamingAsync( /// protected override IEnumerable GetChannelKeys() { + // Distinguish from other channel types. + yield return typeof(ChatHistoryChannel).FullName!; + // Agents with different reducers shall not share the same channel. // Agents with the same or equivalent reducer shall share the same channel. if (this.HistoryReducer != null) @@ -145,7 +148,7 @@ protected override Task CreateChannelAsync(CancellationToken cance return Task.FromResult(channel); } - private (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) + internal static (IChatCompletionService service, PromptExecutionSettings? executionSettings) GetChatCompletionService(Kernel kernel, KernelArguments? arguments) { // Need to provide a KernelFunction to the service selector as a container for the execution-settings. KernelFunction nullPrompt = KernelFunctionFactory.CreateFromPrompt("placeholder", arguments?.ExecutionSettings?.Values); diff --git a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs index a45bfa57011d..8c2f022830d1 100644 --- a/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs +++ b/dotnet/src/Agents/Core/History/ChatHistorySummarizationReducer.cs @@ -80,7 +80,7 @@ Provide a concise and complete summarizion of the entire dialog that does not ex IEnumerable summarizedHistory = history.Extract( this.UseSingleSummary ? 0 : insertionPoint, - truncationIndex, + truncationIndex - 1, (m) => m.Items.Any(i => i is FunctionCallContent || i is FunctionResultContent)); try @@ -154,7 +154,9 @@ public override bool Equals(object? obj) ChatHistorySummarizationReducer? other = obj as ChatHistorySummarizationReducer; return other != null && this._thresholdCount == other._thresholdCount && - this._targetCount == other._targetCount; + this._targetCount == other._targetCount && + this.UseSingleSummary == other.UseSingleSummary && + string.Equals(this.SummarizationInstructions, other.SummarizationInstructions, StringComparison.Ordinal); } /// diff --git a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj index 22db4073d90a..a5a4cde76d6f 100644 --- a/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj +++ b/dotnet/src/Agents/OpenAI/Agents.OpenAI.csproj @@ -19,6 +19,7 @@ + @@ -28,12 +29,11 @@ - - + diff --git a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs index cd4e80c3abf1..895482927515 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/AuthorRoleExtensions.cs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs index 9665fb680498..97a439729ff3 100644 --- a/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs +++ b/dotnet/src/Agents/OpenAI/Extensions/KernelFunctionExtensions.cs @@ -2,7 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Azure.AI.OpenAI.Assistants; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -13,9 +13,8 @@ internal static class KernelFunctionExtensions /// /// The source function /// The plugin name - /// The delimiter character /// An OpenAI tool definition - public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName, string delimiter) + public static FunctionToolDefinition ToToolDefinition(this KernelFunction function, string pluginName) { var metadata = function.Metadata; if (metadata.Parameters.Count > 0) @@ -47,10 +46,10 @@ public static FunctionToolDefinition ToToolDefinition(this KernelFunction functi required, }; - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description, BinaryData.FromObjectAsJson(spec)); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description, BinaryData.FromObjectAsJson(spec)); } - return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName, delimiter), function.Description); + return new FunctionToolDefinition(FunctionName.ToFullyQualifiedName(function.Name, pluginName), function.Description); } private static string ConvertType(Type? type) diff --git a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs similarity index 87% rename from dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs rename to dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs index 084e533fe757..d017fb403f23 100644 --- a/dotnet/src/Agents/OpenAI/Azure/AddHeaderRequestPolicy.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AddHeaderRequestPolicy.cs @@ -2,7 +2,7 @@ using Azure.Core; using Azure.Core.Pipeline; -namespace Microsoft.SemanticKernel.Agents.OpenAI.Azure; +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; /// /// Helper class to inject headers into Azure SDK HTTP pipeline diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs new file mode 100644 index 000000000000..4c31a1bcf291 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantMessageFactory.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using OpenAI.Assistants; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +/// +/// Factory for creating based on . +/// Also able to produce . +/// +/// +/// Improves testability. +/// +internal static class AssistantMessageFactory +{ + /// + /// Produces based on . + /// + /// The message content. + public static MessageCreationOptions CreateOptions(ChatMessageContent message) + { + MessageCreationOptions options = new(); + + if (message.Metadata != null) + { + foreach (var metadata in message.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value?.ToString() ?? string.Empty); + } + } + + return options; + } + + /// + /// Translates into enumeration of . + /// + /// The message content. + public static IEnumerable GetMessageContents(ChatMessageContent message) + { + foreach (KernelContent content in message.Items) + { + if (content is TextContent textContent) + { + yield return MessageContent.FromText(content.ToString()); + } + else if (content is ImageContent imageContent) + { + if (imageContent.Uri != null) + { + yield return MessageContent.FromImageUrl(imageContent.Uri); + } + else if (string.IsNullOrWhiteSpace(imageContent.DataUri)) + { + yield return MessageContent.FromImageUrl(new(imageContent.DataUri!)); + } + } + else if (content is FileReferenceContent fileContent) + { + yield return MessageContent.FromImageFileId(fileContent.FileId); + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs new file mode 100644 index 000000000000..981c646254af --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantRunOptionsFactory.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using OpenAI.Assistants; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +/// +/// Factory for creating definition. +/// +/// +/// Improves testability. +/// +internal static class AssistantRunOptionsFactory +{ + /// + /// Produce by reconciling and . + /// + /// The assistant definition + /// The run specific options + public static RunCreationOptions GenerateOptions(OpenAIAssistantDefinition definition, OpenAIAssistantInvocationOptions? invocationOptions) + { + int? truncationMessageCount = ResolveExecutionSetting(invocationOptions?.TruncationMessageCount, definition.ExecutionOptions?.TruncationMessageCount); + + RunCreationOptions options = + new() + { + MaxCompletionTokens = ResolveExecutionSetting(invocationOptions?.MaxCompletionTokens, definition.ExecutionOptions?.MaxCompletionTokens), + MaxPromptTokens = ResolveExecutionSetting(invocationOptions?.MaxPromptTokens, definition.ExecutionOptions?.MaxPromptTokens), + ModelOverride = invocationOptions?.ModelName, + NucleusSamplingFactor = ResolveExecutionSetting(invocationOptions?.TopP, definition.TopP), + ParallelToolCallsEnabled = ResolveExecutionSetting(invocationOptions?.ParallelToolCallsEnabled, definition.ExecutionOptions?.ParallelToolCallsEnabled), + ResponseFormat = ResolveExecutionSetting(invocationOptions?.EnableJsonResponse, definition.EnableJsonResponse) ?? false ? AssistantResponseFormat.JsonObject : null, + Temperature = ResolveExecutionSetting(invocationOptions?.Temperature, definition.Temperature), + TruncationStrategy = truncationMessageCount.HasValue ? RunTruncationStrategy.CreateLastMessagesStrategy(truncationMessageCount.Value) : null, + }; + + if (invocationOptions?.Metadata != null) + { + foreach (var metadata in invocationOptions.Metadata) + { + options.Metadata.Add(metadata.Key, metadata.Value ?? string.Empty); + } + } + + return options; + } + + private static TValue? ResolveExecutionSetting(TValue? setting, TValue? agentSetting) where TValue : struct + => + setting.HasValue && (!agentSetting.HasValue || !EqualityComparer.Default.Equals(setting.Value, agentSetting.Value)) ? + setting.Value : + null; +} diff --git a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs similarity index 68% rename from dotnet/src/Agents/OpenAI/AssistantThreadActions.cs rename to dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index cfc7a905cfc7..d66f54917d3f 100644 --- a/dotnet/src/Agents/OpenAI/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -7,19 +7,18 @@ using System.Threading; using System.Threading.Tasks; using Azure; -using Azure.AI.OpenAI.Assistants; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI; +using OpenAI.Assistants; -namespace Microsoft.SemanticKernel.Agents.OpenAI; +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; /// /// Actions associated with an Open Assistant thread. /// internal static class AssistantThreadActions { - private const string FunctionDelimiter = "-"; - private static readonly HashSet s_pollingStatuses = [ RunStatus.Queued, @@ -34,6 +33,43 @@ internal static class AssistantThreadActions RunStatus.Cancelled, ]; + /// + /// Create a new assistant thread. + /// + /// The assistant client + /// The options for creating the thread + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public static async Task CreateThreadAsync(AssistantClient client, OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) + { + ThreadCreationOptions createOptions = + new() + { + ToolResources = AssistantToolResourcesFactory.GenerateToolResources(options?.VectorStoreId, options?.CodeInterpreterFileIds), + }; + + if (options?.Messages is not null) + { + foreach (ChatMessageContent message in options.Messages) + { + ThreadInitializationMessage threadMessage = new(AssistantMessageFactory.GetMessageContents(message)); + createOptions.InitialMessages.Add(threadMessage); + } + } + + if (options?.Metadata != null) + { + foreach (KeyValuePair item in options.Metadata) + { + createOptions.Metadata[item.Key] = item.Value; + } + } + + AssistantThread thread = await client.CreateThreadAsync(createOptions, cancellationToken).ConfigureAwait(false); + + return thread.Id; + } + /// /// Create a message in the specified thread. /// @@ -42,18 +78,20 @@ internal static class AssistantThreadActions /// The message to add /// The to monitor for cancellation requests. The default is . /// if a system message is present, without taking any other action - public static async Task CreateMessageAsync(AssistantsClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) + public static async Task CreateMessageAsync(AssistantClient client, string threadId, ChatMessageContent message, CancellationToken cancellationToken) { if (message.Items.Any(i => i is FunctionCallContent)) { return; } + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); + await client.CreateMessageAsync( threadId, - message.Role.ToMessageRole(), - message.Content, - cancellationToken: cancellationToken).ConfigureAwait(false); + AssistantMessageFactory.GetMessageContents(message), + options, + cancellationToken).ConfigureAwait(false); } /// @@ -63,51 +101,45 @@ await client.CreateMessageAsync( /// The thread identifier /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. - public static async IAsyncEnumerable GetMessagesAsync(AssistantsClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) + public static async IAsyncEnumerable GetMessagesAsync(AssistantClient client, string threadId, [EnumeratorCancellation] CancellationToken cancellationToken) { Dictionary agentNames = []; // Cache agent names by their identifier - PageableList messages; - - string? lastId = null; - do + await foreach (ThreadMessage message in client.GetMessagesAsync(threadId, ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) { - messages = await client.GetMessagesAsync(threadId, limit: 100, ListSortOrder.Descending, after: lastId, null, cancellationToken).ConfigureAwait(false); - foreach (ThreadMessage message in messages) + AuthorRole role = new(message.Role.ToString()); + + string? assistantName = null; + if (!string.IsNullOrWhiteSpace(message.AssistantId) && + !agentNames.TryGetValue(message.AssistantId, out assistantName)) { - string? assistantName = null; - if (!string.IsNullOrWhiteSpace(message.AssistantId) && - !agentNames.TryGetValue(message.AssistantId, out assistantName)) + Assistant assistant = await client.GetAssistantAsync(message.AssistantId).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) + if (!string.IsNullOrWhiteSpace(assistant.Name)) { - Assistant assistant = await client.GetAssistantAsync(message.AssistantId, cancellationToken).ConfigureAwait(false); - if (!string.IsNullOrWhiteSpace(assistant.Name)) - { - agentNames.Add(assistant.Id, assistant.Name); - } + agentNames.Add(assistant.Id, assistant.Name); } + } - assistantName ??= message.AssistantId; - - ChatMessageContent content = GenerateMessageContent(assistantName, message); + assistantName ??= message.AssistantId; - if (content.Items.Count > 0) - { - yield return content; - } + ChatMessageContent content = GenerateMessageContent(assistantName, message); - lastId = message.Id; + if (content.Items.Count > 0) + { + yield return content; } } - while (messages.HasMore); } /// /// Invoke the assistant on the specified thread. + /// In the enumeration returned by this method, a message is considered visible if it is intended to be displayed to the user. + /// Example of a non-visible message is function-content for functions that are automatically executed. /// /// The assistant agent to interact with the thread. /// The assistant client /// The thread identifier - /// Config to utilize when polling for run state. + /// Options to utilize for the invocation /// The logger to utilize (might be agent or channel scoped) /// The plugins and other state. /// Optional arguments to pass to the agents's invocation, including any . @@ -118,9 +150,9 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist /// public static async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync( OpenAIAssistantAgent agent, - AssistantsClient client, + AssistantClient client, string threadId, - OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration, + OpenAIAssistantInvocationOptions? invocationOptions, ILogger logger, Kernel kernel, KernelArguments? arguments, @@ -131,19 +163,15 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); } - ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name, FunctionDelimiter)))]; - logger.LogOpenAIAssistantCreatingRun(nameof(InvokeAsync), threadId); - CreateRunOptions options = - new(agent.Id) - { - OverrideInstructions = agent.Instructions, - OverrideTools = tools, - }; + ToolDefinition[]? tools = [.. agent.Tools, .. kernel.Plugins.SelectMany(p => p.Select(f => f.ToToolDefinition(p.Name)))]; + + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(agent.Definition, invocationOptions); + + options.ToolsOverride.AddRange(tools); - // Create run - ThreadRun run = await client.CreateRunAsync(threadId, options, cancellationToken).ConfigureAwait(false); + ThreadRun run = await client.CreateRunAsync(threadId, agent.Id, options, cancellationToken).ConfigureAwait(false); logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId); @@ -154,7 +182,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist do { // Poll run and steps until actionable - PageableList steps = await PollRunStatusAsync().ConfigureAwait(false); + await PollRunStatusAsync().ConfigureAwait(false); // Is in terminal state? if (s_terminalStatuses.Contains(run.Status)) @@ -162,13 +190,15 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } + RunStep[] steps = await client.GetRunStepsAsync(run).ToArrayAsync(cancellationToken).ConfigureAwait(false); + // Is tool action required? if (run.Status == RunStatus.RequiresAction) { logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); // Execute functions in parallel and post results at once. - FunctionCallContent[] activeFunctionSteps = steps.Data.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + FunctionCallContent[] activeFunctionSteps = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); if (activeFunctionSteps.Length > 0) { // Emit function-call content @@ -183,7 +213,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist // Process tool output ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - await client.SubmitToolOutputsToRunAsync(run, toolOutputs, cancellationToken).ConfigureAwait(false); + await client.SubmitToolOutputsToRunAsync(threadId, run.Id, toolOutputs, cancellationToken).ConfigureAwait(false); } logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), activeFunctionSteps.Length, run.Id, threadId); @@ -200,26 +230,24 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist int messageCount = 0; foreach (RunStep completedStep in completedStepsToProcess) { - if (completedStep.Type.Equals(RunStepType.ToolCalls)) + if (completedStep.Type == RunStepType.ToolCalls) { - RunStepToolCallDetails toolCallDetails = (RunStepToolCallDetails)completedStep.StepDetails; - - foreach (RunStepToolCall toolCall in toolCallDetails.ToolCalls) + foreach (RunStepToolCall toolCall in completedStep.Details.ToolCalls) { bool isVisible = false; ChatMessageContent? content = null; // Process code-interpreter content - if (toolCall is RunStepCodeInterpreterToolCall toolCodeInterpreter) + if (toolCall.ToolKind == RunStepToolCallKind.CodeInterpreter) { - content = GenerateCodeInterpreterContent(agent.GetName(), toolCodeInterpreter); + content = GenerateCodeInterpreterContent(agent.GetName(), toolCall.CodeInterpreterInput); isVisible = true; } // Process function result content - else if (toolCall is RunStepFunctionToolCall toolFunction) + else if (toolCall.ToolKind == RunStepToolCallKind.Function) { - FunctionCallContent functionStep = functionSteps[toolFunction.Id]; // Function step always captured on invocation - content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolFunction.Output); + FunctionCallContent functionStep = functionSteps[toolCall.ToolCallId]; // Function step always captured on invocation + content = GenerateFunctionResultContent(agent.GetName(), functionStep, toolCall.FunctionOutput); } if (content is not null) @@ -230,12 +258,10 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist } } } - else if (completedStep.Type.Equals(RunStepType.MessageCreation)) + else if (completedStep.Type == RunStepType.MessageCreation) { - RunStepMessageCreationDetails messageCreationDetails = (RunStepMessageCreationDetails)completedStep.StepDetails; - // Retrieve the message - ThreadMessage? message = await RetrieveMessageAsync(messageCreationDetails, cancellationToken).ConfigureAwait(false); + ThreadMessage? message = await RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false); if (message is not null) { @@ -260,7 +286,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run.Id, threadId); // Local function to assist in run polling (participates in method closure). - async Task> PollRunStatusAsync() + async Task PollRunStatusAsync() { logger.LogOpenAIAssistantPollingRunStatus(nameof(PollRunStatusAsync), run.Id, threadId); @@ -269,7 +295,7 @@ async Task> PollRunStatusAsync() do { // Reduce polling frequency after a couple attempts - await Task.Delay(count >= 2 ? pollingConfiguration.RunPollingInterval : pollingConfiguration.RunPollingBackoff, cancellationToken).ConfigureAwait(false); + await Task.Delay(agent.PollingOptions.GetPollingInterval(count), cancellationToken).ConfigureAwait(false); ++count; #pragma warning disable CA1031 // Do not catch general exception types @@ -286,39 +312,37 @@ async Task> PollRunStatusAsync() while (s_pollingStatuses.Contains(run.Status)); logger.LogOpenAIAssistantPolledRunStatus(nameof(PollRunStatusAsync), run.Status, run.Id, threadId); - - return await client.GetRunStepsAsync(run, cancellationToken: cancellationToken).ConfigureAwait(false); } // Local function to capture kernel function state for further processing (participates in method closure). IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) { - if (step.Status == RunStepStatus.InProgress && step.StepDetails is RunStepToolCallDetails callDetails) + if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls) { - foreach (RunStepFunctionToolCall toolCall in callDetails.ToolCalls.OfType()) + foreach (RunStepToolCall toolCall in step.Details.ToolCalls) { - var nameParts = FunctionName.Parse(toolCall.Name, FunctionDelimiter); + var nameParts = FunctionName.Parse(toolCall.FunctionName); KernelArguments functionArguments = []; - if (!string.IsNullOrWhiteSpace(toolCall.Arguments)) + if (!string.IsNullOrWhiteSpace(toolCall.FunctionArguments)) { - Dictionary arguments = JsonSerializer.Deserialize>(toolCall.Arguments)!; + Dictionary arguments = JsonSerializer.Deserialize>(toolCall.FunctionArguments)!; foreach (var argumentKvp in arguments) { functionArguments[argumentKvp.Key] = argumentKvp.Value.ToString(); } } - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.Id, functionArguments); + var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - functionSteps.Add(toolCall.Id, content); + functionSteps.Add(toolCall.ToolCallId, content); yield return content; } } } - async Task RetrieveMessageAsync(RunStepMessageCreationDetails detail, CancellationToken cancellationToken) + async Task RetrieveMessageAsync(string messageId, CancellationToken cancellationToken) { ThreadMessage? message = null; @@ -328,7 +352,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R { try { - message = await client.GetMessageAsync(threadId, detail.MessageCreation.MessageId, cancellationToken).ConfigureAwait(false); + message = await client.GetMessageAsync(threadId, messageId, cancellationToken).ConfigureAwait(false); } catch (RequestFailedException exception) { @@ -340,7 +364,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R if (retry) { - await Task.Delay(pollingConfiguration.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); + await Task.Delay(agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); } ++count; @@ -361,57 +385,58 @@ private static ChatMessageContent GenerateMessageContent(string? assistantName, AuthorName = assistantName, }; - foreach (MessageContent itemContent in message.ContentItems) + foreach (MessageContent itemContent in message.Content) { // Process text content - if (itemContent is MessageTextContent contentMessage) + if (!string.IsNullOrEmpty(itemContent.Text)) { - content.Items.Add(new TextContent(contentMessage.Text.Trim())); + content.Items.Add(new TextContent(itemContent.Text)); - foreach (MessageTextAnnotation annotation in contentMessage.Annotations) + foreach (TextAnnotation annotation in itemContent.TextAnnotations) { content.Items.Add(GenerateAnnotationContent(annotation)); } } // Process image content - else if (itemContent is MessageImageFileContent contentImage) + else if (itemContent.ImageFileId != null) { - content.Items.Add(new FileReferenceContent(contentImage.FileId)); + content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); } } return content; } - private static AnnotationContent GenerateAnnotationContent(MessageTextAnnotation annotation) + private static AnnotationContent GenerateAnnotationContent(TextAnnotation annotation) { string? fileId = null; - if (annotation is MessageTextFileCitationAnnotation citationAnnotation) + + if (!string.IsNullOrEmpty(annotation.OutputFileId)) { - fileId = citationAnnotation.FileId; + fileId = annotation.OutputFileId; } - else if (annotation is MessageTextFilePathAnnotation pathAnnotation) + else if (!string.IsNullOrEmpty(annotation.InputFileId)) { - fileId = pathAnnotation.FileId; + fileId = annotation.InputFileId; } return new() { - Quote = annotation.Text, + Quote = annotation.TextToReplace, StartIndex = annotation.StartIndex, EndIndex = annotation.EndIndex, FileId = fileId, }; } - private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, RunStepCodeInterpreterToolCall contentCodeInterpreter) + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string pythonCode) { return new ChatMessageContent( AuthorRole.Assistant, [ - new TextContent(contentCodeInterpreter.Input) + new TextContent(pythonCode) ]) { AuthorName = agentName, diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs new file mode 100644 index 000000000000..6874e1d21755 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantToolResourcesFactory.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using OpenAI.Assistants; + +namespace Microsoft.SemanticKernel.Agents.OpenAI.Internal; + +/// +/// Factory for creating definition. +/// +/// +/// Improves testability. +/// +internal static class AssistantToolResourcesFactory +{ + /// + /// Produces a definition based on the provided parameters. + /// + /// An optional vector-store-id for the 'file_search' tool + /// An optionallist of file-identifiers for the 'code_interpreter' tool. + public static ToolResources? GenerateToolResources(string? vectorStoreId, IReadOnlyList? codeInterpreterFileIds) + { + bool hasVectorStore = !string.IsNullOrWhiteSpace(vectorStoreId); + bool hasCodeInterpreterFiles = (codeInterpreterFileIds?.Count ?? 0) > 0; + + ToolResources? toolResources = null; + + if (hasVectorStore || hasCodeInterpreterFiles) + { + toolResources = + new ToolResources() + { + FileSearch = + hasVectorStore ? + new FileSearchToolResources() + { + VectorStoreIds = [vectorStoreId!], + } : + null, + CodeInterpreter = + hasCodeInterpreterFiles ? + new CodeInterpreterToolResources() + { + FileIds = (IList)codeInterpreterFileIds!, + } : + null, + }; + } + + return toolResources; + } +} diff --git a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs index bc7c8d9919f0..3a39c314c5c3 100644 --- a/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs +++ b/dotnet/src/Agents/OpenAI/Logging/AssistantThreadActionsLogMessages.cs @@ -1,7 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. using System.Diagnostics.CodeAnalysis; -using Azure.AI.OpenAI.Assistants; using Microsoft.Extensions.Logging; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 6746c6c50d9a..f5c4a3588cf8 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,17 +1,16 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Azure; -using Azure.AI.OpenAI.Assistants; -using Azure.Core; -using Azure.Core.Pipeline; using Microsoft.Extensions.Logging; -using Microsoft.SemanticKernel.Agents.OpenAI.Azure; -using Microsoft.SemanticKernel.Http; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using OpenAI; +using OpenAI.Assistants; +using OpenAI.Files; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -25,9 +24,12 @@ public sealed class OpenAIAssistantAgent : KernelAgent /// public const string CodeInterpreterMetadataKey = "code"; + internal const string OptionsMetadataKey = "__run_options"; + + private readonly OpenAIClientProvider _provider; private readonly Assistant _assistant; - private readonly AssistantsClient _client; - private readonly OpenAIAssistantConfiguration _config; + private readonly AssistantClient _client; + private readonly string[] _channelKeys; /// /// Optional arguments for the agent. @@ -38,57 +40,55 @@ public sealed class OpenAIAssistantAgent : KernelAgent public KernelArguments? Arguments { get; init; } /// - /// A list of previously uploaded file IDs to attach to the assistant. + /// The assistant definition. /// - public IReadOnlyList FileIds => this._assistant.FileIds; + public OpenAIAssistantDefinition Definition { get; private init; } /// - /// A set of up to 16 key/value pairs that can be attached to an agent, used for - /// storing additional information about that object in a structured format.Keys - /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// Set when the assistant has been deleted via . + /// An assistant removed by other means will result in an exception when invoked. /// - public IReadOnlyDictionary Metadata => this._assistant.Metadata; + public bool IsDeleted { get; private set; } /// - /// Expose predefined tools. + /// Defines polling behavior for run processing /// - internal IReadOnlyList Tools => this._assistant.Tools; + public RunPollingOptions PollingOptions { get; } = new(); /// - /// Set when the assistant has been deleted via . - /// An assistant removed by other means will result in an exception when invoked. + /// Expose predefined tools for run-processing. /// - public bool IsDeleted { get; private set; } + internal IReadOnlyList Tools => this._assistant.Tools; /// /// Define a new . /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service, such as the api-key. + /// OpenAI client provider for accessing the API service. /// The assistant definition. /// The to monitor for cancellation requests. The default is . /// An instance public static async Task CreateAsync( Kernel kernel, - OpenAIAssistantConfiguration config, + OpenAIClientProvider clientProvider, OpenAIAssistantDefinition definition, CancellationToken cancellationToken = default) { // Validate input Verify.NotNull(kernel, nameof(kernel)); - Verify.NotNull(config, nameof(config)); + Verify.NotNull(clientProvider, nameof(clientProvider)); Verify.NotNull(definition, nameof(definition)); // Create the client - AssistantsClient client = CreateClient(config); + AssistantClient client = CreateClient(clientProvider); // Create the assistant AssistantCreationOptions assistantCreationOptions = CreateAssistantCreationOptions(definition); - Assistant model = await client.CreateAssistantAsync(assistantCreationOptions, cancellationToken).ConfigureAwait(false); + Assistant model = await client.CreateAssistantAsync(definition.ModelId, assistantCreationOptions, cancellationToken).ConfigureAwait(false); // Instantiate the agent return - new OpenAIAssistantAgent(client, model, config) + new OpenAIAssistantAgent(model, clientProvider, client) { Kernel = kernel, }; @@ -97,79 +97,46 @@ public static async Task CreateAsync( /// /// Retrieve a list of assistant definitions: . /// - /// Configuration for accessing the Assistants API service, such as the api-key. - /// The maximum number of assistant definitions to retrieve - /// The identifier of the assistant beyond which to begin selection. + /// Configuration for accessing the API service. /// The to monitor for cancellation requests. The default is . /// An list of objects. public static async IAsyncEnumerable ListDefinitionsAsync( - OpenAIAssistantConfiguration config, - int maxResults = 100, - string? lastId = null, + OpenAIClientProvider provider, [EnumeratorCancellation] CancellationToken cancellationToken = default) { // Create the client - AssistantsClient client = CreateClient(config); + AssistantClient client = CreateClient(provider); - // Retrieve the assistants - PageableList assistants; - - int resultCount = 0; - do + // Query and enumerate assistant definitions + await foreach (Assistant model in client.GetAssistantsAsync(ListOrder.NewestFirst, cancellationToken).ConfigureAwait(false)) { - assistants = await client.GetAssistantsAsync(limit: Math.Min(maxResults, 100), ListSortOrder.Descending, after: lastId, cancellationToken: cancellationToken).ConfigureAwait(false); - foreach (Assistant assistant in assistants) - { - if (resultCount >= maxResults) - { - break; - } - - resultCount++; - - yield return - new() - { - Id = assistant.Id, - Name = assistant.Name, - Description = assistant.Description, - Instructions = assistant.Instructions, - EnableCodeInterpreter = assistant.Tools.Any(t => t is CodeInterpreterToolDefinition), - EnableRetrieval = assistant.Tools.Any(t => t is RetrievalToolDefinition), - FileIds = assistant.FileIds, - Metadata = assistant.Metadata, - ModelId = assistant.Model, - }; - - lastId = assistant.Id; - } + yield return CreateAssistantDefinition(model); } - while (assistants.HasMore && resultCount < maxResults); } /// /// Retrieve a by identifier. /// /// The containing services, plugins, and other state for use throughout the operation. - /// Configuration for accessing the Assistants API service, such as the api-key. + /// Configuration for accessing the API service. /// The agent identifier /// The to monitor for cancellation requests. The default is . /// An instance public static async Task RetrieveAsync( Kernel kernel, - OpenAIAssistantConfiguration config, + OpenAIClientProvider provider, string id, CancellationToken cancellationToken = default) { // Create the client - AssistantsClient client = CreateClient(config); + AssistantClient client = CreateClient(provider); // Retrieve the assistant - Assistant model = await client.GetAssistantAsync(id, cancellationToken).ConfigureAwait(false); + Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) // Instantiate the agent return - new OpenAIAssistantAgent(client, model, config) + new OpenAIAssistantAgent(model, provider, client) { Kernel = kernel, }; @@ -180,12 +147,17 @@ public static async Task RetrieveAsync( /// /// The to monitor for cancellation requests. The default is . /// The thread identifier - public async Task CreateThreadAsync(CancellationToken cancellationToken = default) - { - AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + public Task CreateThreadAsync(CancellationToken cancellationToken = default) + => AssistantThreadActions.CreateThreadAsync(this._client, options: null, cancellationToken); - return thread.Id; - } + /// + /// Create a new assistant thread. + /// + /// The options for creating the thread + /// The to monitor for cancellation requests. The default is . + /// The thread identifier + public Task CreateThreadAsync(OpenAIThreadCreationOptions? options, CancellationToken cancellationToken = default) + => AssistantThreadActions.CreateThreadAsync(this._client, options, cancellationToken); /// /// Create a new assistant thread. @@ -203,6 +175,25 @@ public async Task DeleteThreadAsync( return await this._client.DeleteThreadAsync(threadId, cancellationToken).ConfigureAwait(false); } + /// + /// Uploads an file for the purpose of using with assistant. + /// + /// The content to upload + /// The name of the file + /// The to monitor for cancellation requests. The default is . + /// The file identifier + /// + /// Use the directly for more advanced file operations. + /// + public async Task UploadFileAsync(Stream stream, string name, CancellationToken cancellationToken = default) + { + FileClient client = this._provider.Client.GetFileClient(); + + OpenAIFileInfo fileInfo = await client.UploadFileAsync(stream, name, FileUploadPurpose.Assistants, cancellationToken).ConfigureAwait(false); + + return fileInfo.Id; + } + /// /// Adds a message to the specified thread. /// @@ -232,7 +223,7 @@ public IAsyncEnumerable GetThreadMessagesAsync(string thread /// /// Delete the assistant definition. /// - /// + /// The to monitor for cancellation requests. The default is . /// True if assistant definition has been deleted /// /// Assistant based agent will not be useable after deletion. @@ -258,8 +249,28 @@ public async Task DeleteAsync(CancellationToken cancellationToken = defaul /// /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. /// + public IAsyncEnumerable InvokeAsync( + string threadId, + KernelArguments? arguments = null, + Kernel? kernel = null, + CancellationToken cancellationToken = default) + => this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken); + + /// + /// Invoke the assistant on the specified thread. + /// + /// The thread identifier + /// Optional invocation options + /// Optional arguments to pass to the agents's invocation, including any . + /// The containing services, plugins, and other state for use by the agent. + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of messages. + /// + /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. + /// public async IAsyncEnumerable InvokeAsync( string threadId, + OpenAIAssistantInvocationOptions? options, KernelArguments? arguments = null, Kernel? kernel = null, [EnumeratorCancellation] CancellationToken cancellationToken = default) @@ -269,7 +280,7 @@ public async IAsyncEnumerable InvokeAsync( kernel ??= this.Kernel; arguments ??= this.Arguments; - await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, this._config.Polling, this.Logger, kernel, arguments, cancellationToken).ConfigureAwait(false)) + await foreach ((bool isVisible, ChatMessageContent message) in AssistantThreadActions.InvokeAsync(this, this._client, threadId, options, this.Logger, kernel, arguments, cancellationToken).ConfigureAwait(false)) { if (isVisible) { @@ -282,29 +293,11 @@ public async IAsyncEnumerable InvokeAsync( protected override IEnumerable GetChannelKeys() { // Distinguish from other channel types. - yield return typeof(AgentChannel).FullName!; + yield return typeof(OpenAIAssistantChannel).FullName!; - // Distinguish between different Azure OpenAI endpoints or OpenAI services. - yield return this._config.Endpoint ?? "openai"; - - // Distinguish between different API versioning. - if (this._config.Version.HasValue) + foreach (string key in this._channelKeys) { - yield return this._config.Version.ToString()!; - } - - // Custom client receives dedicated channel. - if (this._config.HttpClient is not null) - { - if (this._config.HttpClient.BaseAddress is not null) - { - yield return this._config.HttpClient.BaseAddress.AbsoluteUri; - } - - foreach (string header in this._config.HttpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) - { - yield return header; - } + yield return key; } } @@ -313,10 +306,12 @@ protected override async Task CreateChannelAsync(CancellationToken { this.Logger.LogOpenAIAssistantAgentCreatingChannel(nameof(CreateChannelAsync), nameof(OpenAIAssistantChannel)); - AssistantThread thread = await this._client.CreateThreadAsync(cancellationToken).ConfigureAwait(false); + AssistantThread thread = await this._client.CreateThreadAsync(options: null, cancellationToken).ConfigureAwait(false); + + this.Logger.LogInformation("[{MethodName}] Created assistant thread: {ThreadId}", nameof(CreateChannelAsync), thread.Id); OpenAIAssistantChannel channel = - new(this._client, thread.Id, this._config.Polling) + new(this._client, thread.Id) { Logger = this.LoggerFactory.CreateLogger() }; @@ -338,13 +333,16 @@ internal void ThrowIfDeleted() /// Initializes a new instance of the class. /// private OpenAIAssistantAgent( - AssistantsClient client, Assistant model, - OpenAIAssistantConfiguration config) + OpenAIClientProvider provider, + AssistantClient client) { + this._provider = provider; this._assistant = model; - this._client = client; - this._config = config; + this._client = provider.Client.GetAssistantClient(); + this._channelKeys = provider.ConfigurationKeys.ToArray(); + + this.Definition = CreateAssistantDefinition(model); this.Description = this._assistant.Description; this.Id = this._assistant.Id; @@ -352,64 +350,94 @@ private OpenAIAssistantAgent( this.Instructions = this._assistant.Instructions; } + private static OpenAIAssistantDefinition CreateAssistantDefinition(Assistant model) + { + OpenAIAssistantExecutionOptions? options = null; + + if (model.Metadata.TryGetValue(OptionsMetadataKey, out string? optionsJson)) + { + options = JsonSerializer.Deserialize(optionsJson); + } + + IReadOnlyList? fileIds = (IReadOnlyList?)model.ToolResources?.CodeInterpreter?.FileIds; + string? vectorStoreId = model.ToolResources?.FileSearch?.VectorStoreIds?.SingleOrDefault(); + bool enableJsonResponse = model.ResponseFormat is not null && model.ResponseFormat == AssistantResponseFormat.JsonObject; + + return new(model.Model) + { + Id = model.Id, + Name = model.Name, + Description = model.Description, + Instructions = model.Instructions, + CodeInterpreterFileIds = fileIds, + EnableCodeInterpreter = model.Tools.Any(t => t is CodeInterpreterToolDefinition), + EnableFileSearch = model.Tools.Any(t => t is FileSearchToolDefinition), + Metadata = model.Metadata, + EnableJsonResponse = enableJsonResponse, + TopP = model.NucleusSamplingFactor, + Temperature = model.Temperature, + VectorStoreId = string.IsNullOrWhiteSpace(vectorStoreId) ? null : vectorStoreId, + ExecutionOptions = options, + }; + } + private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAssistantDefinition definition) { AssistantCreationOptions assistantCreationOptions = - new(definition.ModelId) + new() { Description = definition.Description, Instructions = definition.Instructions, Name = definition.Name, - Metadata = definition.Metadata?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), + ToolResources = + AssistantToolResourcesFactory.GenerateToolResources( + definition.EnableFileSearch ? definition.VectorStoreId : null, + definition.EnableCodeInterpreter ? definition.CodeInterpreterFileIds : null), + ResponseFormat = definition.EnableJsonResponse ? AssistantResponseFormat.JsonObject : AssistantResponseFormat.Auto, + Temperature = definition.Temperature, + NucleusSamplingFactor = definition.TopP, }; - assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []); + if (definition.Metadata != null) + { + foreach (KeyValuePair item in definition.Metadata) + { + assistantCreationOptions.Metadata[item.Key] = item.Value; + } + } + + if (definition.ExecutionOptions != null) + { + string optionsJson = JsonSerializer.Serialize(definition.ExecutionOptions); + assistantCreationOptions.Metadata[OptionsMetadataKey] = optionsJson; + } if (definition.EnableCodeInterpreter) { - assistantCreationOptions.Tools.Add(new CodeInterpreterToolDefinition()); + assistantCreationOptions.Tools.Add(ToolDefinition.CreateCodeInterpreter()); } - if (definition.EnableRetrieval) + if (definition.EnableFileSearch) { - assistantCreationOptions.Tools.Add(new RetrievalToolDefinition()); + assistantCreationOptions.Tools.Add(ToolDefinition.CreateFileSearch()); } return assistantCreationOptions; } - private static AssistantsClient CreateClient(OpenAIAssistantConfiguration config) + private static AssistantClient CreateClient(OpenAIClientProvider config) { - AssistantsClientOptions clientOptions = CreateClientOptions(config); - - // Inspect options - if (!string.IsNullOrWhiteSpace(config.Endpoint)) - { - // Create client configured for Azure OpenAI, if endpoint definition is present. - return new AssistantsClient(new Uri(config.Endpoint), new AzureKeyCredential(config.ApiKey), clientOptions); - } - - // Otherwise, create client configured for OpenAI. - return new AssistantsClient(config.ApiKey, clientOptions); + return config.Client.GetAssistantClient(); } - private static AssistantsClientOptions CreateClientOptions(OpenAIAssistantConfiguration config) + private static IEnumerable DefineChannelKeys(OpenAIClientProvider config) { - AssistantsClientOptions options = - config.Version.HasValue ? - new(config.Version.Value) : - new(); - - options.Diagnostics.ApplicationId = HttpHeaderConstant.Values.UserAgent; - options.AddPolicy(new AddHeaderRequestPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), HttpPipelinePosition.PerCall); + // Distinguish from other channel types. + yield return typeof(AgentChannel).FullName!; - if (config.HttpClient is not null) + foreach (string key in config.ConfigurationKeys) { - options.Transport = new HttpClientTransport(config.HttpClient); - options.RetryPolicy = new RetryPolicy(maxRetries: 0); // Disable Azure SDK retry policy if and only if a custom HttpClient is provided. - options.Retry.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable Azure SDK default timeout + yield return key; } - - return options; } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index 051281c95abe..77e8de748653 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -2,17 +2,18 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; -using Azure.AI.OpenAI.Assistants; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; /// /// A specialization for use with . /// -internal sealed class OpenAIAssistantChannel(AssistantsClient client, string threadId, OpenAIAssistantConfiguration.PollingConfiguration pollingConfiguration) +internal sealed class OpenAIAssistantChannel(AssistantClient client, string threadId) : AgentChannel { - private readonly AssistantsClient _client = client; + private readonly AssistantClient _client = client; private readonly string _threadId = threadId; /// @@ -31,7 +32,7 @@ protected override async Task ReceiveAsync(IEnumerable histo { agent.ThrowIfDeleted(); - return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, pollingConfiguration, this.Logger, agent.Kernel, agent.Arguments, cancellationToken); + return AssistantThreadActions.InvokeAsync(agent, this._client, this._threadId, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken); } /// diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs deleted file mode 100644 index aa037266e7d5..000000000000 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantConfiguration.cs +++ /dev/null @@ -1,91 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Azure.AI.OpenAI.Assistants; - -namespace Microsoft.SemanticKernel.Agents.OpenAI; - -/// -/// Configuration to target an OpenAI Assistant API. -/// -public sealed class OpenAIAssistantConfiguration -{ - /// - /// The Assistants API Key. - /// - public string ApiKey { get; } - - /// - /// An optional endpoint if targeting Azure OpenAI Assistants API. - /// - public string? Endpoint { get; } - - /// - /// An optional API version override. - /// - public AssistantsClientOptions.ServiceVersion? Version { get; init; } - - /// - /// Custom for HTTP requests. - /// - public HttpClient? HttpClient { get; init; } - - /// - /// Defineds polling behavior for Assistant API requests. - /// - public PollingConfiguration Polling { get; } = new PollingConfiguration(); - - /// - /// Initializes a new instance of the class. - /// - /// The Assistants API Key - /// An optional endpoint if targeting Azure OpenAI Assistants API - public OpenAIAssistantConfiguration(string apiKey, string? endpoint = null) - { - Verify.NotNullOrWhiteSpace(apiKey); - if (!string.IsNullOrWhiteSpace(endpoint)) - { - // Only verify `endpoint` when provided (AzureOAI vs OpenAI) - Verify.StartsWith(endpoint, "https://", "The Azure OpenAI endpoint must start with 'https://'"); - } - - this.ApiKey = apiKey; - this.Endpoint = endpoint; - } - - /// - /// Configuration and defaults associated with polling behavior for Assistant API requests. - /// - public sealed class PollingConfiguration - { - /// - /// The default polling interval when monitoring thread-run status. - /// - public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500); - - /// - /// The default back-off interval when monitoring thread-run status. - /// - public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1); - - /// - /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. - /// - public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500); - - /// - /// The polling interval when monitoring thread-run status. - /// - public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval; - - /// - /// The back-off interval when monitoring thread-run status. - /// - public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff; - - /// - /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. - /// - public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay; - } -} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs index 3699e07ee1ed..7b7015aa3b4a 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantDefinition.cs @@ -1,57 +1,112 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Microsoft.SemanticKernel.Agents.OpenAI; /// -/// The data associated with an assistant's definition. +/// Defines an assistant. /// public sealed class OpenAIAssistantDefinition { /// - /// Identifies the AI model (OpenAI) or deployment (AzureOAI) this agent targets. + /// Identifies the AI model targeted by the agent. /// - public string? ModelId { get; init; } + public string ModelId { get; } /// /// The description of the assistant. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Description { get; init; } /// /// The assistant's unique id. (Ignored on create.) /// - public string? Id { get; init; } + public string Id { get; init; } = string.Empty; /// /// The system instructions for the assistant to use. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Instructions { get; init; } /// /// The name of the assistant. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string? Name { get; init; } + /// + /// Optional file-ids made available to the code_interpreter tool, if enabled. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? CodeInterpreterFileIds { get; init; } + /// /// Set if code-interpreter is enabled. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] public bool EnableCodeInterpreter { get; init; } /// - /// Set if retrieval is enabled. + /// Set if file-search is enabled. /// - public bool EnableRetrieval { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool EnableFileSearch { get; init; } /// - /// A list of previously uploaded file IDs to attach to the assistant. + /// Set if json response-format is enabled. /// - public IEnumerable? FileIds { get; init; } + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool EnableJsonResponse { get; init; } /// /// A set of up to 16 key/value pairs that can be attached to an agent, used for /// storing additional information about that object in a structured format.Keys /// may be up to 64 characters in length and values may be up to 512 characters in length. /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public IReadOnlyDictionary? Metadata { get; init; } + + /// + /// The sampling temperature to use, between 0 and 2. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Temperature { get; init; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model + /// considers the results of the tokens with top_p probability mass. + /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. + /// + /// + /// Recommended to set this or temperature but not both. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? TopP { get; init; } + + /// + /// Requires file-search if specified. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? VectorStoreId { get; init; } + + /// + /// Default execution options for each agent invocation. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public OpenAIAssistantExecutionOptions? ExecutionOptions { get; init; } + + /// + /// Initializes a new instance of the class. + /// + /// The targeted model + [JsonConstructor] + public OpenAIAssistantDefinition(string modelId) + { + Verify.NotNullOrWhiteSpace(modelId); + + this.ModelId = modelId; + } } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs new file mode 100644 index 000000000000..074b92831c92 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantExecutionOptions.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Defines assistant execution options for each invocation. +/// +/// +/// These options are persisted as a single entry of the assistant's metadata with key: "__run_options" +/// +public sealed class OpenAIAssistantExecutionOptions +{ + /// + /// The maximum number of completion tokens that may be used over the course of the run. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxCompletionTokens { get; init; } + + /// + /// The maximum number of prompt tokens that may be used over the course of the run. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxPromptTokens { get; init; } + + /// + /// Enables parallel function calling during tool use. Enabled by default. + /// Use this property to disable. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? ParallelToolCallsEnabled { get; init; } + + /// + /// When set, the thread will be truncated to the N most recent messages in the thread. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TruncationMessageCount { get; init; } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs new file mode 100644 index 000000000000..0653c83a13e2 --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantInvocationOptions.cs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Defines per invocation execution options that override the assistant definition. +/// +/// +/// Not applicable to usage. +/// +public sealed class OpenAIAssistantInvocationOptions +{ + /// + /// Override the AI model targeted by the agent. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? ModelName { get; init; } + + /// + /// Set if code_interpreter tool is enabled. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool EnableCodeInterpreter { get; init; } + + /// + /// Set if file_search tool is enabled. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] + public bool EnableFileSearch { get; init; } + + /// + /// Set if json response-format is enabled. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? EnableJsonResponse { get; init; } + + /// + /// The maximum number of completion tokens that may be used over the course of the run. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxCompletionTokens { get; init; } + + /// + /// The maximum number of prompt tokens that may be used over the course of the run. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? MaxPromptTokens { get; init; } + + /// + /// Enables parallel function calling during tool use. Enabled by default. + /// Use this property to disable. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public bool? ParallelToolCallsEnabled { get; init; } + + /// + /// When set, the thread will be truncated to the N most recent messages in the thread. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public int? TruncationMessageCount { get; init; } + + /// + /// The sampling temperature to use, between 0 and 2. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? Temperature { get; init; } + + /// + /// An alternative to sampling with temperature, called nucleus sampling, where the model + /// considers the results of the tokens with top_p probability mass. + /// So 0.1 means only the tokens comprising the top 10% probability mass are considered. + /// + /// + /// Recommended to set this or temperature but not both. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public float? TopP { get; init; } + + /// + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyDictionary? Metadata { get; init; } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs new file mode 100644 index 000000000000..3e2e395a77ea --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIClientProvider.cs @@ -0,0 +1,173 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.ClientModel; +using System.ClientModel.Primitives; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading; +using Azure.AI.OpenAI; +using Azure.Core; +using Microsoft.SemanticKernel.Http; +using OpenAI; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Provides an for use by . +/// +public sealed class OpenAIClientProvider +{ + /// + /// Avoids an exception from OpenAI Client when a custom endpoint is provided without an API key. + /// + private const string SingleSpaceKey = " "; + + /// + /// An active client instance. + /// + public OpenAIClient Client { get; } + + /// + /// Configuration keys required for management. + /// + internal IReadOnlyList ConfigurationKeys { get; } + + private OpenAIClientProvider(OpenAIClient client, IEnumerable keys) + { + this.Client = client; + this.ConfigurationKeys = keys.ToArray(); + } + + /// + /// Produce a based on . + /// + /// The API key + /// The service endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForAzureOpenAI(ApiKeyCredential apiKey, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNull(apiKey, nameof(apiKey)); + Verify.NotNull(endpoint, nameof(endpoint)); + + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + + return new(new AzureOpenAIClient(endpoint, apiKey!, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// The credentials + /// The service endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForAzureOpenAI(TokenCredential credential, Uri endpoint, HttpClient? httpClient = null) + { + Verify.NotNull(credential, nameof(credential)); + Verify.NotNull(endpoint, nameof(endpoint)); + + AzureOpenAIClientOptions clientOptions = CreateAzureClientOptions(endpoint, httpClient); + + return new(new AzureOpenAIClient(endpoint, credential, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// An optional endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForOpenAI(Uri? endpoint = null, HttpClient? httpClient = null) + { + OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient); + return new(new OpenAIClient(SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Produce a based on . + /// + /// The API key + /// An optional endpoint + /// Custom for HTTP requests. + public static OpenAIClientProvider ForOpenAI(ApiKeyCredential apiKey, Uri? endpoint = null, HttpClient? httpClient = null) + { + OpenAIClientOptions clientOptions = CreateOpenAIClientOptions(endpoint, httpClient); + return new(new OpenAIClient(apiKey ?? SingleSpaceKey, clientOptions), CreateConfigurationKeys(endpoint, httpClient)); + } + + /// + /// Directly provide a client instance. + /// + public static OpenAIClientProvider FromClient(OpenAIClient client) + { + return new(client, [client.GetType().FullName!, client.GetHashCode().ToString()]); + } + + private static AzureOpenAIClientOptions CreateAzureClientOptions(Uri? endpoint, HttpClient? httpClient) + { + AzureOpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint, + }; + + ConfigureClientOptions(httpClient, options); + + return options; + } + + private static OpenAIClientOptions CreateOpenAIClientOptions(Uri? endpoint, HttpClient? httpClient) + { + OpenAIClientOptions options = new() + { + ApplicationId = HttpHeaderConstant.Values.UserAgent, + Endpoint = endpoint ?? httpClient?.BaseAddress, + }; + + ConfigureClientOptions(httpClient, options); + + return options; + } + + private static void ConfigureClientOptions(HttpClient? httpClient, OpenAIClientOptions options) + { + options.AddPolicy(CreateRequestHeaderPolicy(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIAssistantAgent))), PipelinePosition.PerCall); + + if (httpClient is not null) + { + options.Transport = new HttpClientPipelineTransport(httpClient); + options.RetryPolicy = new ClientRetryPolicy(maxRetries: 0); // Disable retry policy if and only if a custom HttpClient is provided. + options.NetworkTimeout = Timeout.InfiniteTimeSpan; // Disable default timeout + } + } + + private static GenericActionPipelinePolicy CreateRequestHeaderPolicy(string headerName, string headerValue) + => + new((message) => + { + if (message?.Request?.Headers?.TryGetValue(headerName, out string? _) == false) + { + message.Request.Headers.Set(headerName, headerValue); + } + }); + + private static IEnumerable CreateConfigurationKeys(Uri? endpoint, HttpClient? httpClient) + { + if (endpoint != null) + { + yield return endpoint.ToString(); + } + + if (httpClient is not null) + { + if (httpClient.BaseAddress is not null) + { + yield return httpClient.BaseAddress.AbsoluteUri; + } + + foreach (string header in httpClient.DefaultRequestHeaders.SelectMany(h => h.Value)) + { + yield return header; + } + } + } +} diff --git a/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs new file mode 100644 index 000000000000..3f39c43d03dc --- /dev/null +++ b/dotnet/src/Agents/OpenAI/OpenAIThreadCreationOptions.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Thread creation options. +/// +public sealed class OpenAIThreadCreationOptions +{ + /// + /// Optional file-ids made available to the code_interpreter tool, if enabled. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? CodeInterpreterFileIds { get; init; } + + /// + /// Optional messages to initialize thread with.. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyList? Messages { get; init; } + + /// + /// Enables file-search if specified. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? VectorStoreId { get; init; } + + /// + /// A set of up to 16 key/value pairs that can be attached to an agent, used for + /// storing additional information about that object in a structured format.Keys + /// may be up to 64 characters in length and values may be up to 512 characters in length. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public IReadOnlyDictionary? Metadata { get; init; } +} diff --git a/dotnet/src/Agents/OpenAI/RunPollingOptions.cs b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs new file mode 100644 index 000000000000..756ba689131c --- /dev/null +++ b/dotnet/src/Agents/OpenAI/RunPollingOptions.cs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Configuration and defaults associated with polling behavior for Assistant API run processing. +/// +public sealed class RunPollingOptions +{ + /// + /// The default polling interval when monitoring thread-run status. + /// + public static TimeSpan DefaultPollingInterval { get; } = TimeSpan.FromMilliseconds(500); + + /// + /// The default back-off interval when monitoring thread-run status. + /// + public static TimeSpan DefaultPollingBackoff { get; } = TimeSpan.FromSeconds(1); + + /// + /// The default number of polling iterations before using . + /// + public static int DefaultPollingBackoffThreshold { get; } = 2; + + /// + /// The default polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. + /// + public static TimeSpan DefaultMessageSynchronizationDelay { get; } = TimeSpan.FromMilliseconds(500); + + /// + /// The polling interval when monitoring thread-run status. + /// + public TimeSpan RunPollingInterval { get; set; } = DefaultPollingInterval; + + /// + /// The back-off interval when monitoring thread-run status. + /// + public TimeSpan RunPollingBackoff { get; set; } = DefaultPollingBackoff; + + /// + /// The number of polling iterations before using . + /// + public int RunPollingBackoffThreshold { get; set; } = DefaultPollingBackoffThreshold; + + /// + /// The polling delay when retrying message retrieval due to a 404/NotFound from synchronization lag. + /// + public TimeSpan MessageSynchronizationDelay { get; set; } = DefaultMessageSynchronizationDelay; + + /// + /// Gets the polling interval for the specified iteration count. + /// + /// The number of polling iterations already attempted + public TimeSpan GetPollingInterval(int iterationCount) => + iterationCount > this.RunPollingBackoffThreshold ? this.RunPollingBackoff : this.RunPollingInterval; +} diff --git a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs index 2a680614a54f..17994a12e6a0 100644 --- a/dotnet/src/Agents/UnitTests/AgentChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChannelTests.cs @@ -23,20 +23,26 @@ public class AgentChannelTests [Fact] public async Task VerifyAgentChannelUpcastAsync() { + // Arrange TestChannel channel = new(); + // Assert Assert.Equal(0, channel.InvokeCount); - var messages = channel.InvokeAgentAsync(new TestAgent()).ToArrayAsync(); + // Act + var messages = channel.InvokeAgentAsync(new MockAgent()).ToArrayAsync(); + // Assert Assert.Equal(1, channel.InvokeCount); + // Act await Assert.ThrowsAsync(() => channel.InvokeAgentAsync(new NextAgent()).ToArrayAsync().AsTask()); + // Assert Assert.Equal(1, channel.InvokeCount); } /// /// Not using mock as the goal here is to provide entrypoint to protected method. /// - private sealed class TestChannel : AgentChannel + private sealed class TestChannel : AgentChannel { public int InvokeCount { get; private set; } @@ -44,7 +50,7 @@ private sealed class TestChannel : AgentChannel => base.InvokeAsync(agent, cancellationToken); #pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously - protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(TestAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) + protected internal override async IAsyncEnumerable<(bool IsVisible, ChatMessageContent Message)> InvokeAsync(MockAgent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) #pragma warning restore CS1998 // Async method lacks 'await' operators and will run synchronously { this.InvokeCount++; @@ -63,18 +69,5 @@ protected internal override Task ReceiveAsync(IEnumerable hi } } - private sealed class NextAgent : TestAgent; - - private class TestAgent : KernelAgent - { - protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - - protected internal override IEnumerable GetChannelKeys() - { - throw new NotImplementedException(); - } - } + private sealed class NextAgent : MockAgent; } diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index 49c36ae73c53..cd83ab8b9f45 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -3,9 +3,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; +using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests; @@ -21,53 +23,80 @@ public class AgentChatTests [Fact] public async Task VerifyAgentChatLifecycleAsync() { - // Create chat + // Arrange: Create chat TestChat chat = new(); - // Verify initial state + // Assert: Verify initial state Assert.False(chat.IsActive); await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent history - // Inject history + // Act: Inject history chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "More")]); chat.AddChatMessages([new ChatMessageContent(AuthorRole.User, "And then some")]); - // Verify updated history + // Assert: Verify updated history await this.VerifyHistoryAsync(expectedCount: 2, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 0, chat.GetChatMessagesAsync(chat.Agent)); // Agent hasn't joined - // Invoke with input & verify (agent joins chat) + // Act: Invoke with input & verify (agent joins chat) chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi")); await chat.InvokeAsync().ToArrayAsync(); - Assert.Equal(1, chat.Agent.InvokeCount); - // Verify updated history + // Assert: Verify updated history + Assert.Equal(1, chat.Agent.InvokeCount); await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 4, chat.GetChatMessagesAsync(chat.Agent)); // Agent history - // Invoke without input & verify + // Act: Invoke without input await chat.InvokeAsync().ToArrayAsync(); - Assert.Equal(2, chat.Agent.InvokeCount); - // Verify final history + // Assert: Verify final history + Assert.Equal(2, chat.Agent.InvokeCount); await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync()); // Primary history await this.VerifyHistoryAsync(expectedCount: 5, chat.GetChatMessagesAsync(chat.Agent)); // Agent history } + /// + /// Verify throw exception for system message. + /// + [Fact] + public void VerifyAgentChatRejectsSystemMessage() + { + // Arrange: Create chat + TestChat chat = new() { LoggerFactory = new Mock().Object }; + + // Assert and Act: Verify system message not accepted + Assert.Throws(() => chat.AddChatMessage(new ChatMessageContent(AuthorRole.System, "hi"))); + } + + /// + /// Verify throw exception for if invoked when active. + /// + [Fact] + public async Task VerifyAgentChatThrowsWhenActiveAsync() + { + // Arrange: Create chat + TestChat chat = new(); + + // Assert and Act: Verify system message not accepted + await Assert.ThrowsAsync(() => chat.InvalidInvokeAsync().ToArrayAsync().AsTask()); + } + /// /// Verify the management of instances as they join . /// [Fact(Skip = "Not 100% reliable for github workflows, but useful for dev testing.")] public async Task VerifyGroupAgentChatConcurrencyAsync() { + // Arrange TestChat chat = new(); Task[] tasks; int isActive = 0; - // Queue concurrent tasks + // Act: Queue concurrent tasks object syncObject = new(); lock (syncObject) { @@ -89,7 +118,7 @@ public async Task VerifyGroupAgentChatConcurrencyAsync() await Task.Yield(); - // Verify failure + // Assert: Verify failure await Assert.ThrowsAsync(() => Task.WhenAll(tasks)); async Task SynchronizedInvokeAsync() @@ -119,5 +148,12 @@ private sealed class TestChat : AgentChat public override IAsyncEnumerable InvokeAsync( CancellationToken cancellationToken = default) => this.InvokeAgentAsync(this.Agent, cancellationToken); + + public IAsyncEnumerable InvalidInvokeAsync( + CancellationToken cancellationToken = default) + { + this.SetActivityOrThrow(); + return this.InvokeAgentAsync(this.Agent, cancellationToken); + } } } diff --git a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj index 27e1afcfa92c..6b9fea49fde2 100644 --- a/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj +++ b/dotnet/src/Agents/UnitTests/Agents.UnitTests.csproj @@ -8,7 +8,7 @@ true false 12 - $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110 + $(NoWarn);CA2007,CA1812,CA1861,CA1063,VSTHRD111,SKEXP0001,SKEXP0050,SKEXP0110;OPENAI001 @@ -32,6 +32,7 @@ + diff --git a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs index 1a607ea7e6c7..e6668c7ea568 100644 --- a/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/AggregatorAgentTests.cs @@ -21,6 +21,7 @@ public class AggregatorAgentTests [InlineData(AggregatorMode.Flat, 2)] public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeOffset) { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); @@ -44,38 +45,57 @@ public async Task VerifyAggregatorAgentUsageAsync(AggregatorMode mode, int modeO // Add message to outer chat (no agent has joined) uberChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test uber")); + // Act var messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent hasn't joined chat, no broadcast + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent hasn't joined chat, no broadcast - // Add message to inner chat (not visible to parent) + // Arrange: Add message to inner chat (not visible to parent) groupChat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "test inner")); + // Act messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Empty(messages); // Agent still hasn't joined chat + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Single(messages); - // Invoke outer chat (outer chat captures final inner message) + // Act: Invoke outer chat (outer chat captures final inner message) messages = await uberChat.InvokeAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Equal(1 + modeOffset, messages.Length); // New messages generated from inner chat + // Act messages = await uberChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(2 + modeOffset, messages.Length); // Total messages on uber chat + // Act messages = await groupChat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized + // Act messages = await uberChat.GetChatMessagesAsync(uberAgent).ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); // Total messages on inner chat once synchronized (agent equivalent) } diff --git a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs index ad7428f6f0b9..62420f90e62b 100644 --- a/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/AgentGroupChatTests.cs @@ -23,12 +23,18 @@ public class AgentGroupChatTests [Fact] public void VerifyGroupAgentChatDefaultState() { + // Arrange AgentGroupChat chat = new(); + + // Assert Assert.Empty(chat.Agents); Assert.NotNull(chat.ExecutionSettings); Assert.False(chat.IsComplete); + // Act chat.IsComplete = true; + + // Assert Assert.True(chat.IsComplete); } @@ -38,21 +44,30 @@ public void VerifyGroupAgentChatDefaultState() [Fact] public async Task VerifyGroupAgentChatAgentMembershipAsync() { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); Agent agent4 = CreateMockAgent(); AgentGroupChat chat = new(agent1, agent2); + + // Assert Assert.Equal(2, chat.Agents.Count); + // Act chat.AddAgent(agent3); + // Assert Assert.Equal(3, chat.Agents.Count); + // Act var messages = await chat.InvokeAsync(agent4, isJoining: false).ToArrayAsync(); + // Assert Assert.Equal(3, chat.Agents.Count); + // Act messages = await chat.InvokeAsync(agent4).ToArrayAsync(); + // Assert Assert.Equal(4, chat.Agents.Count); } @@ -62,6 +77,7 @@ public async Task VerifyGroupAgentChatAgentMembershipAsync() [Fact] public async Task VerifyGroupAgentChatMultiTurnAsync() { + // Arrange Agent agent1 = CreateMockAgent(); Agent agent2 = CreateMockAgent(); Agent agent3 = CreateMockAgent(); @@ -81,10 +97,14 @@ public async Task VerifyGroupAgentChatMultiTurnAsync() IsComplete = true }; + // Act and Assert await Assert.ThrowsAsync(() => chat.InvokeAsync(CancellationToken.None).ToArrayAsync().AsTask()); + // Act chat.ExecutionSettings.TerminationStrategy.AutomaticReset = true; var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync(); + + // Assert Assert.Equal(9, messages.Length); Assert.False(chat.IsComplete); @@ -111,6 +131,7 @@ public async Task VerifyGroupAgentChatMultiTurnAsync() [Fact] public async Task VerifyGroupAgentChatFailedSelectionAsync() { + // Arrange AgentGroupChat chat = Create3AgentChat(); chat.ExecutionSettings = @@ -128,6 +149,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync() // Remove max-limit in order to isolate the target behavior. chat.ExecutionSettings.TerminationStrategy.MaximumIterations = int.MaxValue; + // Act and Assert await Assert.ThrowsAsync(() => chat.InvokeAsync().ToArrayAsync().AsTask()); } @@ -137,6 +159,7 @@ public async Task VerifyGroupAgentChatFailedSelectionAsync() [Fact] public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() { + // Arrange AgentGroupChat chat = Create3AgentChat(); chat.ExecutionSettings = @@ -150,7 +173,10 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() } }; + // Act var messages = await chat.InvokeAsync(CancellationToken.None).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.True(chat.IsComplete); } @@ -161,6 +187,7 @@ public async Task VerifyGroupAgentChatMultiTurnTerminationAsync() [Fact] public async Task VerifyGroupAgentChatDiscreteTerminationAsync() { + // Arrange Agent agent1 = CreateMockAgent(); AgentGroupChat chat = @@ -178,7 +205,10 @@ public async Task VerifyGroupAgentChatDiscreteTerminationAsync() } }; + // Act var messages = await chat.InvokeAsync(agent1).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.True(chat.IsComplete); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs index d17391ee24be..ecb5cd6eee33 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/AgentGroupChatSettingsTests.cs @@ -16,7 +16,10 @@ public class AgentGroupChatSettingsTests [Fact] public void VerifyChatExecutionSettingsDefault() { + // Arrange AgentGroupChatSettings settings = new(); + + // Assert Assert.IsType(settings.TerminationStrategy); Assert.Equal(1, settings.TerminationStrategy.MaximumIterations); Assert.IsType(settings.SelectionStrategy); @@ -28,6 +31,7 @@ public void VerifyChatExecutionSettingsDefault() [Fact] public void VerifyChatExecutionContinuationStrategyDefault() { + // Arrange Mock strategyMock = new(); AgentGroupChatSettings settings = new() @@ -35,6 +39,7 @@ public void VerifyChatExecutionContinuationStrategyDefault() TerminationStrategy = strategyMock.Object }; + // Assert Assert.Equal(strategyMock.Object, settings.TerminationStrategy); } @@ -44,6 +49,7 @@ public void VerifyChatExecutionContinuationStrategyDefault() [Fact] public void VerifyChatExecutionSelectionStrategyDefault() { + // Arrange Mock strategyMock = new(); AgentGroupChatSettings settings = new() @@ -51,6 +57,7 @@ public void VerifyChatExecutionSelectionStrategyDefault() SelectionStrategy = strategyMock.Object }; + // Assert Assert.NotNull(settings.SelectionStrategy); Assert.Equal(strategyMock.Object, settings.SelectionStrategy); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs index 6ad6fd75b18f..5af211c6cdf1 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/AggregatorTerminationStrategyTests.cs @@ -6,7 +6,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -22,7 +21,10 @@ public class AggregatorTerminationStrategyTests [Fact] public void VerifyAggregateTerminationStrategyInitialState() { + // Arrange AggregatorTerminationStrategy strategy = new(); + + // Assert Assert.Equal(AggregateTerminationCondition.All, strategy.Condition); } @@ -32,14 +34,16 @@ public void VerifyAggregateTerminationStrategyInitialState() [Fact] public async Task VerifyAggregateTerminationStrategyAnyAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMock = new(); + MockAgent agentMock = new(); + // Act and Assert await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockFalse) { Condition = AggregateTerminationCondition.Any, @@ -47,7 +51,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockFalse, strategyMockFalse) { Condition = AggregateTerminationCondition.Any, @@ -55,7 +59,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockTrue) { Condition = AggregateTerminationCondition.Any, @@ -68,14 +72,16 @@ await VerifyResultAsync( [Fact] public async Task VerifyAggregateTerminationStrategyAllAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMock = new(); + MockAgent agentMock = new(); + // Act and Assert await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockFalse) { Condition = AggregateTerminationCondition.All, @@ -83,7 +89,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: false, - agentMock.Object, + agentMock, new(strategyMockFalse, strategyMockFalse) { Condition = AggregateTerminationCondition.All, @@ -91,7 +97,7 @@ await VerifyResultAsync( await VerifyResultAsync( expectedResult: true, - agentMock.Object, + agentMock, new(strategyMockTrue, strategyMockTrue) { Condition = AggregateTerminationCondition.All, @@ -104,34 +110,39 @@ await VerifyResultAsync( [Fact] public async Task VerifyAggregateTerminationStrategyAgentAsync() { + // Arrange TerminationStrategy strategyMockTrue = new MockTerminationStrategy(terminationResult: true); TerminationStrategy strategyMockFalse = new MockTerminationStrategy(terminationResult: false); - Mock agentMockA = new(); - Mock agentMockB = new(); + MockAgent agentMockA = new(); + MockAgent agentMockB = new(); + // Act and Assert await VerifyResultAsync( expectedResult: false, - agentMockB.Object, + agentMockB, new(strategyMockTrue, strategyMockTrue) { - Agents = [agentMockA.Object], + Agents = [agentMockA], Condition = AggregateTerminationCondition.All, }); await VerifyResultAsync( expectedResult: true, - agentMockB.Object, + agentMockB, new(strategyMockTrue, strategyMockTrue) { - Agents = [agentMockB.Object], + Agents = [agentMockB], Condition = AggregateTerminationCondition.All, }); } private static async Task VerifyResultAsync(bool expectedResult, Agent agent, AggregatorTerminationStrategy strategyRoot) { + // Act var result = await strategyRoot.ShouldTerminateAsync(agent, []); + + // Assert Assert.Equal(expectedResult, result); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs index af045e67873d..83cb9a3ea337 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionSelectionStrategyTests.cs @@ -5,7 +5,6 @@ using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -21,42 +20,73 @@ public class KernelFunctionSelectionStrategyTests [Fact] public async Task VerifyKernelFunctionSelectionStrategyDefaultsAsync() { - Mock mockAgent = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Object.Id)); + // Arrange + MockAgent mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id)); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - ResultParser = (result) => result.GetValue() ?? string.Empty, + ResultParser = (result) => mockAgent.Id, + AgentsVariableName = "agents", + HistoryVariableName = "history", }; + // Assert Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); + Assert.NotEqual("agent", KernelFunctionSelectionStrategy.DefaultAgentsVariableName); + Assert.NotEqual("history", KernelFunctionSelectionStrategy.DefaultHistoryVariableName); - Agent nextAgent = await strategy.NextAsync([mockAgent.Object], []); + // Act + Agent nextAgent = await strategy.NextAsync([mockAgent], []); + // Assert Assert.NotNull(nextAgent); - Assert.Equal(mockAgent.Object, nextAgent); + Assert.Equal(mockAgent, nextAgent); } /// /// Verify strategy mismatch. /// [Fact] - public async Task VerifyKernelFunctionSelectionStrategyParsingAsync() + public async Task VerifyKernelFunctionSelectionStrategyThrowsOnNullResultAsync() { - Mock mockAgent = new(); - KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(string.Empty)); + // Arrange + MockAgent mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin(mockAgent.Id)); KernelFunctionSelectionStrategy strategy = new(plugin.Single(), new()) { - Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Object.Name } }, - ResultParser = (result) => result.GetValue() ?? string.Empty, + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } }, + ResultParser = (result) => "larry", }; - await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent.Object], [])); + // Act and Assert + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], [])); + } + + /// + /// Verify strategy mismatch. + /// + [Fact] + public async Task VerifyKernelFunctionSelectionStrategyThrowsOnBadResultAsync() + { + // Arrange + MockAgent mockAgent = new(); + KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin("")); + + KernelFunctionSelectionStrategy strategy = + new(plugin.Single(), new()) + { + Arguments = new(new OpenAIPromptExecutionSettings()) { { "key", mockAgent.Name } }, + ResultParser = (result) => result.GetValue() ?? null!, + }; + + // Act and Assert + await Assert.ThrowsAsync(() => strategy.NextAsync([mockAgent], [])); } private sealed class TestPlugin(string agentName) diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs index 6f0b446e5e7a..7ee5cf838bc3 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/KernelFunctionTerminationStrategyTests.cs @@ -3,10 +3,8 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.Connectors.OpenAI; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -22,17 +20,26 @@ public class KernelFunctionTerminationStrategyTests [Fact] public async Task VerifyKernelFunctionTerminationStrategyDefaultsAsync() { + // Arrange KernelPlugin plugin = KernelPluginFactory.CreateFromObject(new TestPlugin()); - KernelFunctionTerminationStrategy strategy = new(plugin.Single(), new()); + KernelFunctionTerminationStrategy strategy = + new(plugin.Single(), new()) + { + AgentVariableName = "agent", + HistoryVariableName = "history", + }; + // Assert Assert.Null(strategy.Arguments); Assert.NotNull(strategy.Kernel); Assert.NotNull(strategy.ResultParser); + Assert.NotEqual("agent", KernelFunctionTerminationStrategy.DefaultAgentVariableName); + Assert.NotEqual("history", KernelFunctionTerminationStrategy.DefaultHistoryVariableName); - Mock mockAgent = new(); - - bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + // Act + MockAgent mockAgent = new(); + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []); Assert.True(isTerminating); } @@ -52,9 +59,9 @@ public async Task VerifyKernelFunctionTerminationStrategyParsingAsync() ResultParser = (result) => string.Equals("test", result.GetValue(), StringComparison.OrdinalIgnoreCase) }; - Mock mockAgent = new(); + MockAgent mockAgent = new(); - bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent.Object, []); + bool isTerminating = await strategy.ShouldTerminateAsync(mockAgent, []); Assert.True(isTerminating); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs index a1b739ae1d1e..196a89ded6e3 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/RegExTerminationStrategyTests.cs @@ -2,10 +2,8 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.ChatCompletion; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -13,7 +11,7 @@ namespace SemanticKernel.Agents.UnitTests.Core.Chat; /// /// Unit testing of . /// -public class RegexTerminationStrategyTests +public partial class RegexTerminationStrategyTests { /// /// Verify abililty of strategy to match expression. @@ -21,10 +19,12 @@ public class RegexTerminationStrategyTests [Fact] public async Task VerifyExpressionTerminationStrategyAsync() { + // Arrange RegexTerminationStrategy strategy = new("test"); - Regex r = new("(?:^|\\W)test(?:$|\\W)"); + Regex r = MyRegex(); + // Act and Assert await VerifyResultAsync( expectedResult: false, new(r), @@ -38,9 +38,17 @@ await VerifyResultAsync( private static async Task VerifyResultAsync(bool expectedResult, RegexTerminationStrategy strategyRoot, string content) { + // Arrange ChatMessageContent message = new(AuthorRole.Assistant, content); - Mock agent = new(); - var result = await strategyRoot.ShouldTerminateAsync(agent.Object, [message]); + MockAgent agent = new(); + + // Act + var result = await strategyRoot.ShouldTerminateAsync(agent, [message]); + + // Assert Assert.Equal(expectedResult, result); } + + [GeneratedRegex("(?:^|\\W)test(?:$|\\W)")] + private static partial Regex MyRegex(); } diff --git a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs index 04339a8309e4..8f7ff6b29d03 100644 --- a/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/Chat/SequentialSelectionStrategyTests.cs @@ -3,7 +3,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; -using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core.Chat; @@ -19,28 +18,38 @@ public class SequentialSelectionStrategyTests [Fact] public async Task VerifySequentialSelectionStrategyTurnsAsync() { - Mock agent1 = new(); - Mock agent2 = new(); + // Arrange + MockAgent agent1 = new(); + MockAgent agent2 = new(); - Agent[] agents = [agent1.Object, agent2.Object]; + Agent[] agents = [agent1, agent2]; SequentialSelectionStrategy strategy = new(); - await VerifyNextAgent(agent1.Object); - await VerifyNextAgent(agent2.Object); - await VerifyNextAgent(agent1.Object); - await VerifyNextAgent(agent2.Object); - await VerifyNextAgent(agent1.Object); + // Act and Assert + await VerifyNextAgent(agent1); + await VerifyNextAgent(agent2); + await VerifyNextAgent(agent1); + await VerifyNextAgent(agent2); + await VerifyNextAgent(agent1); + // Arrange strategy.Reset(); - await VerifyNextAgent(agent1.Object); - // Verify index does not exceed current bounds. - agents = [agent1.Object]; - await VerifyNextAgent(agent1.Object); + // Act and Assert + await VerifyNextAgent(agent1); + + // Arrange: Verify index does not exceed current bounds. + agents = [agent1]; + + // Act and Assert + await VerifyNextAgent(agent1); async Task VerifyNextAgent(Agent agent1) { + // Act Agent? nextAgent = await strategy.NextAsync(agents, []); + + // Assert Assert.NotNull(nextAgent); Assert.Equal(agent1.Id, nextAgent.Id); } @@ -52,7 +61,10 @@ async Task VerifyNextAgent(Agent agent1) [Fact] public async Task VerifySequentialSelectionStrategyEmptyAsync() { + // Arrange SequentialSelectionStrategy strategy = new(); + + // Act and Assert await Assert.ThrowsAsync(() => strategy.NextAsync([], [])); } } diff --git a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs index c8a1c0578613..01debd8ded5f 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatCompletionAgentTests.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.History; using Microsoft.SemanticKernel.ChatCompletion; using Moq; using Xunit; @@ -22,6 +23,7 @@ public class ChatCompletionAgentTests [Fact] public void VerifyChatCompletionAgentDefinition() { + // Arrange ChatCompletionAgent agent = new() { @@ -30,6 +32,7 @@ public void VerifyChatCompletionAgentDefinition() Name = "test name", }; + // Assert Assert.NotNull(agent.Id); Assert.Equal("test instructions", agent.Instructions); Assert.Equal("test description", agent.Description); @@ -43,7 +46,8 @@ public void VerifyChatCompletionAgentDefinition() [Fact] public async Task VerifyChatCompletionAgentInvocationAsync() { - var mockService = new Mock(); + // Arrange + Mock mockService = new(); mockService.Setup( s => s.GetChatMessageContentsAsync( It.IsAny(), @@ -51,16 +55,18 @@ public async Task VerifyChatCompletionAgentInvocationAsync() It.IsAny(), It.IsAny())).ReturnsAsync([new(AuthorRole.Assistant, "what?")]); - var agent = - new ChatCompletionAgent() + ChatCompletionAgent agent = + new() { Instructions = "test instructions", Kernel = CreateKernel(mockService.Object), Arguments = [], }; - var result = await agent.InvokeAsync([]).ToArrayAsync(); + // Act + ChatMessageContent[] result = await agent.InvokeAsync([]).ToArrayAsync(); + // Assert Assert.Single(result); mockService.Verify( @@ -79,13 +85,14 @@ public async Task VerifyChatCompletionAgentInvocationAsync() [Fact] public async Task VerifyChatCompletionAgentStreamingAsync() { + // Arrange StreamingChatMessageContent[] returnContent = [ new(AuthorRole.Assistant, "wh"), new(AuthorRole.Assistant, "at?"), ]; - var mockService = new Mock(); + Mock mockService = new(); mockService.Setup( s => s.GetStreamingChatMessageContentsAsync( It.IsAny(), @@ -93,16 +100,18 @@ public async Task VerifyChatCompletionAgentStreamingAsync() It.IsAny(), It.IsAny())).Returns(returnContent.ToAsyncEnumerable()); - var agent = - new ChatCompletionAgent() + ChatCompletionAgent agent = + new() { Instructions = "test instructions", Kernel = CreateKernel(mockService.Object), Arguments = [], }; - var result = await agent.InvokeStreamingAsync([]).ToArrayAsync(); + // Act + StreamingChatMessageContent[] result = await agent.InvokeStreamingAsync([]).ToArrayAsync(); + // Assert Assert.Equal(2, result.Length); mockService.Verify( @@ -115,6 +124,52 @@ public async Task VerifyChatCompletionAgentStreamingAsync() Times.Once); } + /// + /// Verify the invocation and response of . + /// + [Fact] + public void VerifyChatCompletionServiceSelection() + { + // Arrange + Mock mockService = new(); + Kernel kernel = CreateKernel(mockService.Object); + + // Act + (IChatCompletionService service, PromptExecutionSettings? settings) = ChatCompletionAgent.GetChatCompletionService(kernel, null); + // Assert + Assert.Equal(mockService.Object, service); + Assert.Null(settings); + + // Act + (service, settings) = ChatCompletionAgent.GetChatCompletionService(kernel, []); + // Assert + Assert.Equal(mockService.Object, service); + Assert.Null(settings); + + // Act and Assert + Assert.Throws(() => ChatCompletionAgent.GetChatCompletionService(kernel, new KernelArguments(new PromptExecutionSettings() { ServiceId = "anything" }))); + } + + /// + /// Verify the invocation and response of . + /// + [Fact] + public void VerifyChatCompletionChannelKeys() + { + // Arrange + ChatCompletionAgent agent1 = new(); + ChatCompletionAgent agent2 = new(); + ChatCompletionAgent agent3 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; + ChatCompletionAgent agent4 = new() { HistoryReducer = new ChatHistoryTruncationReducer(50) }; + ChatCompletionAgent agent5 = new() { HistoryReducer = new ChatHistoryTruncationReducer(100) }; + + // Act ans Assert + Assert.Equal(agent1.GetChannelKeys(), agent2.GetChannelKeys()); + Assert.Equal(agent3.GetChannelKeys(), agent4.GetChannelKeys()); + Assert.NotEqual(agent1.GetChannelKeys(), agent3.GetChannelKeys()); + Assert.NotEqual(agent3.GetChannelKeys(), agent5.GetChannelKeys()); + } + private static Kernel CreateKernel(IChatCompletionService chatCompletionService) { var builder = Kernel.CreateBuilder(); diff --git a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs index 43aae918ad52..dfa9f59032c1 100644 --- a/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/ChatHistoryChannelTests.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System; -using System.Collections.Generic; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; +using Moq; using Xunit; namespace SemanticKernel.Agents.UnitTests.Core; @@ -22,21 +20,11 @@ public class ChatHistoryChannelTests [Fact] public async Task VerifyAgentWithoutIChatHistoryHandlerAsync() { - TestAgent agent = new(); // Not a IChatHistoryHandler + // Arrange + Mock agent = new(); // Not a IChatHistoryHandler ChatHistoryChannel channel = new(); // Requires IChatHistoryHandler - await Assert.ThrowsAsync(() => channel.InvokeAsync(agent).ToArrayAsync().AsTask()); - } - - private sealed class TestAgent : KernelAgent - { - protected internal override Task CreateChannelAsync(CancellationToken cancellationToken) - { - throw new NotImplementedException(); - } - protected internal override IEnumerable GetChannelKeys() - { - throw new NotImplementedException(); - } + // Act & Assert + await Assert.ThrowsAsync(() => channel.InvokeAsync(agent.Object).ToArrayAsync().AsTask()); } } diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs index a75533474147..d9042305d9fa 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryReducerExtensionsTests.cs @@ -30,8 +30,10 @@ public class ChatHistoryReducerExtensionsTests [InlineData(100, 0, int.MaxValue, 100)] public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? endIndex = null, int? expectedCount = null) { + // Arrange ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)]; + // Act ChatMessageContent[] extractedHistory = history.Extract(startIndex, endIndex).ToArray(); int finalIndex = endIndex ?? messageCount - 1; @@ -39,6 +41,7 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e expectedCount ??= finalIndex - startIndex + 1; + // Assert Assert.Equal(expectedCount, extractedHistory.Length); if (extractedHistory.Length > 0) @@ -58,16 +61,19 @@ public void VerifyChatHistoryExtraction(int messageCount, int startIndex, int? e [InlineData(100, 0)] public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount) { + // Arrange ChatHistory summaries = [.. MockHistoryGenerator.CreateSimpleHistory(summaryCount)]; foreach (ChatMessageContent summary in summaries) { summary.Metadata = new Dictionary() { { "summary", true } }; } + // Act ChatHistory history = [.. summaries, .. MockHistoryGenerator.CreateSimpleHistory(regularCount)]; int finalSummaryIndex = history.LocateSummarizationBoundary("summary"); + // Assert Assert.Equal(summaryCount, finalSummaryIndex); } @@ -77,17 +83,22 @@ public void VerifyGetFinalSummaryIndex(int summaryCount, int regularCount) [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange ChatHistory history = []; + Mock mockReducer = new(); + mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null); + // Act bool isReduced = await history.ReduceAsync(null, default); + // Assert Assert.False(isReduced); Assert.Empty(history); - Mock mockReducer = new(); - mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)null); + // Act isReduced = await history.ReduceAsync(mockReducer.Object, default); + // Assert Assert.False(isReduced); Assert.Empty(history); } @@ -98,13 +109,16 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange Mock mockReducer = new(); mockReducer.Setup(r => r.ReduceAsync(It.IsAny>(), default)).ReturnsAsync((IEnumerable?)[]); ChatHistory history = [.. MockHistoryGenerator.CreateSimpleHistory(10)]; + // Act bool isReduced = await history.ReduceAsync(mockReducer.Object, default); + // Assert Assert.True(isReduced); Assert.Empty(history); } @@ -124,11 +138,13 @@ public async Task VerifyChatHistoryReducedAsync() [InlineData(900, 500, int.MaxValue)] public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount, int? thresholdCount = null) { - // Shape of history doesn't matter since reduction is not expected + // Arrange: Shape of history doesn't matter since reduction is not expected ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.Equal(0, reductionIndex); } @@ -146,11 +162,13 @@ public void VerifyLocateSafeReductionIndexNone(int messageCount, int targetCount [InlineData(1000, 500, 499)] public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCount, int? thresholdCount = null) { - // Generate history with only assistant messages + // Arrange: Generate history with only assistant messages ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateSimpleHistory(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); Assert.Equal(targetCount, messageCount - reductionIndex); } @@ -170,17 +188,20 @@ public void VerifyLocateSafeReductionIndexFound(int messageCount, int targetCoun [InlineData(1000, 500, 499)] public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int targetCount, int? thresholdCount = null) { - // Generate history with alternating user and assistant messages + // Arrange: Generate history with alternating user and assistant messages ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithUserInput(messageCount)]; + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); - // The reduction length should align with a user message, if threshold is specified + // Act: The reduction length should align with a user message, if threshold is specified bool hasThreshold = thresholdCount > 0; int expectedCount = targetCount + (hasThreshold && sourceHistory[^targetCount].Role != AuthorRole.User ? 1 : 0); + // Assert Assert.Equal(expectedCount, messageCount - reductionIndex); } @@ -201,14 +222,16 @@ public void VerifyLocateSafeReductionIndexFoundWithUser(int messageCount, int ta [InlineData(9)] public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, int? thresholdCount = null) { - // Generate a history with function call on index 5 and 9 and + // Arrange: Generate a history with function call on index 5 and 9 and // function result on index 6 and 10 (total length: 14) ChatHistory sourceHistory = [.. MockHistoryGenerator.CreateHistoryWithFunctionContent()]; ChatHistoryTruncationReducer reducer = new(targetCount, thresholdCount); + // Act int reductionIndex = sourceHistory.LocateSafeReductionIndex(targetCount, thresholdCount); + // Assert Assert.True(reductionIndex > 0); // The reduction length avoid splitting function call and result, regardless of threshold @@ -216,7 +239,7 @@ public void VerifyLocateSafeReductionIndexWithFunctionContent(int targetCount, i if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionCallContent)) { - expectedCount += 1; + expectedCount++; } else if (sourceHistory[sourceHistory.Count - targetCount].Items.Any(i => i is FunctionResultContent)) { diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs index f464b6a8214a..53e93d0026c3 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistorySummarizationReducerTests.cs @@ -23,10 +23,12 @@ public class ChatHistorySummarizationReducerTests [InlineData(-1)] [InlineData(-1, int.MaxValue)] [InlineData(int.MaxValue, -1)] - public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null) + public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); + // Act & Assert Assert.Throws(() => new ChatHistorySummarizationReducer(mockCompletionService.Object, targetCount, thresholdCount)); } @@ -34,15 +36,17 @@ public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? /// Verify object state after initialization. /// [Fact] - public void VerifyChatHistoryInitializationState() + public void VerifyInitializationState() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + // Assert Assert.Equal(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions); Assert.True(reducer.FailOnError); + // Act reducer = new(mockCompletionService.Object, 10) { @@ -50,25 +54,62 @@ public void VerifyChatHistoryInitializationState() SummarizationInstructions = "instructions", }; + // Assert Assert.NotEqual(ChatHistorySummarizationReducer.DefaultSummarizationPrompt, reducer.SummarizationInstructions); Assert.False(reducer.FailOnError); } + /// + /// Validate equality override. + /// + [Fact] + public void VerifyEquality() + { + // Arrange + Mock mockCompletionService = this.CreateMockCompletionService(); + + ChatHistorySummarizationReducer reducer1 = new(mockCompletionService.Object, 3, 3); + ChatHistorySummarizationReducer reducer2 = new(mockCompletionService.Object, 3, 3); + ChatHistorySummarizationReducer reducer3 = new(mockCompletionService.Object, 3, 3) { UseSingleSummary = false }; + ChatHistorySummarizationReducer reducer4 = new(mockCompletionService.Object, 3, 3) { SummarizationInstructions = "override" }; + ChatHistorySummarizationReducer reducer5 = new(mockCompletionService.Object, 4, 3); + ChatHistorySummarizationReducer reducer6 = new(mockCompletionService.Object, 3, 5); + ChatHistorySummarizationReducer reducer7 = new(mockCompletionService.Object, 3); + ChatHistorySummarizationReducer reducer8 = new(mockCompletionService.Object, 3); + + // Assert + Assert.True(reducer1.Equals(reducer1)); + Assert.True(reducer1.Equals(reducer2)); + Assert.True(reducer7.Equals(reducer8)); + Assert.True(reducer3.Equals(reducer3)); + Assert.True(reducer4.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer5)); + Assert.False(reducer1.Equals(reducer6)); + Assert.False(reducer1.Equals(reducer7)); + Assert.False(reducer1.Equals(reducer8)); + Assert.False(reducer1.Equals(null)); + } + /// /// Validate hash-code expresses reducer equivalency. /// [Fact] - public void VerifyChatHistoryHasCode() + public void VerifyHashCode() { + // Arrange HashSet reducers = []; Mock mockCompletionService = this.CreateMockCompletionService(); + // Act int hashCode1 = GenerateHashCode(3, 4); int hashCode2 = GenerateHashCode(33, 44); int hashCode3 = GenerateHashCode(3000, 4000); int hashCode4 = GenerateHashCode(3000, 4000); + // Assert Assert.NotEqual(hashCode1, hashCode2); Assert.NotEqual(hashCode2, hashCode3); Assert.Equal(hashCode3, hashCode4); @@ -90,12 +131,15 @@ int GenerateHashCode(int targetCount, int thresholdCount) [Fact] public async Task VerifyChatHistoryReductionSilentFailureAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(throwException: true); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10) { FailOnError = false }; + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -105,10 +149,12 @@ public async Task VerifyChatHistoryReductionSilentFailureAsync() [Fact] public async Task VerifyChatHistoryReductionThrowsOnFailureAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(throwException: true); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act and Assert await Assert.ThrowsAsync(() => reducer.ReduceAsync(sourceHistory)); } @@ -118,12 +164,15 @@ public async Task VerifyChatHistoryReductionThrowsOnFailureAsync() [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 20); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -133,12 +182,15 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11); VerifySummarization(messages[0]); } @@ -149,19 +201,24 @@ public async Task VerifyChatHistoryReducedAsync() [Fact] public async Task VerifyChatHistoryRereducedAsync() { + // Arrange Mock mockCompletionService = this.CreateMockCompletionService(); IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistorySummarizationReducer reducer = new(mockCompletionService.Object, 10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert ChatMessageContent[] messages = VerifyReducedHistory(reducedHistory, 11); VerifySummarization(messages[0]); + // Act reducer = new(mockCompletionService.Object, 10) { UseSingleSummary = false }; reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert messages = VerifyReducedHistory(reducedHistory, 12); VerifySummarization(messages[0]); VerifySummarization(messages[1]); diff --git a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs index eebcf8fc6136..9d8b2e721fdf 100644 --- a/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs +++ b/dotnet/src/Agents/UnitTests/Core/History/ChatHistoryTruncationReducerTests.cs @@ -21,24 +21,54 @@ public class ChatHistoryTruncationReducerTests [InlineData(-1)] [InlineData(-1, int.MaxValue)] [InlineData(int.MaxValue, -1)] - public void VerifyChatHistoryConstructorArgumentValidation(int targetCount, int? thresholdCount = null) + public void VerifyConstructorArgumentValidation(int targetCount, int? thresholdCount = null) { + // Act and Assert Assert.Throws(() => new ChatHistoryTruncationReducer(targetCount, thresholdCount)); } + /// + /// Validate equality override. + /// + [Fact] + public void VerifyEquality() + { + // Arrange + ChatHistoryTruncationReducer reducer1 = new(3, 3); + ChatHistoryTruncationReducer reducer2 = new(3, 3); + ChatHistoryTruncationReducer reducer3 = new(4, 3); + ChatHistoryTruncationReducer reducer4 = new(3, 5); + ChatHistoryTruncationReducer reducer5 = new(3); + ChatHistoryTruncationReducer reducer6 = new(3); + + // Assert + Assert.True(reducer1.Equals(reducer1)); + Assert.True(reducer1.Equals(reducer2)); + Assert.True(reducer5.Equals(reducer6)); + Assert.True(reducer3.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer3)); + Assert.False(reducer1.Equals(reducer4)); + Assert.False(reducer1.Equals(reducer5)); + Assert.False(reducer1.Equals(reducer6)); + Assert.False(reducer1.Equals(null)); + } + /// /// Validate hash-code expresses reducer equivalency. /// [Fact] - public void VerifyChatHistoryHasCode() + public void VerifyHashCode() { + // Arrange HashSet reducers = []; + // Act int hashCode1 = GenerateHashCode(3, 4); int hashCode2 = GenerateHashCode(33, 44); int hashCode3 = GenerateHashCode(3000, 4000); int hashCode4 = GenerateHashCode(3000, 4000); + // Assert Assert.NotEqual(hashCode1, hashCode2); Assert.NotEqual(hashCode2, hashCode3); Assert.Equal(hashCode3, hashCode4); @@ -60,11 +90,14 @@ int GenerateHashCode(int targetCount, int thresholdCount) [Fact] public async Task VerifyChatHistoryNotReducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(10).ToArray(); - ChatHistoryTruncationReducer reducer = new(20); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert Assert.Null(reducedHistory); } @@ -74,11 +107,14 @@ public async Task VerifyChatHistoryNotReducedAsync() [Fact] public async Task VerifyChatHistoryReducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistoryTruncationReducer reducer = new(10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); + // Assert VerifyReducedHistory(reducedHistory, 10); } @@ -88,12 +124,15 @@ public async Task VerifyChatHistoryReducedAsync() [Fact] public async Task VerifyChatHistoryRereducedAsync() { + // Arrange IReadOnlyList sourceHistory = MockHistoryGenerator.CreateSimpleHistory(20).ToArray(); - ChatHistoryTruncationReducer reducer = new(10); + + // Act IEnumerable? reducedHistory = await reducer.ReduceAsync(sourceHistory); reducedHistory = await reducer.ReduceAsync([.. reducedHistory!, .. sourceHistory]); + // Assert VerifyReducedHistory(reducedHistory, 10); } diff --git a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs index 14a938a7b169..d7f370e3734c 100644 --- a/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/Extensions/ChatHistoryExtensionsTests.cs @@ -19,10 +19,12 @@ public class ChatHistoryExtensionsTests [Fact] public void VerifyChatHistoryOrdering() { + // Arrange ChatHistory history = []; history.AddUserMessage("Hi"); history.AddAssistantMessage("Hi"); + // Act and Assert VerifyRole(AuthorRole.User, history.First()); VerifyRole(AuthorRole.Assistant, history.Last()); @@ -36,10 +38,12 @@ public void VerifyChatHistoryOrdering() [Fact] public async Task VerifyChatHistoryOrderingAsync() { + // Arrange ChatHistory history = []; history.AddUserMessage("Hi"); history.AddAssistantMessage("Hi"); + // Act and Assert VerifyRole(AuthorRole.User, history.First()); VerifyRole(AuthorRole.Assistant, history.Last()); diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index 452a0566e11f..96ed232fb109 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -22,8 +22,10 @@ public class BroadcastQueueTests [Fact] public void VerifyBroadcastQueueDefaultConfiguration() { + // Arrange BroadcastQueue queue = new(); + // Assert Assert.True(queue.BlockDuration.TotalSeconds > 0); } @@ -33,7 +35,7 @@ public void VerifyBroadcastQueueDefaultConfiguration() [Fact] public async Task VerifyBroadcastQueueReceiveAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -42,23 +44,31 @@ public async Task VerifyBroadcastQueueReceiveAsync() TestChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Verify initial state + // Act: Verify initial state await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify empty invocation with no channels. + // Act: Verify empty invocation with no channels. queue.Enqueue([], []); await VerifyReceivingStateAsync(receiveCount: 0, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify empty invocation of channel. + // Act: Verify empty invocation of channel. queue.Enqueue([reference], []); await VerifyReceivingStateAsync(receiveCount: 1, queue, channel, "test"); + + // Assert Assert.Empty(channel.ReceivedMessages); - // Verify expected invocation of channel. + // Act: Verify expected invocation of channel. queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]); await VerifyReceivingStateAsync(receiveCount: 2, queue, channel, "test"); + + // Assert Assert.NotEmpty(channel.ReceivedMessages); } @@ -68,7 +78,7 @@ public async Task VerifyBroadcastQueueReceiveAsync() [Fact] public async Task VerifyBroadcastQueueFailureAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -77,9 +87,10 @@ public async Task VerifyBroadcastQueueFailureAsync() BadChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Verify expected invocation of channel. + // Act: Verify expected invocation of channel. queue.Enqueue([reference], [new ChatMessageContent(AuthorRole.User, "hi")]); + // Assert await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); await Assert.ThrowsAsync(() => queue.EnsureSynchronizedAsync(reference)); @@ -91,7 +102,7 @@ public async Task VerifyBroadcastQueueFailureAsync() [Fact] public async Task VerifyBroadcastQueueConcurrencyAsync() { - // Create queue and channel. + // Arrange: Create queue and channel. BroadcastQueue queue = new() { @@ -100,7 +111,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() TestChannel channel = new(); ChannelReference reference = new(channel, "test"); - // Enqueue multiple channels + // Act: Enqueue multiple channels for (int count = 0; count < 10; ++count) { queue.Enqueue([new(channel, $"test{count}")], [new ChatMessageContent(AuthorRole.User, "hi")]); @@ -112,7 +123,7 @@ public async Task VerifyBroadcastQueueConcurrencyAsync() await queue.EnsureSynchronizedAsync(new ChannelReference(channel, $"test{count}")); } - // Verify result + // Assert Assert.NotEmpty(channel.ReceivedMessages); Assert.Equal(10, channel.ReceivedMessages.Count); } diff --git a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs index 0a9715f25115..13cc3203d58c 100644 --- a/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/KeyEncoderTests.cs @@ -17,21 +17,24 @@ public class KeyEncoderTests [Fact] public void VerifyKeyEncoderUniqueness() { + // Act this.VerifyHashEquivalancy([]); this.VerifyHashEquivalancy(nameof(KeyEncoderTests)); this.VerifyHashEquivalancy(nameof(KeyEncoderTests), "http://localhost", "zoo"); - // Verify "well-known" value + // Assert: Verify "well-known" value string localHash = KeyEncoder.GenerateHash([typeof(ChatHistoryChannel).FullName!]); Assert.Equal("Vdx37EnWT9BS+kkCkEgFCg9uHvHNw1+hXMA4sgNMKs4=", localHash); } private void VerifyHashEquivalancy(params string[] keys) { + // Act string hash1 = KeyEncoder.GenerateHash(keys); string hash2 = KeyEncoder.GenerateHash(keys); string hash3 = KeyEncoder.GenerateHash(keys.Concat(["another"])); + // Assert Assert.Equal(hash1, hash2); Assert.NotEqual(hash1, hash3); } diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index f3b833024001..2535446dae7b 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -14,7 +15,7 @@ namespace SemanticKernel.Agents.UnitTests; /// /// Mock definition of with a contract. /// -internal sealed class MockAgent : KernelAgent, IChatHistoryHandler +internal class MockAgent : KernelAgent, IChatHistoryHandler { public int InvokeCount { get; private set; } @@ -46,7 +47,7 @@ public IAsyncEnumerable InvokeStreamingAsync( /// protected internal override IEnumerable GetChannelKeys() { - yield return typeof(ChatHistoryChannel).FullName!; + yield return Guid.NewGuid().ToString(); } /// diff --git a/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs new file mode 100644 index 000000000000..cd51c736ac18 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/AssertCollection.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +internal static class AssertCollection +{ + public static void Equal(IReadOnlyList? source, IReadOnlyList? target, Func? adapter = null) + { + if (source == null) + { + Assert.Null(target); + return; + } + + Assert.NotNull(target); + Assert.Equal(source.Count, target.Count); + + adapter ??= (x) => x; + + for (int i = 0; i < source.Count; i++) + { + Assert.Equal(adapter(source[i]), adapter(target[i])); + } + } + + public static void Equal(IReadOnlyDictionary? source, IReadOnlyDictionary? target) + { + if (source == null) + { + Assert.Null(target); + return; + } + + Assert.NotNull(target); + Assert.Equal(source.Count, target.Count); + + foreach ((TKey key, TValue value) in source) + { + Assert.True(target.TryGetValue(key, out TValue? targetValue)); + Assert.Equal(value, targetValue); + } + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs index b1e4d397eded..6288c6a5aed8 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Azure/AddHeaderRequestPolicyTests.cs @@ -2,7 +2,7 @@ using System.Linq; using Azure.Core; using Azure.Core.Pipeline; -using Microsoft.SemanticKernel.Agents.OpenAI.Azure; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Azure; @@ -18,14 +18,17 @@ public class AddHeaderRequestPolicyTests [Fact] public void VerifyAddHeaderRequestPolicyExecution() { + // Arrange using HttpClientTransport clientTransport = new(); HttpPipeline pipeline = new(clientTransport); HttpMessage message = pipeline.CreateMessage(); - AddHeaderRequestPolicy policy = new(headerName: "testname", headerValue: "testvalue"); + + // Act policy.OnSendingRequest(message); + // Assert Assert.Single(message.Request.Headers); HttpHeader header = message.Request.Headers.Single(); Assert.Equal("testname", header.Name); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs index 0b0a0707e49a..97dbf32903d6 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/AuthorRoleExtensionsTests.cs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; using Xunit; using KernelExtensions = Microsoft.SemanticKernel.Agents.OpenAI; @@ -29,7 +29,10 @@ public void VerifyToMessageRole() private void VerifyRoleConversion(AuthorRole inputRole, MessageRole expectedRole) { + // Arrange MessageRole convertedRole = inputRole.ToMessageRole(); + + // Assert Assert.Equal(expectedRole, convertedRole); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs index 3f982f3a7b47..70c27ccb2152 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelExtensionsTests.cs @@ -17,11 +17,15 @@ public class KernelExtensionsTests [Fact] public void VerifyGetKernelFunctionLookup() { + // Arrange Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); + // Act KernelFunction function = kernel.GetKernelFunction($"{nameof(TestPlugin)}-{nameof(TestPlugin.TestFunction)}", '-'); + + // Assert Assert.NotNull(function); Assert.Equal(nameof(TestPlugin.TestFunction), function.Name); } @@ -32,10 +36,12 @@ public void VerifyGetKernelFunctionLookup() [Fact] public void VerifyGetKernelFunctionInvalid() { + // Arrange Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); + // Act and Assert Assert.Throws(() => kernel.GetKernelFunction("a", '-')); Assert.Throws(() => kernel.GetKernelFunction("a-b", ':')); Assert.Throws(() => kernel.GetKernelFunction("a-b-c", '-')); diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs index eeb8a4d3b9d1..acf195840366 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/Extensions/KernelFunctionExtensionsTests.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.ComponentModel; -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents.OpenAI; +using OpenAI.Assistants; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI.Extensions; @@ -19,18 +19,28 @@ public class KernelFunctionExtensionsTests [Fact] public void VerifyKernelFunctionToFunctionTool() { + // Arrange KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + + // Assert Assert.Equal(2, plugin.FunctionCount); + // Arrange KernelFunction f1 = plugin[nameof(TestPlugin.TestFunction1)]; KernelFunction f2 = plugin[nameof(TestPlugin.TestFunction2)]; - FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin", "-"); - Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.Name, StringComparison.Ordinal); + // Act + FunctionToolDefinition definition1 = f1.ToToolDefinition("testplugin"); + + // Assert + Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction1)}", definition1.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition1.Description); - FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin", "-"); - Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.Name, StringComparison.Ordinal); + // Act + FunctionToolDefinition definition2 = f2.ToToolDefinition("testplugin"); + + // Assert + Assert.StartsWith($"testplugin-{nameof(TestPlugin.TestFunction2)}", definition2.FunctionName, StringComparison.Ordinal); Assert.Equal("test description", definition2.Description); } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs new file mode 100644 index 000000000000..50dec2cb95ae --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantMessageFactoryTests.cs @@ -0,0 +1,210 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; + +/// +/// Unit testing of . +/// +public class AssistantMessageFactoryTests +{ + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsDefault() + { + // Arrange (Setup message with null metadata) + ChatMessageContent message = new(AuthorRole.User, "test"); + + // Act: Create options + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); + + // Assert + Assert.NotNull(options); + Assert.Empty(options.Metadata); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataEmpty() + { + // Arrange Setup message with empty metadata + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = new Dictionary() + }; + + // Act: Create options + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); + + // Assert + Assert.NotNull(options); + Assert.Empty(options.Metadata); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadata() + { + // Arrange: Setup message with metadata + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = + new Dictionary() + { + { "a", 1 }, + { "b", "2" }, + } + }; + + // Act: Create options + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); + + // Assert + Assert.NotNull(options); + Assert.NotEmpty(options.Metadata); + Assert.Equal(2, options.Metadata.Count); + Assert.Equal("1", options.Metadata["a"]); + Assert.Equal("2", options.Metadata["b"]); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterCreateOptionsWithMetadataNull() + { + // Arrange: Setup message with null metadata value + ChatMessageContent message = + new(AuthorRole.User, "test") + { + Metadata = + new Dictionary() + { + { "a", null }, + { "b", "2" }, + } + }; + + // Act: Create options + MessageCreationOptions options = AssistantMessageFactory.CreateOptions(message); + + // Assert + Assert.NotNull(options); + Assert.NotEmpty(options.Metadata); + Assert.Equal(2, options.Metadata.Count); + Assert.Equal(string.Empty, options.Metadata["a"]); + Assert.Equal("2", options.Metadata["b"]); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageContentsWithText() + { + // Arrange + ChatMessageContent message = new(AuthorRole.User, items: [new TextContent("test")]); + + // Act + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().Text); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithImageUrl() + { + // Arrange + ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new Uri("https://localhost/myimage.png"))]); + + // Act + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageUrl); + } + + /// + /// Verify options creation. + /// + [Fact(Skip = "API bug with data Uri construction")] + public void VerifyAssistantMessageAdapterGetMessageWithImageData() + { + // Arrange + ChatMessageContent message = new(AuthorRole.User, items: [new ImageContent(new byte[] { 1, 2, 3 }, "image/png")]); + + // Act + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageUrl); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithImageFile() + { + // Arrange + ChatMessageContent message = new(AuthorRole.User, items: [new FileReferenceContent("file-id")]); + + // Act + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert + Assert.NotNull(contents); + Assert.Single(contents); + Assert.NotNull(contents.Single().ImageFileId); + } + + /// + /// Verify options creation. + /// + [Fact] + public void VerifyAssistantMessageAdapterGetMessageWithAll() + { + // Arrange + ChatMessageContent message = + new( + AuthorRole.User, + items: + [ + new TextContent("test"), + new ImageContent(new Uri("https://localhost/myimage.png")), + new FileReferenceContent("file-id") + ]); + + // Act + MessageContent[] contents = AssistantMessageFactory.GetMessageContents(message).ToArray(); + + // Assert + Assert.NotNull(contents); + Assert.Equal(3, contents.Length); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs new file mode 100644 index 000000000000..d6bcf91b8a94 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/Internal/AssistantRunOptionsFactoryTests.cs @@ -0,0 +1,139 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using OpenAI.Assistants; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI.Internal; + +/// +/// Unit testing of . +/// +public class AssistantRunOptionsFactoryTests +{ + /// + /// Verify run options generation with null . + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsNullTest() + { + // Arrange + OpenAIAssistantDefinition definition = + new("gpt-anything") + { + Temperature = 0.5F, + }; + + // Act + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, null); + + // Assert + Assert.NotNull(options); + Assert.Null(options.Temperature); + Assert.Null(options.NucleusSamplingFactor); + Assert.Empty(options.Metadata); + } + + /// + /// Verify run options generation with equivalent . + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsEquivalentTest() + { + // Arrange + OpenAIAssistantDefinition definition = + new("gpt-anything") + { + Temperature = 0.5F, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Temperature = 0.5F, + }; + + // Act + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + // Assert + Assert.NotNull(options); + Assert.Null(options.Temperature); + Assert.Null(options.NucleusSamplingFactor); + } + + /// + /// Verify run options generation with override. + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsOverrideTest() + { + // Arrange + OpenAIAssistantDefinition definition = + new("gpt-anything") + { + Temperature = 0.5F, + ExecutionOptions = + new() + { + TruncationMessageCount = 5, + }, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Temperature = 0.9F, + TruncationMessageCount = 8, + EnableJsonResponse = true, + }; + + // Act + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + // Assert + Assert.NotNull(options); + Assert.Equal(0.9F, options.Temperature); + Assert.Equal(8, options.TruncationStrategy.LastMessages); + Assert.Equal(AssistantResponseFormat.JsonObject, options.ResponseFormat); + Assert.Null(options.NucleusSamplingFactor); + } + + /// + /// Verify run options generation with metadata. + /// + [Fact] + public void AssistantRunOptionsFactoryExecutionOptionsMetadataTest() + { + // Arrange + OpenAIAssistantDefinition definition = + new("gpt-anything") + { + Temperature = 0.5F, + ExecutionOptions = + new() + { + TruncationMessageCount = 5, + }, + }; + + OpenAIAssistantInvocationOptions invocationOptions = + new() + { + Metadata = new Dictionary + { + { "key1", "value" }, + { "key2", null! }, + }, + }; + + // Act + RunCreationOptions options = AssistantRunOptionsFactory.GenerateOptions(definition, invocationOptions); + + // Assert + Assert.Equal(2, options.Metadata.Count); + Assert.Equal("value", options.Metadata["key1"]); + Assert.Equal(string.Empty, options.Metadata["key2"]); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs index 1d9a9ec9dfcf..ef67c48f1473 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantAgentTests.cs @@ -4,12 +4,14 @@ using System.Linq; using System.Net; using System.Net.Http; +using System.Text; +using System.Text.Json; using System.Threading.Tasks; -using Azure.AI.OpenAI.Assistants; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Assistants; using Xunit; namespace SemanticKernel.Agents.UnitTests.OpenAI; @@ -30,100 +32,257 @@ public sealed class OpenAIAssistantAgentTests : IDisposable [Fact] public async Task VerifyOpenAIAssistantAgentCreationEmptyAsync() { - OpenAIAssistantDefinition definition = - new() - { - ModelId = "testmodel", - }; - - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); - - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(targetAzure: true, useVersion: true), - definition); + // Arrange + OpenAIAssistantDefinition definition = new("testmodel"); - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.Null(agent.Instructions); - Assert.Null(agent.Name); - Assert.Null(agent.Description); - Assert.False(agent.IsDeleted); + // Act and Assert + await this.VerifyAgentCreationAsync(definition); } /// /// Verify the invocation and response of - /// for an agent with optional properties defined. + /// for an agent with name, instructions, and description. /// [Fact] public async Task VerifyOpenAIAssistantAgentCreationPropertiesAsync() { + // Arrange OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", Name = "testname", Description = "testdescription", Instructions = "testinstructions", }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentFull); + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with code-interpreter enabled. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + EnableCodeInterpreter = true, + }; - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.NotNull(agent.Instructions); - Assert.NotNull(agent.Name); - Assert.NotNull(agent.Description); - Assert.False(agent.IsDeleted); + // Act and Assert + await this.VerifyAgentCreationAsync(definition); } /// /// Verify the invocation and response of - /// for an agent that has all properties defined.. + /// for an agent with code-interpreter files. /// [Fact] - public async Task VerifyOpenAIAssistantAgentCreationEverythingAsync() + public async Task VerifyOpenAIAssistantAgentCreationWithCodeInterpreterFilesAsync() { + // Arrange OpenAIAssistantDefinition definition = - new() + new("testmodel") { - ModelId = "testmodel", EnableCodeInterpreter = true, - EnableRetrieval = true, - FileIds = ["#1", "#2"], - Metadata = new Dictionary() { { "a", "1" } }, + CodeInterpreterFileIds = ["file1", "file2"], }; - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentWithEverything); + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } - OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - this._emptyKernel, - this.CreateTestConfiguration(), - definition); + /// + /// Verify the invocation and response of + /// for an agent with a file-search and no vector-store + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithFileSearchAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + EnableFileSearch = true, + }; - Assert.NotNull(agent); - Assert.Equal(2, agent.Tools.Count); - Assert.True(agent.Tools.OfType().Any()); - Assert.True(agent.Tools.OfType().Any()); - Assert.NotEmpty(agent.FileIds); - Assert.NotEmpty(agent.Metadata); + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with a vector-store-id (for file-search). + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithVectorStoreAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + EnableFileSearch = true, + VectorStoreId = "#vs1", + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with metadata. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithMetadataAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + Metadata = new Dictionary() + { + { "a", "1" }, + { "b", "2" }, + }, + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with json-response mode enabled. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithJsonResponseAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + EnableJsonResponse = true, + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with temperature defined. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithTemperatureAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + Temperature = 2.0F, + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with topP defined. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithTopPAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + TopP = 2.0F, + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with empty execution settings. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + ExecutionOptions = new OpenAIAssistantExecutionOptions(), + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with populated execution settings. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithExecutionOptionsAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + ExecutionOptions = + new() + { + MaxCompletionTokens = 100, + ParallelToolCallsEnabled = false, + } + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); + } + + /// + /// Verify the invocation and response of + /// for an agent with execution settings and meta-data. + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreationWithEmptyExecutionOptionsAndMetadataAsync() + { + // Arrange + OpenAIAssistantDefinition definition = + new("testmodel") + { + ExecutionOptions = new(), + Metadata = new Dictionary() + { + { "a", "1" }, + { "b", "2" }, + }, + }; + + // Act and Assert + await this.VerifyAgentCreationAsync(definition); } /// /// Verify the invocation and response of . /// [Fact] - public async Task VerifyOpenAIAssistantAgentRetrieveAsync() + public async Task VerifyOpenAIAssistantAgentRetrievalAsync() { - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); + // Arrange + OpenAIAssistantDefinition definition = new("testmodel"); + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); OpenAIAssistantAgent agent = await OpenAIAssistantAgent.RetrieveAsync( @@ -131,12 +290,8 @@ await OpenAIAssistantAgent.RetrieveAsync( this.CreateTestConfiguration(), "#id"); - Assert.NotNull(agent); - Assert.NotNull(agent.Id); - Assert.Null(agent.Instructions); - Assert.Null(agent.Name); - Assert.Null(agent.Description); - Assert.False(agent.IsDeleted); + // Act and Assert + ValidateAgentDefinition(agent, definition); } /// @@ -145,16 +300,50 @@ await OpenAIAssistantAgent.RetrieveAsync( [Fact] public async Task VerifyOpenAIAssistantAgentDeleteAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); + // Assert Assert.False(agent.IsDeleted); + // Arrange this.SetupResponse(HttpStatusCode.OK, ResponseContent.DeleteAgent); + // Act await agent.DeleteAsync(); + // Assert Assert.True(agent.IsDeleted); + // Act await agent.DeleteAsync(); // Doesn't throw + // Assert Assert.True(agent.IsDeleted); + await Assert.ThrowsAsync(() => agent.AddChatMessageAsync("threadid", new(AuthorRole.User, "test"))); + await Assert.ThrowsAsync(() => agent.InvokeAsync("threadid").ToArrayAsync().AsTask()); + } + + /// + /// Verify the deletion of agent via . + /// + [Fact] + public async Task VerifyOpenAIAssistantAgentCreateThreadAsync() + { + // Arrange + OpenAIAssistantAgent agent = await this.CreateAgentAsync(); + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + + // Act + string threadId = await agent.CreateThreadAsync(); + // Assert + Assert.NotNull(threadId); + + // Arrange + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateThread); + + // Act + threadId = await agent.CreateThreadAsync(new()); + // Assert + Assert.NotNull(threadId); } /// @@ -163,6 +352,7 @@ public async Task VerifyOpenAIAssistantAgentDeleteAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -174,7 +364,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); @@ -186,6 +380,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -197,7 +392,11 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() ResponseContent.GetTextMessageWithAnnotation); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Equal(2, messages[0].Items.Count); Assert.NotNull(messages[0].Items.SingleOrDefault(c => c is TextContent)); @@ -210,6 +409,7 @@ public async Task VerifyOpenAIAssistantAgentChatTextMessageWithAnnotationAsync() [Fact] public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -221,7 +421,11 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() ResponseContent.GetImageMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); @@ -233,7 +437,7 @@ public async Task VerifyOpenAIAssistantAgentChatImageMessageAsync() [Fact] public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() { - // Create agent + // Arrange: Create agent OpenAIAssistantAgent agent = await this.CreateAgentAsync(); // Initialize agent channel @@ -246,18 +450,22 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + // Assert Assert.Single(messages); - // Setup messages + // Arrange: Setup messages this.SetupResponses( HttpStatusCode.OK, ResponseContent.ListMessagesPageMore, ResponseContent.ListMessagesPageMore, ResponseContent.ListMessagesPageFinal); - // Get messages and verify + // Act: Get messages messages = await chat.GetChatMessagesAsync(agent).ToArrayAsync(); + // Assert Assert.Equal(5, messages.Length); } @@ -267,7 +475,7 @@ public async Task VerifyOpenAIAssistantAgentGetMessagesAsync() [Fact] public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() { - // Create agent + // Arrange: Create agent OpenAIAssistantAgent agent = await this.CreateAgentAsync(); // Initialize agent channel @@ -279,12 +487,18 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() ResponseContent.MessageSteps, ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + // Assert Assert.Single(messages); + // Arrange chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "hi")); + // Act messages = await chat.GetChatMessagesAsync().ToArrayAsync(); + // Assert Assert.Equal(2, messages.Length); } @@ -294,6 +508,7 @@ public async Task VerifyOpenAIAssistantAgentAddMessagesAsync() [Fact] public async Task VerifyOpenAIAssistantAgentListDefinitionAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); this.SetupResponses( @@ -302,20 +517,24 @@ public async Task VerifyOpenAIAssistantAgentListDefinitionAsync() ResponseContent.ListAgentsPageMore, ResponseContent.ListAgentsPageFinal); + // Act var messages = await OpenAIAssistantAgent.ListDefinitionsAsync( this.CreateTestConfiguration()).ToArrayAsync(); + // Assert Assert.Equal(7, messages.Length); + // Arrange this.SetupResponses( HttpStatusCode.OK, ResponseContent.ListAgentsPageMore, - ResponseContent.ListAgentsPageMore); + ResponseContent.ListAgentsPageFinal); + // Act messages = await OpenAIAssistantAgent.ListDefinitionsAsync( - this.CreateTestConfiguration(), - maxResults: 4).ToArrayAsync(); + this.CreateTestConfiguration()).ToArrayAsync(); + // Assert Assert.Equal(4, messages.Length); } @@ -325,6 +544,7 @@ await OpenAIAssistantAgent.ListDefinitionsAsync( [Fact] public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync() { + // Arrange OpenAIAssistantAgent agent = await this.CreateAgentAsync(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); @@ -342,7 +562,11 @@ public async Task VerifyOpenAIAssistantAgentWithFunctionCallAsync() ResponseContent.GetTextMessage); AgentGroupChat chat = new(); + + // Act ChatMessageContent[] messages = await chat.InvokeAsync(agent).ToArrayAsync(); + + // Assert Assert.Single(messages); Assert.Single(messages[0].Items); Assert.IsType(messages[0].Items[0]); @@ -365,15 +589,95 @@ public OpenAIAssistantAgentTests() this._emptyKernel = new Kernel(); } - private Task CreateAgentAsync() + private async Task VerifyAgentCreationAsync(OpenAIAssistantDefinition definition) { - OpenAIAssistantDefinition definition = - new() + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + this._emptyKernel, + this.CreateTestConfiguration(), + definition); + + ValidateAgentDefinition(agent, definition); + } + + private static void ValidateAgentDefinition(OpenAIAssistantAgent agent, OpenAIAssistantDefinition sourceDefinition) + { + // Verify fundamental state + Assert.NotNull(agent); + Assert.NotNull(agent.Id); + Assert.False(agent.IsDeleted); + Assert.NotNull(agent.Definition); + Assert.Equal(sourceDefinition.ModelId, agent.Definition.ModelId); + + // Verify core properties + Assert.Equal(sourceDefinition.Instructions ?? string.Empty, agent.Instructions); + Assert.Equal(sourceDefinition.Name ?? string.Empty, agent.Name); + Assert.Equal(sourceDefinition.Description ?? string.Empty, agent.Description); + + // Verify options + Assert.Equal(sourceDefinition.Temperature, agent.Definition.Temperature); + Assert.Equal(sourceDefinition.TopP, agent.Definition.TopP); + Assert.Equal(sourceDefinition.ExecutionOptions?.MaxCompletionTokens, agent.Definition.ExecutionOptions?.MaxCompletionTokens); + Assert.Equal(sourceDefinition.ExecutionOptions?.MaxPromptTokens, agent.Definition.ExecutionOptions?.MaxPromptTokens); + Assert.Equal(sourceDefinition.ExecutionOptions?.ParallelToolCallsEnabled, agent.Definition.ExecutionOptions?.ParallelToolCallsEnabled); + Assert.Equal(sourceDefinition.ExecutionOptions?.TruncationMessageCount, agent.Definition.ExecutionOptions?.TruncationMessageCount); + + // Verify tool definitions + int expectedToolCount = 0; + + bool hasCodeInterpreter = false; + if (sourceDefinition.EnableCodeInterpreter) + { + hasCodeInterpreter = true; + ++expectedToolCount; + } + + Assert.Equal(hasCodeInterpreter, agent.Tools.OfType().Any()); + + bool hasFileSearch = false; + if (sourceDefinition.EnableFileSearch) + { + hasFileSearch = true; + ++expectedToolCount; + } + + Assert.Equal(hasFileSearch, agent.Tools.OfType().Any()); + + Assert.Equal(expectedToolCount, agent.Tools.Count); + + // Verify metadata + Assert.NotNull(agent.Definition.Metadata); + if (sourceDefinition.ExecutionOptions == null) + { + Assert.Equal(sourceDefinition.Metadata ?? new Dictionary(), agent.Definition.Metadata); + } + else // Additional metadata present when execution options are defined + { + Assert.Equal((sourceDefinition.Metadata?.Count ?? 0) + 1, agent.Definition.Metadata.Count); + + if (sourceDefinition.Metadata != null) { - ModelId = "testmodel", - }; + foreach (var (key, value) in sourceDefinition.Metadata) + { + string? targetValue = agent.Definition.Metadata[key]; + Assert.NotNull(targetValue); + Assert.Equal(value, targetValue); + } + } + } + + // Verify detail definition + Assert.Equal(sourceDefinition.VectorStoreId, agent.Definition.VectorStoreId); + Assert.Equal(sourceDefinition.CodeInterpreterFileIds, agent.Definition.CodeInterpreterFileIds); + } - this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentSimple); + private Task CreateAgentAsync() + { + OpenAIAssistantDefinition definition = new("testmodel"); + + this.SetupResponse(HttpStatusCode.OK, ResponseContent.CreateAgentPayload(definition)); return OpenAIAssistantAgent.CreateAsync( @@ -382,14 +686,10 @@ private Task CreateAgentAsync() definition); } - private OpenAIAssistantConfiguration CreateTestConfiguration(bool targetAzure = false, bool useVersion = false) - { - return new(apiKey: "fakekey", endpoint: targetAzure ? "https://localhost" : null) - { - HttpClient = this._httpClient, - Version = useVersion ? AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview : null, - }; - } + private OpenAIClientProvider CreateTestConfiguration(bool targetAzure = false) + => targetAzure ? + OpenAIClientProvider.ForAzureOpenAI(apiKey: "fakekey", endpoint: new Uri("https://localhost"), this._httpClient) : + OpenAIClientProvider.ForOpenAI(apiKey: "fakekey", endpoint: null, this._httpClient); private void SetupResponse(HttpStatusCode statusCode, string content) { @@ -423,58 +723,114 @@ public void MyFunction(int index) private static class ResponseContent { - public const string CreateAgentSimple = - """ + public static string CreateAgentPayload(OpenAIAssistantDefinition definition) + { + StringBuilder builder = new(); + builder.AppendLine("{"); + builder.AppendLine(@" ""id"": ""asst_abc123"","); + builder.AppendLine(@" ""object"": ""assistant"","); + builder.AppendLine(@" ""created_at"": 1698984975,"); + builder.AppendLine(@$" ""name"": ""{definition.Name}"","); + builder.AppendLine(@$" ""description"": ""{definition.Description}"","); + builder.AppendLine(@$" ""instructions"": ""{definition.Instructions}"","); + builder.AppendLine(@$" ""model"": ""{definition.ModelId}"","); + + bool hasCodeInterpreter = definition.EnableCodeInterpreter; + bool hasCodeInterpreterFiles = (definition.CodeInterpreterFileIds?.Count ?? 0) > 0; + bool hasFileSearch = definition.EnableFileSearch; + if (!hasCodeInterpreter && !hasFileSearch) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [], - "file_ids": [], - "metadata": {} + builder.AppendLine(@" ""tools"": [],"); } - """; + else + { + builder.AppendLine(@" ""tools"": ["); - public const string CreateAgentFull = - """ + if (hasCodeInterpreter) + { + builder.Append(@$" {{ ""type"": ""code_interpreter"" }}{(hasFileSearch ? "," : string.Empty)}"); + } + + if (hasFileSearch) + { + builder.AppendLine(@" { ""type"": ""file_search"" }"); + } + + builder.AppendLine(" ],"); + } + + if (!hasCodeInterpreterFiles && !hasFileSearch) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": "testname", - "description": "testdescription", - "model": "gpt-4-turbo", - "instructions": "testinstructions", - "tools": [], - "file_ids": [], - "metadata": {} + builder.AppendLine(@" ""tool_resources"": {},"); } - """; + else + { + builder.AppendLine(@" ""tool_resources"": {"); - public const string CreateAgentWithEverything = - """ + if (hasCodeInterpreterFiles) + { + string fileIds = string.Join(",", definition.CodeInterpreterFileIds!.Select(fileId => "\"" + fileId + "\"")); + builder.AppendLine(@$" ""code_interpreter"": {{ ""file_ids"": [{fileIds}] }}{(hasFileSearch ? "," : string.Empty)}"); + } + + if (hasFileSearch) + { + builder.AppendLine(@$" ""file_search"": {{ ""vector_store_ids"": [""{definition.VectorStoreId}""] }}"); + } + + builder.AppendLine(" },"); + } + + if (definition.Temperature.HasValue) { - "id": "asst_abc123", - "object": "assistant", - "created_at": 1698984975, - "name": null, - "description": null, - "model": "gpt-4-turbo", - "instructions": null, - "tools": [ + builder.AppendLine(@$" ""temperature"": {definition.Temperature},"); + } + + if (definition.TopP.HasValue) + { + builder.AppendLine(@$" ""top_p"": {definition.TopP},"); + } + + bool hasExecutionOptions = definition.ExecutionOptions != null; + int metadataCount = (definition.Metadata?.Count ?? 0); + if (metadataCount == 0 && !hasExecutionOptions) + { + builder.AppendLine(@" ""metadata"": {}"); + } + else + { + int index = 0; + builder.AppendLine(@" ""metadata"": {"); + + if (hasExecutionOptions) { - "type": "code_interpreter" - }, + string serializedExecutionOptions = JsonSerializer.Serialize(definition.ExecutionOptions); + builder.AppendLine(@$" ""{OpenAIAssistantAgent.OptionsMetadataKey}"": ""{JsonEncodedText.Encode(serializedExecutionOptions)}""{(metadataCount > 0 ? "," : string.Empty)}"); + } + + if (metadataCount > 0) { - "type": "retrieval" + foreach (var (key, value) in definition.Metadata!) + { + builder.AppendLine(@$" ""{key}"": ""{value}""{(index < metadataCount - 1 ? "," : string.Empty)}"); + ++index; + } } - ], - "file_ids": ["#1", "#2"], - "metadata": {"a": "1"} + + builder.AppendLine(" }"); + } + + builder.AppendLine("}"); + + return builder.ToString(); + } + + public const string CreateAgentWithEverything = + """ + { + "tool_resources": { + "file_search": { "vector_store_ids": ["#vs"] } + }, } """; @@ -748,7 +1104,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} }, { @@ -760,7 +1115,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} }, { @@ -772,7 +1126,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": null, "tools": [], - "file_ids": [], "metadata": {} } ], @@ -796,7 +1149,6 @@ private static class ResponseContent "model": "gpt-4-turbo", "instructions": "You are a helpful assistant designed to make me better at coding!", "tools": [], - "file_ids": [], "metadata": {} } ], diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs deleted file mode 100644 index 3708ab50ab97..000000000000 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantConfigurationTests.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. -using System; -using System.Net.Http; -using Azure.AI.OpenAI.Assistants; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Xunit; - -namespace SemanticKernel.Agents.UnitTests.OpenAI; - -/// -/// Unit testing of . -/// -public class OpenAIAssistantConfigurationTests -{ - /// - /// Verify initial state. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationInitialState() - { - OpenAIAssistantConfiguration config = new(apiKey: "testkey"); - - Assert.Equal("testkey", config.ApiKey); - Assert.Null(config.Endpoint); - Assert.Null(config.HttpClient); - Assert.Null(config.Version); - } - - /// - /// Verify assignment. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationAssignment() - { - using HttpClient client = new(); - - OpenAIAssistantConfiguration config = - new(apiKey: "testkey", endpoint: "https://localhost") - { - HttpClient = client, - Version = AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, - }; - - Assert.Equal("testkey", config.ApiKey); - Assert.Equal("https://localhost", config.Endpoint); - Assert.NotNull(config.HttpClient); - Assert.Equal(AssistantsClientOptions.ServiceVersion.V2024_02_15_Preview, config.Version); - } - - /// - /// Verify secure endpoint. - /// - [Fact] - public void VerifyOpenAIAssistantConfigurationThrows() - { - using HttpClient client = new(); - - Assert.Throws( - () => new OpenAIAssistantConfiguration(apiKey: "testkey", endpoint: "http://localhost")); - } -} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs index b17b61211c18..f8547f375f13 100644 --- a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantDefinitionTests.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Text.Json; using Microsoft.SemanticKernel.Agents.OpenAI; using Xunit; @@ -16,17 +17,27 @@ public class OpenAIAssistantDefinitionTests [Fact] public void VerifyOpenAIAssistantDefinitionInitialState() { - OpenAIAssistantDefinition definition = new(); + // Arrange + OpenAIAssistantDefinition definition = new("testmodel"); - Assert.Null(definition.Id); + // Assert + Assert.Equal(string.Empty, definition.Id); + Assert.Equal("testmodel", definition.ModelId); Assert.Null(definition.Name); - Assert.Null(definition.ModelId); Assert.Null(definition.Instructions); Assert.Null(definition.Description); Assert.Null(definition.Metadata); - Assert.Null(definition.FileIds); + Assert.Null(definition.ExecutionOptions); + Assert.Null(definition.Temperature); + Assert.Null(definition.TopP); + Assert.False(definition.EnableFileSearch); + Assert.Null(definition.VectorStoreId); + Assert.Null(definition.CodeInterpreterFileIds); Assert.False(definition.EnableCodeInterpreter); - Assert.False(definition.EnableRetrieval); + Assert.False(definition.EnableJsonResponse); + + // Act and Assert + ValidateSerialization(definition); } /// @@ -35,28 +46,80 @@ public void VerifyOpenAIAssistantDefinitionInitialState() [Fact] public void VerifyOpenAIAssistantDefinitionAssignment() { + // Arrange OpenAIAssistantDefinition definition = - new() + new("testmodel") { Id = "testid", Name = "testname", - ModelId = "testmodel", Instructions = "testinstructions", Description = "testdescription", - FileIds = ["id"], + EnableFileSearch = true, + VectorStoreId = "#vs", Metadata = new Dictionary() { { "a", "1" } }, + Temperature = 2, + TopP = 0, + ExecutionOptions = + new() + { + MaxCompletionTokens = 1000, + MaxPromptTokens = 1000, + ParallelToolCallsEnabled = false, + TruncationMessageCount = 12, + }, + CodeInterpreterFileIds = ["file1"], EnableCodeInterpreter = true, - EnableRetrieval = true, + EnableJsonResponse = true, }; + // Assert Assert.Equal("testid", definition.Id); Assert.Equal("testname", definition.Name); Assert.Equal("testmodel", definition.ModelId); Assert.Equal("testinstructions", definition.Instructions); Assert.Equal("testdescription", definition.Description); + Assert.True(definition.EnableFileSearch); + Assert.Equal("#vs", definition.VectorStoreId); + Assert.Equal(2, definition.Temperature); + Assert.Equal(0, definition.TopP); + Assert.NotNull(definition.ExecutionOptions); + Assert.Equal(1000, definition.ExecutionOptions.MaxCompletionTokens); + Assert.Equal(1000, definition.ExecutionOptions.MaxPromptTokens); + Assert.Equal(12, definition.ExecutionOptions.TruncationMessageCount); + Assert.False(definition.ExecutionOptions.ParallelToolCallsEnabled); Assert.Single(definition.Metadata); - Assert.Single(definition.FileIds); + Assert.Single(definition.CodeInterpreterFileIds); Assert.True(definition.EnableCodeInterpreter); - Assert.True(definition.EnableRetrieval); + Assert.True(definition.EnableJsonResponse); + + // Act and Assert + ValidateSerialization(definition); + } + + private static void ValidateSerialization(OpenAIAssistantDefinition source) + { + string json = JsonSerializer.Serialize(source); + + OpenAIAssistantDefinition? target = JsonSerializer.Deserialize(json); + + Assert.NotNull(target); + Assert.Equal(source.Id, target.Id); + Assert.Equal(source.Name, target.Name); + Assert.Equal(source.ModelId, target.ModelId); + Assert.Equal(source.Instructions, target.Instructions); + Assert.Equal(source.Description, target.Description); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + Assert.Equal(source.Temperature, target.Temperature); + Assert.Equal(source.TopP, target.TopP); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter); + Assert.Equal(source.ExecutionOptions?.MaxCompletionTokens, target.ExecutionOptions?.MaxCompletionTokens); + Assert.Equal(source.ExecutionOptions?.MaxPromptTokens, target.ExecutionOptions?.MaxPromptTokens); + Assert.Equal(source.ExecutionOptions?.TruncationMessageCount, target.ExecutionOptions?.TruncationMessageCount); + Assert.Equal(source.ExecutionOptions?.ParallelToolCallsEnabled, target.ExecutionOptions?.ParallelToolCallsEnabled); + AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds); + AssertCollection.Equal(source.Metadata, target.Metadata); } } diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs new file mode 100644 index 000000000000..99cbe012f183 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIAssistantInvocationOptionsTests.cs @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIAssistantInvocationOptionsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void OpenAIAssistantInvocationOptionsInitialState() + { + // Arrange + OpenAIAssistantInvocationOptions options = new(); + + // Assert + Assert.Null(options.ModelName); + Assert.Null(options.Metadata); + Assert.Null(options.Temperature); + Assert.Null(options.TopP); + Assert.Null(options.ParallelToolCallsEnabled); + Assert.Null(options.MaxCompletionTokens); + Assert.Null(options.MaxPromptTokens); + Assert.Null(options.TruncationMessageCount); + Assert.Null(options.EnableJsonResponse); + Assert.False(options.EnableCodeInterpreter); + Assert.False(options.EnableFileSearch); + + // Act and Assert + ValidateSerialization(options); + } + + /// + /// Verify initialization. + /// + [Fact] + public void OpenAIAssistantInvocationOptionsAssignment() + { + // Arrange + OpenAIAssistantInvocationOptions options = + new() + { + ModelName = "testmodel", + Metadata = new Dictionary() { { "a", "1" } }, + MaxCompletionTokens = 1000, + MaxPromptTokens = 1000, + ParallelToolCallsEnabled = false, + TruncationMessageCount = 12, + Temperature = 2, + TopP = 0, + EnableCodeInterpreter = true, + EnableJsonResponse = true, + EnableFileSearch = true, + }; + + // Assert + Assert.Equal("testmodel", options.ModelName); + Assert.Equal(2, options.Temperature); + Assert.Equal(0, options.TopP); + Assert.Equal(1000, options.MaxCompletionTokens); + Assert.Equal(1000, options.MaxPromptTokens); + Assert.Equal(12, options.TruncationMessageCount); + Assert.False(options.ParallelToolCallsEnabled); + Assert.Single(options.Metadata); + Assert.True(options.EnableCodeInterpreter); + Assert.True(options.EnableJsonResponse); + Assert.True(options.EnableFileSearch); + + // Act and Assert + ValidateSerialization(options); + } + + private static void ValidateSerialization(OpenAIAssistantInvocationOptions source) + { + // Act + string json = JsonSerializer.Serialize(source); + + OpenAIAssistantInvocationOptions? target = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(target); + Assert.Equal(source.ModelName, target.ModelName); + Assert.Equal(source.Temperature, target.Temperature); + Assert.Equal(source.TopP, target.TopP); + Assert.Equal(source.MaxCompletionTokens, target.MaxCompletionTokens); + Assert.Equal(source.MaxPromptTokens, target.MaxPromptTokens); + Assert.Equal(source.TruncationMessageCount, target.TruncationMessageCount); + Assert.Equal(source.EnableCodeInterpreter, target.EnableCodeInterpreter); + Assert.Equal(source.EnableJsonResponse, target.EnableJsonResponse); + Assert.Equal(source.EnableFileSearch, target.EnableFileSearch); + AssertCollection.Equal(source.Metadata, target.Metadata); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs new file mode 100644 index 000000000000..7799eb26c305 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIClientProviderTests.cs @@ -0,0 +1,86 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Net.Http; +using Azure.Core; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Moq; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIClientProviderTests +{ + /// + /// Verify that provisioning of client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByKey() + { + // Arrange + OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI("key", new Uri("https://localhost")); + + // Assert + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for Azure OpenAI. + /// + [Fact] + public void VerifyOpenAIClientFactoryTargetAzureByCredential() + { + // Arrange + Mock mockCredential = new(); + OpenAIClientProvider provider = OpenAIClientProvider.ForAzureOpenAI(mockCredential.Object, new Uri("https://localhost")); + + // Assert + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for OpenAI. + /// + [Theory] + [InlineData(null)] + [InlineData("http://myproxy:9819")] + public void VerifyOpenAIClientFactoryTargetOpenAINoKey(string? endpoint) + { + // Arrange + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(endpoint != null ? new Uri(endpoint) : null); + + // Assert + Assert.NotNull(provider.Client); + } + + /// + /// Verify that provisioning of client for OpenAI. + /// + [Theory] + [InlineData("key", null)] + [InlineData("key", "http://myproxy:9819")] + public void VerifyOpenAIClientFactoryTargetOpenAIByKey(string key, string? endpoint) + { + // Arrange + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(key, endpoint != null ? new Uri(endpoint) : null); + + // Assert + Assert.NotNull(provider.Client); + } + + /// + /// Verify that the factory can create a client with http proxy. + /// + [Fact] + public void VerifyOpenAIClientFactoryWithHttpClient() + { + // Arrange + using HttpClient httpClient = new() { BaseAddress = new Uri("http://myproxy:9819") }; + OpenAIClientProvider provider = OpenAIClientProvider.ForOpenAI(httpClient: httpClient); + + // Assert + Assert.NotNull(provider.Client); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs new file mode 100644 index 000000000000..1689bec1f828 --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/OpenAIThreadCreationOptionsTests.cs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Text.Json; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class OpenAIThreadCreationOptionsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void OpenAIThreadCreationOptionsInitialState() + { + // Arrange + OpenAIThreadCreationOptions options = new(); + + // Assert + Assert.Null(options.Messages); + Assert.Null(options.Metadata); + Assert.Null(options.VectorStoreId); + Assert.Null(options.CodeInterpreterFileIds); + + // Act and Assert + ValidateSerialization(options); + } + + /// + /// Verify initialization. + /// + [Fact] + public void OpenAIThreadCreationOptionsAssignment() + { + // Arrange + OpenAIThreadCreationOptions options = + new() + { + Messages = [new ChatMessageContent(AuthorRole.User, "test")], + VectorStoreId = "#vs", + Metadata = new Dictionary() { { "a", "1" } }, + CodeInterpreterFileIds = ["file1"], + }; + + // Assert + Assert.Single(options.Messages); + Assert.Single(options.Metadata); + Assert.Equal("#vs", options.VectorStoreId); + Assert.Single(options.CodeInterpreterFileIds); + + // Act and Assert + ValidateSerialization(options); + } + + private static void ValidateSerialization(OpenAIThreadCreationOptions source) + { + // Act + string json = JsonSerializer.Serialize(source); + + OpenAIThreadCreationOptions? target = JsonSerializer.Deserialize(json); + + // Assert + Assert.NotNull(target); + Assert.Equal(source.VectorStoreId, target.VectorStoreId); + AssertCollection.Equal(source.CodeInterpreterFileIds, target.CodeInterpreterFileIds); + AssertCollection.Equal(source.Messages, target.Messages, m => m.Items.Count); // ChatMessageContent already validated for deep serialization + AssertCollection.Equal(source.Metadata, target.Metadata); + } +} diff --git a/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs new file mode 100644 index 000000000000..e75a962dfc5e --- /dev/null +++ b/dotnet/src/Agents/UnitTests/OpenAI/RunPollingOptionsTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Xunit; + +namespace SemanticKernel.Agents.UnitTests.OpenAI; + +/// +/// Unit testing of . +/// +public class RunPollingOptionsTests +{ + /// + /// Verify initial state. + /// + [Fact] + public void RunPollingOptionsInitialStateTest() + { + // Arrange + RunPollingOptions options = new(); + + // Assert + Assert.Equal(RunPollingOptions.DefaultPollingInterval, options.RunPollingInterval); + Assert.Equal(RunPollingOptions.DefaultPollingBackoff, options.RunPollingBackoff); + Assert.Equal(RunPollingOptions.DefaultMessageSynchronizationDelay, options.MessageSynchronizationDelay); + Assert.Equal(RunPollingOptions.DefaultPollingBackoffThreshold, options.RunPollingBackoffThreshold); + } + + /// s + /// Verify initialization. + /// + [Fact] + public void RunPollingOptionsAssignmentTest() + { + // Arrange + RunPollingOptions options = + new() + { + RunPollingInterval = TimeSpan.FromSeconds(3), + RunPollingBackoff = TimeSpan.FromSeconds(4), + RunPollingBackoffThreshold = 8, + MessageSynchronizationDelay = TimeSpan.FromSeconds(5), + }; + + // Assert + Assert.Equal(3, options.RunPollingInterval.TotalSeconds); + Assert.Equal(4, options.RunPollingBackoff.TotalSeconds); + Assert.Equal(5, options.MessageSynchronizationDelay.TotalSeconds); + Assert.Equal(8, options.RunPollingBackoffThreshold); + } + + /// s + /// Verify initialization. + /// + [Fact] + public void RunPollingOptionsGetIntervalTest() + { + // Arrange + RunPollingOptions options = + new() + { + RunPollingInterval = TimeSpan.FromSeconds(3), + RunPollingBackoff = TimeSpan.FromSeconds(4), + RunPollingBackoffThreshold = 8, + }; + + // Assert + Assert.Equal(options.RunPollingInterval, options.GetPollingInterval(8)); + Assert.Equal(options.RunPollingBackoff, options.GetPollingInterval(9)); + } +} diff --git a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs index a8d446dad360..c099f7d609e4 100644 --- a/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs +++ b/dotnet/src/Experimental/Agents/Extensions/OpenAIRestExtensions.cs @@ -4,7 +4,6 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.Connectors.OpenAI; using Microsoft.SemanticKernel.Experimental.Agents.Exceptions; using Microsoft.SemanticKernel.Experimental.Agents.Internal; using Microsoft.SemanticKernel.Http; @@ -92,7 +91,7 @@ private static void AddHeaders(this HttpRequestMessage request, OpenAIRestContex { request.Headers.Add(HeaderNameOpenAIAssistant, HeaderOpenAIValueAssistant); request.Headers.Add(HeaderNameUserAgent, HttpHeaderConstant.Values.UserAgent); - request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(OpenAIChatCompletionService))); + request.Headers.Add(HttpHeaderConstant.Names.SemanticKernelVersion, HttpHeaderConstant.Values.GetAssemblyVersion(typeof(IAgent))); if (context.HasVersion) { diff --git a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs index 1928f219c903..218ef3e3ddfc 100644 --- a/dotnet/src/Experimental/Agents/Internal/ChatRun.cs +++ b/dotnet/src/Experimental/Agents/Internal/ChatRun.cs @@ -163,13 +163,12 @@ private IEnumerable> ExecuteStep(ThreadRunStepModel step, private async Task ProcessFunctionStepAsync(string callId, ThreadRunStepModel.FunctionDetailsModel functionDetails, CancellationToken cancellationToken) { var result = await InvokeFunctionCallAsync().ConfigureAwait(false); - var toolResult = result as string ?? JsonSerializer.Serialize(result); return new ToolResultModel { CallId = callId, - Output = toolResult!, + Output = ParseFunctionResult(result), }; async Task InvokeFunctionCallAsync() @@ -191,4 +190,19 @@ async Task InvokeFunctionCallAsync() return result.GetValue() ?? string.Empty; } } + + private static string ParseFunctionResult(object result) + { + if (result is string stringResult) + { + return stringResult; + } + + if (result is ChatMessageContent messageResult) + { + return messageResult.Content ?? JsonSerializer.Serialize(messageResult); + } + + return JsonSerializer.Serialize(result); + } } diff --git a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs index 4fd99b717b5e..3e3625050551 100644 --- a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -5,20 +5,19 @@ using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -using Xunit.Abstractions; -namespace SemanticKernel.IntegrationTests.Agents.OpenAI; +namespace SemanticKernel.IntegrationTests.Agents; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class ChatCompletionAgentTests(ITestOutputHelper output) : IDisposable +public sealed class ChatCompletionAgentTests() { private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() @@ -42,8 +41,6 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns KernelPlugin plugin = KernelPluginFactory.CreateFromType(); - this._kernelBuilder.Services.AddSingleton(this._logger); - this._kernelBuilder.AddAzureOpenAIChatCompletion( configuration.ChatDeploymentName!, configuration.Endpoint, @@ -94,15 +91,6 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns Assert.Contains(expectedAnswerContains, messages.Single().Content, StringComparison.OrdinalIgnoreCase); } - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index 20d6dcad9146..0dc1ae952c20 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -4,23 +4,19 @@ using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; -using Xunit.Abstractions; -namespace SemanticKernel.IntegrationTests.Agents.OpenAI; +namespace SemanticKernel.IntegrationTests.Agents; #pragma warning disable xUnit1004 // Contains test methods used in manual verification. Disable warning for this file only. -public sealed class OpenAIAssistantAgentTests(ITestOutputHelper output) : IDisposable +public sealed class OpenAIAssistantAgentTests { - private readonly IKernelBuilder _kernelBuilder = Kernel.CreateBuilder(); private readonly IConfigurationRoot _configuration = new ConfigurationBuilder() .AddJsonFile(path: "testsettings.json", optional: false, reloadOnChange: true) .AddJsonFile(path: "testsettings.development.json", optional: true, reloadOnChange: true) @@ -36,12 +32,12 @@ public sealed class OpenAIAssistantAgentTests(ITestOutputHelper output) : IDispo [InlineData("What is the special soup?", "Clam Chowder")] public async Task OpenAIAssistantAgentTestAsync(string input, string expectedAnswerContains) { - var openAIConfiguration = this._configuration.GetSection("OpenAI").Get(); - Assert.NotNull(openAIConfiguration); + OpenAIConfiguration openAISettings = this._configuration.GetSection("OpenAI").Get()!; + Assert.NotNull(openAISettings); await this.ExecuteAgentAsync( - new(openAIConfiguration.ApiKey), - openAIConfiguration.ModelId, + OpenAIClientProvider.ForOpenAI(openAISettings.ApiKey), + openAISettings.ModelId, input, expectedAnswerContains); } @@ -50,7 +46,7 @@ await this.ExecuteAgentAsync( /// Integration test for using function calling /// and targeting Azure OpenAI services. /// - [Theory(Skip = "No supported endpoint configured.")] + [Theory/*(Skip = "No supported endpoint configured.")*/] [InlineData("What is the special soup?", "Clam Chowder")] public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAnswerContains) { @@ -58,22 +54,20 @@ public async Task AzureOpenAIAssistantAgentAsync(string input, string expectedAn Assert.NotNull(azureOpenAIConfiguration); await this.ExecuteAgentAsync( - new(azureOpenAIConfiguration.ApiKey, azureOpenAIConfiguration.Endpoint), + OpenAIClientProvider.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), azureOpenAIConfiguration.ChatDeploymentName!, input, expectedAnswerContains); } private async Task ExecuteAgentAsync( - OpenAIAssistantConfiguration config, + OpenAIClientProvider config, string modelName, string input, string expected) { // Arrange - this._kernelBuilder.Services.AddSingleton(this._logger); - - Kernel kernel = this._kernelBuilder.Build(); + Kernel kernel = new(); KernelPlugin plugin = KernelPluginFactory.CreateFromType(); kernel.Plugins.Add(plugin); @@ -82,10 +76,9 @@ private async Task ExecuteAgentAsync( await OpenAIAssistantAgent.CreateAsync( kernel, config, - new() + new(modelName) { Instructions = "Answer questions about the menu.", - ModelId = modelName, }); AgentGroupChat chat = new(); @@ -102,15 +95,6 @@ await OpenAIAssistantAgent.CreateAsync( Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase); } - private readonly XunitLogger _logger = new(output); - private readonly RedirectOutput _testOutputHelper = new(output); - - public void Dispose() - { - this._logger.Dispose(); - this._testOutputHelper.Dispose(); - } - public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] diff --git a/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs new file mode 100644 index 000000000000..e86c1b77f4c1 --- /dev/null +++ b/dotnet/src/InternalUtilities/samples/AgentUtilities/BaseAgentsTest.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.ObjectModel; +using System.Diagnostics; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using OpenAI.Files; + +/// +/// Base class for samples that demonstrate the usage of agents. +/// +public abstract class BaseAgentsTest(ITestOutputHelper output) : BaseTest(output) +{ + /// + /// Metadata key to indicate the assistant as created for a sample. + /// + protected const string AssistantSampleMetadataKey = "sksample"; + + /// + /// Metadata to indicate the assistant as created for a sample. + /// + /// + /// While the samples do attempt delete the assistants it creates, it is possible + /// that some assistants may remain. This metadata can be used to identify and sample + /// agents for clean-up. + /// + protected static readonly ReadOnlyDictionary AssistantSampleMetadata = + new(new Dictionary + { + { AssistantSampleMetadataKey, bool.TrueString } + }); + + /// + /// Provide a according to the configuration settings. + /// + protected OpenAIClientProvider GetClientProvider() + => + this.UseOpenAIConfig ? + OpenAIClientProvider.ForOpenAI(this.ApiKey) : + OpenAIClientProvider.ForAzureOpenAI(this.ApiKey, new Uri(this.Endpoint!)); + + /// + /// Common method to write formatted agent chat content to the console. + /// + protected void WriteAgentChatMessage(ChatMessageContent message) + { + // Include ChatMessageContent.AuthorName in output, if present. + string authorExpression = message.Role == AuthorRole.User ? string.Empty : $" - {message.AuthorName ?? "*"}"; + // Include TextContent (via ChatMessageContent.Content), if present. + string contentExpression = string.IsNullOrWhiteSpace(message.Content) ? string.Empty : message.Content; + bool isCode = message.Metadata?.ContainsKey(OpenAIAssistantAgent.CodeInterpreterMetadataKey) ?? false; + string codeMarker = isCode ? "\n [CODE]\n" : " "; + Console.WriteLine($"\n# {message.Role}{authorExpression}:{codeMarker}{contentExpression}"); + + // Provide visibility for inner content (that isn't TextContent). + foreach (KernelContent item in message.Items) + { + if (item is AnnotationContent annotation) + { + Console.WriteLine($" [{item.GetType().Name}] {annotation.Quote}: File #{annotation.FileId}"); + } + else if (item is FileReferenceContent fileReference) + { + Console.WriteLine($" [{item.GetType().Name}] File #{fileReference.FileId}"); + } + else if (item is ImageContent image) + { + Console.WriteLine($" [{item.GetType().Name}] {image.Uri?.ToString() ?? image.DataUri ?? $"{image.Data?.Length} bytes"}"); + } + else if (item is FunctionCallContent functionCall) + { + Console.WriteLine($" [{item.GetType().Name}] {functionCall.Id}"); + } + else if (item is FunctionResultContent functionResult) + { + Console.WriteLine($" [{item.GetType().Name}] {functionResult.CallId}"); + } + } + } + + protected async Task DownloadResponseContentAsync(FileClient client, ChatMessageContent message) + { + foreach (KernelContent item in message.Items) + { + if (item is AnnotationContent annotation) + { + await this.DownloadFileContentAsync(client, annotation.FileId!); + } + } + } + + protected async Task DownloadResponseImageAsync(FileClient client, ChatMessageContent message) + { + foreach (KernelContent item in message.Items) + { + if (item is FileReferenceContent fileReference) + { + await this.DownloadFileContentAsync(client, fileReference.FileId, launchViewer: true); + } + } + } + + private async Task DownloadFileContentAsync(FileClient client, string fileId, bool launchViewer = false) + { + OpenAIFileInfo fileInfo = client.GetFile(fileId); + if (fileInfo.Purpose == OpenAIFilePurpose.AssistantsOutput) + { + string filePath = Path.Combine(Path.GetTempPath(), Path.GetFileName(fileInfo.Filename)); + if (launchViewer) + { + filePath = Path.ChangeExtension(filePath, ".png"); + } + + BinaryData content = await client.DownloadFileAsync(fileId); + File.WriteAllBytes(filePath, content.ToArray()); + Console.WriteLine($" File #{fileId} saved to: {filePath}"); + + if (launchViewer) + { + Process.Start( + new ProcessStartInfo + { + FileName = "cmd.exe", + Arguments = $"/C start {filePath}" + }); + } + } + } +} diff --git a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props index 0c47e16d8d93..df5205c40a82 100644 --- a/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props +++ b/dotnet/src/InternalUtilities/samples/SamplesInternalUtilities.props @@ -1,5 +1,8 @@ - + + \ No newline at end of file diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs index f9e6f9f3d71f..fd27b35a4b0f 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs @@ -44,7 +44,7 @@ public AnnotationContent() /// Initializes a new instance of the class. /// /// The model ID used to generate the content. - /// Inner content, + /// Inner content /// Additional metadata public AnnotationContent( string? modelId = null, diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs index 16ac0cd7828e..925d74d0c731 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/FileReferenceContent.cs @@ -28,7 +28,7 @@ public FileReferenceContent() /// /// The identifier of the referenced file. /// The model ID used to generate the content. - /// Inner content, + /// Inner content /// Additional metadata public FileReferenceContent( string fileId, From 45169b985febfc6178976cf1b397cdd5e84e0747 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Tue, 13 Aug 2024 22:32:13 +0100 Subject: [PATCH 191/226] .Net: AzureOpenAI - Enable package validation (#8097) ### Motivation and Context - Resolves #7558 --- dotnet/nuget/nuget-package.props | 2 +- .../Connectors.AzureOpenAI.csproj | 2 +- .../CompatibilitySuppressions.xml | 1222 ----------------- 3 files changed, 2 insertions(+), 1224 deletions(-) delete mode 100644 dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml diff --git a/dotnet/nuget/nuget-package.props b/dotnet/nuget/nuget-package.props index b517a0f7becf..107443170bc2 100644 --- a/dotnet/nuget/nuget-package.props +++ b/dotnet/nuget/nuget-package.props @@ -10,7 +10,7 @@ true - 1.17.1 + 1.18.0-rc $(NoWarn);CP0003 diff --git a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj index 6c0d24c9ce12..15d88496159b 100644 --- a/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj +++ b/dotnet/src/Connectors/Connectors.AzureOpenAI/Connectors.AzureOpenAI.csproj @@ -7,7 +7,7 @@ net8.0;netstandard2.0 true $(NoWarn);NU5104;SKEXP0001,SKEXP0010 - false + true diff --git a/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml b/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml deleted file mode 100644 index 05e86e2b3f75..000000000000 --- a/dotnet/src/Connectors/Connectors.OpenAI/CompatibilitySuppressions.xml +++ /dev/null @@ -1,1222 +0,0 @@ - - - - - CP0001 - T:Microsoft.SemanticKernel.ChatHistoryExtensions - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIAudioToTextService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextEmbeddingGenerationService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextGenerationService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToAudioService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToImageService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataChatMessageContent - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataStreamingChatMessageContent - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingTextContent - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextGenerationService - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.ChatHistoryExtensions - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIAudioToTextService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextEmbeddingGenerationService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextGenerationService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToAudioService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAITextToImageService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataChatMessageContent - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIWithDataStreamingChatMessageContent - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingTextContent - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0001 - T:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextGenerationService - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings.get_Temperature - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatMessageContent.get_ToolCalls - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFunction.ToFunctionDefinition - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPluginCollectionExtensions.TryGetFunctionAndArguments(Microsoft.SemanticKernel.IReadOnlyKernelPluginCollection,Azure.AI.OpenAI.ChatCompletionsFunctionToolCall,Microsoft.SemanticKernel.KernelFunction@,Microsoft.SemanticKernel.KernelArguments@) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(Microsoft.SemanticKernel.PromptExecutionSettings,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_AzureChatExtensionsOptions - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_FrequencyPenalty - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_PresencePenalty - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_ResultsPerPrompt - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_Temperature - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_TopP - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_AzureChatExtensionsOptions(Azure.AI.OpenAI.AzureChatExtensionsOptions) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_ResultsPerPrompt(System.Int32) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_FinishReason - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_ToolCallUpdate - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextEmbeddingGenerationService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToAudioExecutionSettings.get_Speed - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Int32) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.Uri,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/net8.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextExecutionSettings.get_Temperature - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIAudioToTextService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatCompletionService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIChatMessageContent.get_ToolCalls - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIFunction.ToFunctionDefinition - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIMemoryBuilderExtensions.WithAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.Memory.MemoryBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPluginCollectionExtensions.TryGetFunctionAndArguments(Microsoft.SemanticKernel.IReadOnlyKernelPluginCollection,Azure.AI.OpenAI.ChatCompletionsFunctionToolCall,Microsoft.SemanticKernel.KernelFunction@,Microsoft.SemanticKernel.KernelArguments@) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.FromExecutionSettingsWithData(Microsoft.SemanticKernel.PromptExecutionSettings,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_AzureChatExtensionsOptions - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_FrequencyPenalty - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_PresencePenalty - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_ResultsPerPrompt - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_Temperature - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.get_TopP - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_AzureChatExtensionsOptions(Azure.AI.OpenAI.AzureChatExtensionsOptions) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIPromptExecutionSettings.set_ResultsPerPrompt(System.Int32) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_FinishReason - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAIStreamingChatMessageContent.get_ToolCallUpdate - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextEmbeddingGenerationService.#ctor(System.String,Azure.AI.OpenAI.OpenAIClient,Microsoft.Extensions.Logging.ILoggerFactory,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.Connectors.OpenAI.OpenAITextToAudioExecutionSettings.get_Speed - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,Microsoft.SemanticKernel.Connectors.OpenAI.AzureOpenAIChatCompletionWithDataConfig,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String,System.String,System.Int32) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,Azure.Core.TokenCredential,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddAzureOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIAudioToText(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIChatCompletion(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.Uri,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAIFiles(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextEmbeddingGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient,System.Nullable{System.Int32}) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.Extensions.DependencyInjection.IServiceCollection,System.String,System.String,System.String,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,Azure.AI.OpenAI.OpenAIClient,System.String) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextGeneration(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToAudio(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - - CP0002 - M:Microsoft.SemanticKernel.OpenAIServiceCollectionExtensions.AddOpenAITextToImage(Microsoft.SemanticKernel.IKernelBuilder,System.String,System.String,System.String,System.String,System.Net.Http.HttpClient) - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - lib/netstandard2.0/Microsoft.SemanticKernel.Connectors.OpenAI.dll - true - - \ No newline at end of file From e4ad4e7c40ea658fc54409d08343aeec6e4fab28 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:11:49 -0700 Subject: [PATCH 192/226] Feature Complete --- .../Agents/ChatCompletion_Streaming.cs | 21 +-- .../Agents/OpenAIAssistant_Streaming.cs | 125 ++++++++++++++++ .../Step06_DependencyInjection.cs | 2 +- .../src/Agents/Abstractions/AgentChannel.cs | 7 +- dotnet/src/Agents/Abstractions/AgentChat.cs | 5 +- .../Agents/Abstractions/AggregatorChannel.cs | 26 +++- .../Logging/AgentChatLogMessages.cs | 25 ++++ dotnet/src/Agents/Core/AgentGroupChat.cs | 135 +++++++++++------- dotnet/src/Agents/Core/ChatCompletionAgent.cs | 9 ++ dotnet/src/Agents/Core/ChatHistoryChannel.cs | 4 +- .../OpenAI/Internal/AssistantThreadActions.cs | 58 +++++--- .../Agents/OpenAI/OpenAIAssistantChannel.cs | 3 +- dotnet/src/Agents/UnitTests/AgentChatTests.cs | 4 +- dotnet/src/Agents/UnitTests/MockChannel.cs | 3 +- .../OpenAIChatCompletion_NonStreamingTests.cs | 1 + .../Contents/AnnotationContent.cs | 9 +- .../Contents/StreamingAnnotationContent.cs | 79 ++++++++++ .../Contents/StreamingFileReferenceContent.cs | 55 +++++++ .../Contents/AnnotationContentTests.cs | 2 +- .../Contents/ChatMessageContentTests.cs | 2 +- .../StreamingAnnotationContentTests.cs | 71 +++++++++ .../StreamingFileReferenceContentTests.cs | 37 +++++ 22 files changed, 587 insertions(+), 96 deletions(-) create mode 100644 dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs create mode 100644 dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs create mode 100644 dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 575db7f7f288..334b7f53496b 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; -using System.Text; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; @@ -70,7 +69,9 @@ private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, chat.Add(message); this.WriteAgentChatMessage(message); - StringBuilder builder = new(); + int historyCount = chat.Count; + + bool isFirst = false; await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat)) { if (string.IsNullOrEmpty(response.Content)) @@ -78,21 +79,21 @@ private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, continue; } - if (builder.Length == 0) + if (!isFirst) { - Console.WriteLine($"# {response.Role} - {response.AuthorName ?? "*"}:"); + Console.WriteLine($"\n# {response.Role} - {response.AuthorName ?? "*"}:"); + isFirst = true; } Console.WriteLine($"\t > streamed: '{response.Content}'"); - builder.Append(response.Content); } - if (builder.Length > 0) + if (historyCount <= chat.Count) { - // Display full response and capture in chat history - ChatMessageContent response = new(AuthorRole.Assistant, builder.ToString()) { AuthorName = agent.Name }; - chat.Add(response); - this.WriteAgentChatMessage(response); + for (int index = historyCount; index < chat.Count; index++) + { + this.WriteAgentChatMessage(chat[index]); + } } } diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs new file mode 100644 index 000000000000..72a28d6dded8 --- /dev/null +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; + +namespace Agents; + +/// +/// Demonstrate creation of and +/// eliciting its response to three explicit user messages. +/// +public class OpenAIAssistant_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) +{ + private const string ParrotName = "Parrot"; + private const string ParrotInstructions = "Repeat the user message in the voice of a pirate and then end with a parrot sound."; + + [Fact] + public async Task UseStreamingChatCompletionAgentAsync() + { + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + clientProvider: this.GetClientProvider(), + new(this.Model) + { + Instructions = ParrotInstructions, + Name = ParrotName, + Metadata = AssistantSampleMetadata, + }); + + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); + + // Respond to user input + await InvokeAgentAsync(agent, threadId, "Fortune favors the bold."); + await InvokeAgentAsync(agent, threadId, "I came, I saw, I conquered."); + await InvokeAgentAsync(agent, threadId, "Practice makes perfect."); + } + + [Fact] + public async Task UseStreamingChatCompletionAgentWithPluginAsync() + { + const string MenuInstructions = "Answer questions about the menu."; + + // Define the agent + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + clientProvider: this.GetClientProvider(), + new(this.Model) + { + Instructions = MenuInstructions, + Name = "Host", + Metadata = AssistantSampleMetadata, + }); + + // Initialize plugin and add to the agent's Kernel (same as direct Kernel usage). + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + agent.Kernel.Plugins.Add(plugin); + + // Create a thread for the agent conversation. + string threadId = await agent.CreateThreadAsync(new OpenAIThreadCreationOptions { Metadata = AssistantSampleMetadata }); + + // Respond to user input + await InvokeAgentAsync(agent, threadId, "What is the special soup?"); + await InvokeAgentAsync(agent, threadId, "What is the special drink?"); + } + + // Local function to invoke agent and display the conversation messages. + private async Task InvokeAgentAsync(OpenAIAssistantAgent agent, string threadId, string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + await agent.AddChatMessageAsync(threadId, message); + this.WriteAgentChatMessage(message); + + ChatHistory history = []; + + bool isFirst = false; + await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(threadId, history)) + { + if (string.IsNullOrEmpty(response.Content)) + { + continue; + } + + if (!isFirst) + { + Console.WriteLine($"\n# {response.Role} - {response.AuthorName ?? "*"}:"); + isFirst = true; + } + + Console.WriteLine($"\t > streamed: '{response.Content}'"); + } + + foreach (ChatMessageContent content in history) + { + this.WriteAgentChatMessage(content); + } + } + + public sealed class MenuPlugin + { + [KernelFunction, Description("Provides a list of specials from the menu.")] + [System.Diagnostics.CodeAnalysis.SuppressMessage("Design", "CA1024:Use properties where appropriate", Justification = "Too smart")] + public string GetSpecials() + { + return @" +Special Soup: Clam Chowder +Special Salad: Cobb Salad +Special Drink: Chai Tea +"; + } + + [KernelFunction, Description("Provides the price of the requested menu item.")] + public string GetItemPrice( + [Description("The name of the menu item.")] + string menuItem) + { + return "$9.99"; + } + } +} diff --git a/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs index a0d32f8cefba..ca8089a9130f 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step06_DependencyInjection.cs @@ -69,7 +69,7 @@ public async Task UseDependencyInjectionToCreateAgentAsync() AgentClient agentClient = serviceProvider.GetRequiredService(); // Execute the agent-client - await WriteAgentResponse("The sunset is very colorful."); + await WriteAgentResponse("The sunset is nice."); await WriteAgentResponse("The sunset is setting over the mountains."); await WriteAgentResponse("The sunset is setting over the mountains and filled the sky with a deep red flame, setting the clouds ablaze."); diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index 721b1bd11231..cb7ed0b05012 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents; @@ -49,7 +48,7 @@ public abstract class AgentChannel /// Asynchronous enumeration of streaming messages. protected internal abstract IAsyncEnumerable InvokeStreamingAsync( Agent agent, - ChatHistory messages, // %%% IList ??? + IList messages, CancellationToken cancellationToken = default); /// @@ -109,13 +108,13 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent /// protected internal abstract IAsyncEnumerable InvokeStreamingAsync( TAgent agent, - ChatHistory messages, + IList messages, CancellationToken cancellationToken = default); /// protected internal override IAsyncEnumerable InvokeStreamingAsync( Agent agent, - ChatHistory messages, + IList messages, CancellationToken cancellationToken = default) { if (agent.GetType() != typeof(TAgent)) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 78a8d6470920..5bf4853a9d91 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -278,10 +278,11 @@ protected async IAsyncEnumerable InvokeStreamingAge await foreach (StreamingChatMessageContent streamingContent in channel.InvokeStreamingAsync(agent, messages, cancellationToken).ConfigureAwait(false)) { - //this.Logger.LogAgentChatInvokedAgentMessage(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, message); // %%% LOGGING yield return streamingContent; } + this.Logger.LogAgentChatInvokedStreamingAgentMessages(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, messages); + // Broadcast message to other channels (in parallel) // Note: Able to queue messages without synchronizing channels. var channelRefs = @@ -317,7 +318,7 @@ private void ClearActivitySignal() /// The activity signal is used to manage ability and visibility for taking actions based /// on conversation history. /// - protected void SetActivityOrThrow() + internal void SetActivityOrThrow() { // Note: Interlocked is the absolute lightest synchronization mechanism available in dotnet. int wasActive = Interlocked.CompareExchange(ref this._isActive, 1, 0); diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 7b7662416124..27fea5b550bb 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; +using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; @@ -51,9 +52,30 @@ protected internal override IAsyncEnumerable GetHistoryAsync } /// - protected internal override IAsyncEnumerable InvokeStreamingAsync(AggregatorAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + protected internal override async IAsyncEnumerable InvokeStreamingAsync(AggregatorAgent agent, IList messages, [EnumeratorCancellation] CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); // %%% TODO + int messageCount = await this._chat.GetChatMessagesAsync(cancellationToken).CountAsync(cancellationToken).ConfigureAwait(false); + + await foreach (StreamingChatMessageContent message in this._chat.InvokeStreamingAsync(cancellationToken).ConfigureAwait(false)) // %%% NOISY / NEEDED ??? + { + yield return message; + } + + ChatMessageContent[] history = await this._chat.GetChatMessagesAsync(cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); + if (history.Length > messageCount) + { + if (agent.Mode == AggregatorMode.Flat) + { + for (int index = messageCount; index < messages.Count; ++index) + { + messages.Add(history[index]); + } + } + else if (agent.Mode == AggregatorMode.Nested) + { + messages.Add(history[history.Length - 1]); + } + } } /// diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs index b971fe2ce8d4..6417a7cac10e 100644 --- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs +++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Logging; @@ -94,6 +95,30 @@ public static partial void LogAgentChatInvokedAgentMessage( string agentId, ChatMessageContent message); + /// + /// Logs retrieval of streamed messages. + /// + private static readonly Action s_logAgentChatInvokedStreamingAgentMessages = + LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 0, + "[{MethodName}] Agent message {AgentType}/{AgentId}: {Message}."); + public static void LogAgentChatInvokedStreamingAgentMessages( + this ILogger logger, + string methodName, + Type agentType, + string agentId, + IList messages) + { + if (logger.IsEnabled(LogLevel.Debug)) + { + foreach (ChatMessageContent message in messages) + { + s_logAgentChatInvokedStreamingAgentMessages(logger, methodName, agentType, agentId, message, null); + } + } + } + /// /// Logs invoked agent (complete). /// diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs index 33ca3f25ad0b..addf1f6e18d0 100644 --- a/dotnet/src/Agents/Core/AgentGroupChat.cs +++ b/dotnet/src/Agents/Core/AgentGroupChat.cs @@ -8,7 +8,6 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.SemanticKernel.Agents.Chat; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents; @@ -60,47 +59,54 @@ public void AddAgent(Agent agent) public override async IAsyncEnumerable InvokeAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) { this.EnsureStrategyLoggerAssignment(); - - if (this.IsComplete) - { - // Throw exception if chat is completed and automatic-reset is not enabled. - if (!this.ExecutionSettings.TerminationStrategy.AutomaticReset) - { - throw new KernelException("Agent Failure - Chat has completed."); - } - - this.IsComplete = false; - } + this.EnsureCompletionStatus(); this.Logger.LogAgentGroupChatInvokingAgents(nameof(InvokeAsync), this.Agents); for (int index = 0; index < this.ExecutionSettings.TerminationStrategy.MaximumIterations; index++) { // Identify next agent using strategy - this.Logger.LogAgentGroupChatSelectingAgent(nameof(InvokeAsync), this.ExecutionSettings.SelectionStrategy.GetType()); + Agent agent = await this.SelectAgentAsync(cancellationToken).ConfigureAwait(false); - Agent agent; - try + // Invoke agent and process messages along with termination + await foreach (var message in this.InvokeAsync(agent, cancellationToken).ConfigureAwait(false)) { - agent = await this.ExecutionSettings.SelectionStrategy.NextAsync(this.Agents, this.History, cancellationToken).ConfigureAwait(false); + yield return message; } - catch (Exception exception) + + if (this.IsComplete) { - this.Logger.LogAgentGroupChatNoAgentSelected(nameof(InvokeAsync), exception); - throw; + break; } + } - this.Logger.LogAgentGroupChatSelectedAgent(nameof(InvokeAsync), agent.GetType(), agent.Id, this.ExecutionSettings.SelectionStrategy.GetType()); + this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); + } + + /// + /// Process a series of interactions between the that have joined this . + /// The interactions will proceed according to the and the + /// defined via . + /// In the absence of an , this method will not invoke any agents. + /// Any agent may be explicitly selected by calling . + /// + /// The to monitor for cancellation requests. The default is . + /// Asynchronous enumeration of streaming messages. + public override async IAsyncEnumerable InvokeStreamingAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) + { + this.EnsureStrategyLoggerAssignment(); + this.EnsureCompletionStatus(); + + this.Logger.LogAgentGroupChatInvokingAgents(nameof(InvokeAsync), this.Agents); + + for (int index = 0; index < this.ExecutionSettings.TerminationStrategy.MaximumIterations; index++) + { + // Identify next agent using strategy + Agent agent = await this.SelectAgentAsync(cancellationToken).ConfigureAwait(false); // Invoke agent and process messages along with termination - await foreach (var message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false)) + await foreach (var message in this.InvokeStreamingAsync(agent, cancellationToken).ConfigureAwait(false)) { - if (message.Role == AuthorRole.Assistant) - { - var task = this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken); - this.IsComplete = await task.ConfigureAwait(false); - } - yield return message; } @@ -113,17 +119,6 @@ public override async IAsyncEnumerable InvokeAsync([Enumerat this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } - /// - /// %%% - /// - /// - /// - /// - public override IAsyncEnumerable InvokeStreamingAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); // %%% TODO - } - /// /// Process a single interaction between a given an a . /// @@ -141,19 +136,15 @@ public async IAsyncEnumerable InvokeAsync( this.Logger.LogAgentGroupChatInvokingAgent(nameof(InvokeAsync), agent.GetType(), agent.Id); - this.AddAgent(agent); // %%% RECTIFY WITH SERIALIATION + this.AddAgent(agent); await foreach (ChatMessageContent message in base.InvokeAgentAsync(agent, cancellationToken).ConfigureAwait(false)) { - if (message.Role == AuthorRole.Assistant) - { - var task = this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken); - this.IsComplete = await task.ConfigureAwait(false); - } - yield return message; } + this.IsComplete = await this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken).ConfigureAwait(false); + this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } @@ -166,11 +157,25 @@ public async IAsyncEnumerable InvokeAsync( /// /// Specified agent joins the chat. /// > - public IAsyncEnumerable InvokeStreamingAsync( + public async IAsyncEnumerable InvokeStreamingAsync( Agent agent, - CancellationToken cancellationToken = default) + [EnumeratorCancellation] CancellationToken cancellationToken = default) { - throw new NotImplementedException(); // %%% TODO + this.EnsureStrategyLoggerAssignment(); + + this.Logger.LogAgentGroupChatInvokingAgent(nameof(InvokeAsync), agent.GetType(), agent.Id); + + this.AddAgent(agent); + + await foreach (StreamingChatMessageContent message in base.InvokeStreamingAgentAsync(agent, cancellationToken).ConfigureAwait(false)) + { + yield return message; + } + + Task task = this.ExecutionSettings.TerminationStrategy.ShouldTerminateAsync(agent, this.History, cancellationToken); + this.IsComplete = await task.ConfigureAwait(false); + + this.Logger.LogAgentGroupChatYield(nameof(InvokeAsync), this.IsComplete); } /// @@ -196,4 +201,38 @@ private void EnsureStrategyLoggerAssignment() this.ExecutionSettings.TerminationStrategy.Logger = this.LoggerFactory.CreateLogger(this.ExecutionSettings.TerminationStrategy.GetType()); } } + + private void EnsureCompletionStatus() + { + if (this.IsComplete) + { + // Throw exception if chat is completed and automatic-reset is not enabled. + if (!this.ExecutionSettings.TerminationStrategy.AutomaticReset) + { + throw new KernelException("Agent Failure - Chat has completed."); + } + + this.IsComplete = false; + } + } + + private async Task SelectAgentAsync(CancellationToken cancellationToken) + { + this.Logger.LogAgentGroupChatSelectingAgent(nameof(InvokeAsync), this.ExecutionSettings.SelectionStrategy.GetType()); + + Agent agent; + try + { + agent = await this.ExecutionSettings.SelectionStrategy.NextAsync(this.Agents, this.History, cancellationToken).ConfigureAwait(false); + } + catch (Exception exception) + { + this.Logger.LogAgentGroupChatNoAgentSelected(nameof(InvokeAsync), exception); + throw; + } + + this.Logger.LogAgentGroupChatSelectedAgent(nameof(InvokeAsync), agent.GetType(), agent.Id, this.ExecutionSettings.SelectionStrategy.GetType()); + + return agent; + } } diff --git a/dotnet/src/Agents/Core/ChatCompletionAgent.cs b/dotnet/src/Agents/Core/ChatCompletionAgent.cs index 91f5b864e725..424f28a6c6dc 100644 --- a/dotnet/src/Agents/Core/ChatCompletionAgent.cs +++ b/dotnet/src/Agents/Core/ChatCompletionAgent.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Runtime.CompilerServices; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -100,13 +101,21 @@ public async IAsyncEnumerable InvokeStreamingAsync( this.Logger.LogAgentChatServiceInvokedStreamingAgent(nameof(InvokeAsync), this.Id, chatCompletionService.GetType()); + AuthorRole? role = null; + StringBuilder builder = new(); await foreach (StreamingChatMessageContent message in messages.ConfigureAwait(false)) { + role ??= message.Role; + message.Role ??= AuthorRole.Assistant; message.AuthorName = this.Name; + builder.Append(message.ToString()); + yield return message; } + chat.Add(new(role ?? AuthorRole.Assistant, builder.ToString()) { AuthorName = this.Name }); + // Capture mutated messages related function calling / tools for (int messageIndex = messageCount; messageIndex < chat.Count; messageIndex++) { diff --git a/dotnet/src/Agents/Core/ChatHistoryChannel.cs b/dotnet/src/Agents/Core/ChatHistoryChannel.cs index bbba6f89daef..97e6a88d16df 100644 --- a/dotnet/src/Agents/Core/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Core/ChatHistoryChannel.cs @@ -78,7 +78,7 @@ bool IsMessageVisible(ChatMessageContent message) => } /// - protected override async IAsyncEnumerable InvokeStreamingAsync(Agent agent, ChatHistory messages, [EnumeratorCancellation] CancellationToken cancellationToken = default) + protected override async IAsyncEnumerable InvokeStreamingAsync(Agent agent, IList messages, [EnumeratorCancellation] CancellationToken cancellationToken = default) { if (agent is not IChatHistoryHandler historyHandler) { @@ -92,8 +92,6 @@ protected override async IAsyncEnumerable InvokeStr { yield return streamingMessage; } - - // %%% VERIFY this._history } /// diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 8e58a61a2e2c..f66db9cb279f 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.CompilerServices; @@ -10,6 +11,8 @@ using Azure; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; + +//using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; @@ -354,7 +357,7 @@ public static async IAsyncEnumerable InvokeStreamin OpenAIAssistantAgent agent, AssistantClient client, string threadId, - ChatHistory messages, + IList messages, OpenAIAssistantInvocationOptions? invocationOptions, ILogger logger, Kernel kernel, @@ -389,13 +392,16 @@ public static async IAsyncEnumerable InvokeStreamin functionResultTasks.Clear(); messageIds.Clear(); - await foreach (StreamingUpdate update in asyncUpdates.ConfigureAwait(false)) + if (run != null) { - //logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); // %%% LOGGING + Debugger.Break(); + } + await foreach (StreamingUpdate update in asyncUpdates.ConfigureAwait(false)) + { if (update is RunUpdate runUpdate) { - run = runUpdate; + run = runUpdate.Value; logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId); } @@ -413,8 +419,6 @@ public static async IAsyncEnumerable InvokeStreamin messageIds.Add(contentUpdate.MessageId); yield return GenerateStreamingMessageContent(agent.GetName(), contentUpdate); } - - //logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), activeFunctionSteps.Length, run.Id, threadId); // %%% LOGGING } if (run != null) @@ -442,6 +446,8 @@ public static async IAsyncEnumerable InvokeStreamin if (messageIds.Count > 0) { + logger.LogOpenAIAssistantProcessingRunMessages(nameof(InvokeAsync), run!.Id, threadId); + foreach (string messageId in messageIds) { ThreadMessage? message = await RetrieveMessageAsync(client, threadId, messageId, agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); @@ -452,14 +458,13 @@ public static async IAsyncEnumerable InvokeStreamin messages.Add(content); } } - } - //logger.LogOpenAIAssistantProcessingRunMessages(nameof(InvokeAsync), run.Id, threadId); // %%% LOGGING - //logger.LogOpenAIAssistantProcessedRunMessages(nameof(InvokeAsync), messageCount, run.Id, threadId); // %%% LOGGING + logger.LogOpenAIAssistantProcessedRunMessages(nameof(InvokeAsync), messageIds.Count, run!.Id, threadId); + } } - while (run?.Status.IsTerminal == false); + while (run?.Status != RunStatus.Completed); - logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run?.Id ?? "Unknown", threadId); // %%% LOGGING + logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run?.Id ?? "Failed", threadId); } private static ChatMessageContent GenerateMessageContent(string? assistantName, ThreadMessage message) @@ -510,15 +515,15 @@ private static StreamingChatMessageContent GenerateStreamingMessageContent(strin // Process image content else if (update.ImageFileId != null) { - //content.Items.Add(new FileReferenceContent(itemContent.ImageFileId)); // %%% CONTENT + content.Items.Add(new StreamingFileReferenceContent(update.ImageFileId)); } // Process annotations else if (update.TextAnnotation != null) { - //content.Items.Add(GenerateAnnotationContent(annotation)); // %%% CONTENT + content.Items.Add(GenerateStreamingAnnotationContent(update.TextAnnotation)); } - if (update.Role.HasValue) + if (update.Role.HasValue && update.Role.Value != MessageRole.User) { content.Role = new(update.Role.Value.ToString()); } @@ -540,15 +545,36 @@ private static AnnotationContent GenerateAnnotationContent(TextAnnotation annota } return - new() + new(annotation.TextToReplace) { - Quote = annotation.TextToReplace, StartIndex = annotation.StartIndex, EndIndex = annotation.EndIndex, FileId = fileId, }; } + private static StreamingAnnotationContent GenerateStreamingAnnotationContent(TextAnnotationUpdate annotation) + { + string? fileId = null; + + if (!string.IsNullOrEmpty(annotation.OutputFileId)) + { + fileId = annotation.OutputFileId; + } + else if (!string.IsNullOrEmpty(annotation.InputFileId)) + { + fileId = annotation.InputFileId; + } + + return + new(annotation.TextToReplace) + { + StartIndex = annotation.StartIndex ?? 0, + EndIndex = annotation.EndIndex ?? 0, + FileId = fileId, + }; + } + private static ChatMessageContent GenerateCodeInterpreterContent(string agentName, string pythonCode) { return diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs index c514e2cd1bcb..db02718bcf83 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantChannel.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; -using Microsoft.SemanticKernel.ChatCompletion; using OpenAI.Assistants; namespace Microsoft.SemanticKernel.Agents.OpenAI; @@ -37,7 +36,7 @@ protected override async Task ReceiveAsync(IEnumerable histo } /// - protected override IAsyncEnumerable InvokeStreamingAsync(OpenAIAssistantAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + protected override IAsyncEnumerable InvokeStreamingAsync(OpenAIAssistantAgent agent, IList messages, CancellationToken cancellationToken = default) { return AssistantThreadActions.InvokeStreamingAsync(agent, this._client, this._threadId, messages, invocationOptions: null, this.Logger, agent.Kernel, agent.Arguments, cancellationToken); } diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index 8aec1853185e..c45f10f773d3 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; @@ -158,7 +159,8 @@ public IAsyncEnumerable InvalidInvokeAsync( public override IAsyncEnumerable InvokeStreamingAsync(CancellationToken cancellationToken = default) { - throw new System.NotImplementedException(); // %%% TODO + StreamingChatMessageContent[] messages = [new StreamingChatMessageContent(AuthorRole.Assistant, "sup")]; + return messages.ToAsyncEnumerable(); } } } diff --git a/dotnet/src/Agents/UnitTests/MockChannel.cs b/dotnet/src/Agents/UnitTests/MockChannel.cs index 4ed34b93ca97..5c2bbb469f59 100644 --- a/dotnet/src/Agents/UnitTests/MockChannel.cs +++ b/dotnet/src/Agents/UnitTests/MockChannel.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; -using Microsoft.SemanticKernel.ChatCompletion; namespace SemanticKernel.Agents.UnitTests; @@ -44,7 +43,7 @@ protected internal override IAsyncEnumerable GetHistoryAsync yield break; } - protected internal override IAsyncEnumerable InvokeStreamingAsync(MockAgent agent, ChatHistory messages, CancellationToken cancellationToken = default) + protected internal override IAsyncEnumerable InvokeStreamingAsync(MockAgent agent, IList messages, CancellationToken cancellationToken = default) { throw new NotImplementedException(); } diff --git a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs index 4d8f3ac7914d..f803c214ede3 100644 --- a/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs +++ b/dotnet/src/IntegrationTests/Connectors/OpenAI/OpenAIChatCompletion_NonStreamingTests.cs @@ -33,6 +33,7 @@ public async Task ChatCompletionShouldUseChatSystemPromptAsync() var result = await chatCompletion.GetChatMessageContentAsync("What is the capital of France?", settings, kernel); // Assert + Assert.Equal(AuthorRole.Assistant, result.Role); Assert.Contains("I don't know", result.Content); } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs index fd27b35a4b0f..f751ea6fc448 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/AnnotationContent.cs @@ -20,8 +20,7 @@ public class AnnotationContent : KernelContent /// /// The citation. /// - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? Quote { get; init; } + public string Quote { get; init; } = string.Empty; /// /// Start index of the citation. @@ -43,13 +42,17 @@ public AnnotationContent() /// /// Initializes a new instance of the class. /// + /// The source text being referenced. /// The model ID used to generate the content. /// Inner content /// Additional metadata public AnnotationContent( + string quote, string? modelId = null, object? innerContent = null, IReadOnlyDictionary? metadata = null) : base(innerContent, modelId, metadata) - { } + { + this.Quote = quote; + } } diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs new file mode 100644 index 000000000000..1d509a15c242 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft. All rights reserved. +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel.Agents.OpenAI; + +/// +/// Content type to support message annotations. +/// +[Experimental("SKEXP0110")] +public class StreamingAnnotationContent : StreamingKernelContent +{ + /// + /// The file identifier. + /// + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? FileId { get; init; } + + /// + /// The citation. + /// + public string Quote { get; init; } = string.Empty; + + /// + /// Start index of the citation. + /// + public int StartIndex { get; init; } + + /// + /// End index of the citation. + /// + public int EndIndex { get; init; } + + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public StreamingAnnotationContent() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The source text being referenced. + /// The model ID used to generate the content. + /// Inner content + /// Additional metadata + public StreamingAnnotationContent( + string quote, + string? modelId = null, + object? innerContent = null, + IReadOnlyDictionary? metadata = null) + : base(innerContent, choiceIndex: 0, modelId, metadata) + { + this.Quote = quote; + } + + /// + public override string ToString() + { + bool hasFileId = !string.IsNullOrEmpty(this.FileId); + + if (hasFileId) + { + return $"{this.Quote}: {this.FileId}"; + } + + return this.Quote; + } + + /// + public override byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(this.ToString()); + } +} diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs new file mode 100644 index 000000000000..d9c44fb205b1 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Text; +using System.Text.Json.Serialization; + +namespace Microsoft.SemanticKernel; + +/// +/// Content type to support file references. +/// +[Experimental("SKEXP0110")] +public class StreamingFileReferenceContent : StreamingKernelContent +{ + /// + /// The file identifier. + /// + public string FileId { get; init; } = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + [JsonConstructor] + public StreamingFileReferenceContent() + { } + + /// + /// Initializes a new instance of the class. + /// + /// The identifier of the referenced file. + /// The model ID used to generate the content. + /// Inner content + /// Additional metadata + public StreamingFileReferenceContent( + string fileId, + string? modelId = null, + object? innerContent = null, + IReadOnlyDictionary? metadata = null) + : base(innerContent, choiceIndex: 0, modelId, metadata) + { + this.FileId = fileId; + } + + /// + public override string ToString() + { + return this.FileId; + } + + /// + public override byte[] ToByteArray() + { + return Encoding.UTF8.GetBytes(this.ToString()); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/AnnotationContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/AnnotationContentTests.cs index 167811b1b2e7..524caed4ff29 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/AnnotationContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/AnnotationContentTests.cs @@ -19,7 +19,7 @@ public void VerifyAnnotationContentInitialState() { AnnotationContent definition = new(); - Assert.Null(definition.Quote); + Assert.Empty(definition.Quote); Assert.Equal(0, definition.StartIndex); Assert.Equal(0, definition.EndIndex); Assert.Null(definition.FileId); diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs index 759fec2b532b..9fe258d8bba8 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/ChatMessageContentTests.cs @@ -202,7 +202,7 @@ public void ItCanBeSerializeAndDeserialized() new FunctionCallContent("function-name", "plugin-name", "function-id", new KernelArguments { ["parameter"] = "argument" }), new FunctionResultContent(new FunctionCallContent("function-name", "plugin-name", "function-id"), "function-result"), new FileReferenceContent(fileId: "file-id-1") { ModelId = "model-7", Metadata = new Dictionary() { ["metadata-key-7"] = "metadata-value-7" } }, - new AnnotationContent() { ModelId = "model-8", FileId = "file-id-2", StartIndex = 2, EndIndex = 24, Quote = "quote-8", Metadata = new Dictionary() { ["metadata-key-8"] = "metadata-value-8" } } + new AnnotationContent("quote-8") { ModelId = "model-8", FileId = "file-id-2", StartIndex = 2, EndIndex = 24, Metadata = new Dictionary() { ["metadata-key-8"] = "metadata-value-8" } } ]; // Act diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs new file mode 100644 index 000000000000..83e5f088afd8 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs @@ -0,0 +1,71 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Xunit; + +namespace SemanticKernel.UnitTests.Contents; + +#pragma warning disable SKEXP0110 + +/// +/// Unit testing of . +/// +public class StreamingAnnotationContentTests +{ + /// + /// Verify default state. + /// + [Fact] + public void VerifyStreamingAnnotationContentInitialState() + { + StreamingAnnotationContent definition = new(); + + Assert.Empty(definition.Quote); + Assert.Equal(0, definition.StartIndex); + Assert.Equal(0, definition.EndIndex); + Assert.Null(definition.FileId); + } + + /// + /// Verify usage. + /// + [Fact] + public void VerifyStreamingAnnotationContentWithFileId () + { + StreamingAnnotationContent definition = + new("test quote") + { + StartIndex = 33, + EndIndex = 49, + FileId = "#id", + }; + + Assert.Equal("test quote", definition.Quote); + Assert.Equal(33, definition.StartIndex); + Assert.Equal(49, definition.EndIndex); + Assert.Equal("#id", definition.FileId); + Assert.Equal("test quote: #id", definition.ToString()); + Assert.Equal("test quote: #id", Encoding.UTF8.GetString(definition.ToByteArray())); + } + + /// + /// Verify usage. + /// + [Fact] + public void VerifyStreamingAnnotationContentWithoutFileId() + { + StreamingAnnotationContent definition = + new("test quote") + { + StartIndex = 33, + EndIndex = 49, + }; + + Assert.Equal("test quote", definition.Quote); + Assert.Equal(33, definition.StartIndex); + Assert.Equal(49, definition.EndIndex); + Assert.Null(definition.FileId); + Assert.Equal("test quote", definition.ToString()); + Assert.Equal("test quote", Encoding.UTF8.GetString(definition.ToByteArray())); + } +} diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs new file mode 100644 index 000000000000..507980bdb2d2 --- /dev/null +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.Text; +using Microsoft.SemanticKernel; +using Xunit; + +namespace SemanticKernel.UnitTests.Contents; + +#pragma warning disable SKEXP0110 + +/// +/// Unit testing of . +/// +public class StreamingFileReferenceContentTests +{ + /// + /// Verify default state. + /// + [Fact] + public void VerifyStreamingFileReferenceContentInitialState() + { + StreamingFileReferenceContent definition = new(); + + Assert.Empty(definition.FileId); + } + /// + /// Verify usage. + /// + [Fact] + public void VerifyStreamingFileReferenceContentUsage() + { + StreamingFileReferenceContent definition = new(fileId: "testfile"); + + Assert.Equal("testfile", definition.FileId); + Assert.Equal("testfile", definition.ToString()); + Assert.Equal("testfile", Encoding.UTF8.GetString(definition.ToByteArray())); + } +} From 4c97f2ba1d016a33ccc4dcd2aa44491a5e04f412 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:17:05 -0700 Subject: [PATCH 193/226] Typo --- dotnet/src/Agents/Abstractions/AgentChannel.cs | 4 ++-- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 +- dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AgentChannel.cs b/dotnet/src/Agents/Abstractions/AgentChannel.cs index cb7ed0b05012..04dfc58d8564 100644 --- a/dotnet/src/Agents/Abstractions/AgentChannel.cs +++ b/dotnet/src/Agents/Abstractions/AgentChannel.cs @@ -43,7 +43,7 @@ public abstract class AgentChannel /// Perform a discrete incremental interaction between a single and with streaming results. /// /// The agent actively interacting with the chat. - /// The reciever for the completed messages generated + /// The receiver for the completed messages generated /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of streaming messages. protected internal abstract IAsyncEnumerable InvokeStreamingAsync( @@ -99,7 +99,7 @@ public abstract class AgentChannel : AgentChannel where TAgent : Agent /// Process a discrete incremental interaction between a single an a . /// /// The agent actively interacting with the chat. - /// The reciever for the completed messages generated + /// The receiver for the completed messages generated /// The to monitor for cancellation requests. The default is . /// Asynchronous enumeration of messages. /// diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index f66db9cb279f..c6d187010405 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -343,7 +343,7 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R /// The assistant agent to interact with the thread. /// The assistant client /// The thread identifier - /// The reciever for the completed messages generated + /// The receiver for the completed messages generated /// Options to utilize for the invocation /// The logger to utilize (might be agent or channel scoped) /// The plugins and other state. diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index 5a93861c4267..b61cfedbd47a 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -294,7 +294,7 @@ public async IAsyncEnumerable InvokeAsync( /// Invoke the assistant on the specified thread. /// /// The thread identifier - /// The reciever for the completed messages generated + /// The receiver for the completed messages generated /// Optional arguments to pass to the agents's invocation, including any . /// The containing services, plugins, and other state for use by the agent. /// The to monitor for cancellation requests. The default is . @@ -314,7 +314,7 @@ public IAsyncEnumerable InvokeStreamingAsync( /// Invoke the assistant on the specified thread. /// /// The thread identifier - /// The reciever for the completed messages generated + /// The receiver for the completed messages generated /// Optional invocation options /// Optional arguments to pass to the agents's invocation, including any . /// The containing services, plugins, and other state for use by the agent. From 56eed66079e75fc613d503001af284b83111992c Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:27:04 -0700 Subject: [PATCH 194/226] namespace --- dotnet/src/Agents/Abstractions/AggregatorChannel.cs | 1 - dotnet/src/Agents/UnitTests/AgentChatTests.cs | 1 - dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs | 3 --- .../Contents/StreamingAnnotationContent.cs | 1 - 4 files changed, 6 deletions(-) diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 27fea5b550bb..30a3561a7b44 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -4,7 +4,6 @@ using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; -using Microsoft.SemanticKernel.ChatCompletion; namespace Microsoft.SemanticKernel.Agents; diff --git a/dotnet/src/Agents/UnitTests/AgentChatTests.cs b/dotnet/src/Agents/UnitTests/AgentChatTests.cs index c45f10f773d3..cbb04263b6d2 100644 --- a/dotnet/src/Agents/UnitTests/AgentChatTests.cs +++ b/dotnet/src/Agents/UnitTests/AgentChatTests.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.Collections.Generic; using System.Linq; -using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; diff --git a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs index f93cabf82b8e..504032854ebe 100644 --- a/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs +++ b/dotnet/src/Agents/UnitTests/Internal/BroadcastQueueTests.cs @@ -1,10 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; -using System.Collections.Generic; -using System.Threading; using System.Threading.Tasks; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Internal; using Microsoft.SemanticKernel.ChatCompletion; using Xunit; diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs index 1d509a15c242..80619445b780 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs @@ -1,5 +1,4 @@ // Copyright (c) Microsoft. All rights reserved. -using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Text; From ba93fe164f5ce334539660631976e3f0ce889171 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:27:38 -0700 Subject: [PATCH 195/226] Namespace --- dotnet/samples/Demos/AssistantStreaming/Program.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/Demos/AssistantStreaming/Program.cs b/dotnet/samples/Demos/AssistantStreaming/Program.cs index c6953894f751..f8a6347cd987 100644 --- a/dotnet/samples/Demos/AssistantStreaming/Program.cs +++ b/dotnet/samples/Demos/AssistantStreaming/Program.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. -using System.Text; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; From d62b0ea6f7b8f11b59705d6ac5114a37c054654d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:43:08 -0700 Subject: [PATCH 196/226] Namespace --- dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs index 3e3625050551..33605eed8d93 100644 --- a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -8,7 +8,6 @@ using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.AzureOpenAI; using Microsoft.SemanticKernel.Connectors.OpenAI; using SemanticKernel.IntegrationTests.TestSettings; using Xunit; From 019b2cb9c7608296a2fa54213c8baa56fb95ef71 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 15:52:36 -0700 Subject: [PATCH 197/226] Whitespace --- .../Contents/StreamingAnnotationContentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs index 83e5f088afd8..14ab8f4a7dc9 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs @@ -30,7 +30,7 @@ public void VerifyStreamingAnnotationContentInitialState() /// Verify usage. /// [Fact] - public void VerifyStreamingAnnotationContentWithFileId () + public void VerifyStreamingAnnotationContentWithFileId() { StreamingAnnotationContent definition = new("test quote") From 2bf6ccd6bc2cd7ed3210dfefddecb7264dd82f92 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 16:34:49 -0700 Subject: [PATCH 198/226] Suppress --- .../CompatibilitySuppressions.xml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml diff --git a/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml new file mode 100644 index 000000000000..178c983ba1d1 --- /dev/null +++ b/dotnet/src/SemanticKernel.Abstractions/CompatibilitySuppressions.xml @@ -0,0 +1,18 @@ + + + + + CP0002 + M:Microsoft.SemanticKernel.Agents.OpenAI.AnnotationContent.#ctor(System.String,System.Object,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object}) + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + lib/net8.0/Microsoft.SemanticKernel.Abstractions.dll + true + + + CP0002 + M:Microsoft.SemanticKernel.Agents.OpenAI.AnnotationContent.#ctor(System.String,System.Object,System.Collections.Generic.IReadOnlyDictionary{System.String,System.Object}) + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + lib/netstandard2.0/Microsoft.SemanticKernel.Abstractions.dll + true + + \ No newline at end of file From 7803a0fdbb4c641725cf1b5952709506c54f9b24 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 16:58:35 -0700 Subject: [PATCH 199/226] Test coverage --- dotnet/src/Agents/Abstractions/AgentChat.cs | 2 + dotnet/src/Agents/Core/ChatHistoryChannel.cs | 7 ++ .../Agents/ChatCompletionAgentTests.cs | 47 ++++++++++++ .../Agents/OpenAIAssistantAgentTests.cs | 74 +++++++++++++++++++ 4 files changed, 130 insertions(+) diff --git a/dotnet/src/Agents/Abstractions/AgentChat.cs b/dotnet/src/Agents/Abstractions/AgentChat.cs index 5bf4853a9d91..ed16e8224ca2 100644 --- a/dotnet/src/Agents/Abstractions/AgentChat.cs +++ b/dotnet/src/Agents/Abstractions/AgentChat.cs @@ -281,6 +281,8 @@ protected async IAsyncEnumerable InvokeStreamingAge yield return streamingContent; } + this.History.AddRange(messages); + this.Logger.LogAgentChatInvokedStreamingAgentMessages(nameof(InvokeAgentAsync), agent.GetType(), agent.Id, messages); // Broadcast message to other channels (in parallel) diff --git a/dotnet/src/Agents/Core/ChatHistoryChannel.cs b/dotnet/src/Agents/Core/ChatHistoryChannel.cs index 97e6a88d16df..5df63a3b23ba 100644 --- a/dotnet/src/Agents/Core/ChatHistoryChannel.cs +++ b/dotnet/src/Agents/Core/ChatHistoryChannel.cs @@ -88,10 +88,17 @@ protected override async IAsyncEnumerable InvokeStr // Pre-process history reduction. await this._history.ReduceAsync(historyHandler.HistoryReducer, cancellationToken).ConfigureAwait(false); + int messageCount = this._history.Count; + await foreach (StreamingChatMessageContent streamingMessage in historyHandler.InvokeStreamingAsync(this._history, null, null, cancellationToken).ConfigureAwait(false)) { yield return streamingMessage; } + + for (int index = messageCount; index < this._history.Count; ++index) + { + messages.Add(this._history[index]); + } } /// diff --git a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs index 33605eed8d93..c661b627e5c9 100644 --- a/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/ChatCompletionAgentTests.cs @@ -2,6 +2,7 @@ using System; using System.ComponentModel; using System.Linq; +using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -90,6 +91,52 @@ public async Task AzureChatCompletionAgentAsync(string input, string expectedAns Assert.Contains(expectedAnswerContains, messages.Single().Content, StringComparison.OrdinalIgnoreCase); } + /// + /// Integration test for using function calling + /// and targeting Azure OpenAI services. + /// + [Fact] + public async Task AzureChatCompletionStreamingAsync() + { + // Arrange + AzureOpenAIConfiguration configuration = this._configuration.GetSection("AzureOpenAI").Get()!; + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + + this._kernelBuilder.AddAzureOpenAIChatCompletion( + configuration.ChatDeploymentName!, + configuration.Endpoint, + configuration.ApiKey); + + this._kernelBuilder.Plugins.Add(plugin); + + Kernel kernel = this._kernelBuilder.Build(); + + ChatCompletionAgent agent = + new() + { + Kernel = kernel, + Instructions = "Answer questions about the menu.", + Arguments = new(new OpenAIPromptExecutionSettings() { ToolCallBehavior = ToolCallBehavior.AutoInvokeKernelFunctions }), + }; + + AgentGroupChat chat = new(); + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, "What is the special soup?")); + + // Act + StringBuilder builder = new(); + await foreach (var message in chat.InvokeStreamingAsync(agent)) + { + builder.Append(message.Content); + } + + ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync(); + + // Assert + Assert.Contains("Clam Chowder", builder.ToString(), StringComparison.OrdinalIgnoreCase); + Assert.Contains("Clam Chowder", history.First().Content, StringComparison.OrdinalIgnoreCase); + } + public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] diff --git a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs index 0dc1ae952c20..f559df3fc70e 100644 --- a/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs +++ b/dotnet/src/IntegrationTests/Agents/OpenAIAssistantAgentTests.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft. All rights reserved. using System; using System.ComponentModel; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.Extensions.Configuration; @@ -60,6 +61,42 @@ await this.ExecuteAgentAsync( expectedAnswerContains); } + /// + /// Integration test for using function calling + /// and targeting Open AI services. + /// + [Theory(Skip = "OpenAI will often throttle requests. This test is for manual verification.")] + [InlineData("What is the special soup?", "Clam Chowder")] + public async Task OpenAIAssistantAgentStreamingAsync(string input, string expectedAnswerContains) + { + OpenAIConfiguration openAISettings = this._configuration.GetSection("OpenAI").Get()!; + Assert.NotNull(openAISettings); + + await this.ExecuteStreamingAgentAsync( + OpenAIClientProvider.ForOpenAI(openAISettings.ApiKey), + openAISettings.ModelId, + input, + expectedAnswerContains); + } + + /// + /// Integration test for using function calling + /// and targeting Azure OpenAI services. + /// + [Theory/*(Skip = "No supported endpoint configured.")*/] + [InlineData("What is the special soup?", "Clam Chowder")] + public async Task AzureOpenAIAssistantAgentStreamingAsync(string input, string expectedAnswerContains) + { + var azureOpenAIConfiguration = this._configuration.GetSection("AzureOpenAI").Get(); + Assert.NotNull(azureOpenAIConfiguration); + + await this.ExecuteStreamingAgentAsync( + OpenAIClientProvider.ForAzureOpenAI(azureOpenAIConfiguration.ApiKey, new Uri(azureOpenAIConfiguration.Endpoint)), + azureOpenAIConfiguration.ChatDeploymentName!, + input, + expectedAnswerContains); + } + private async Task ExecuteAgentAsync( OpenAIClientProvider config, string modelName, @@ -95,6 +132,43 @@ await OpenAIAssistantAgent.CreateAsync( Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase); } + private async Task ExecuteStreamingAgentAsync( + OpenAIClientProvider config, + string modelName, + string input, + string expected) + { + // Arrange + Kernel kernel = new(); + + KernelPlugin plugin = KernelPluginFactory.CreateFromType(); + kernel.Plugins.Add(plugin); + + OpenAIAssistantAgent agent = + await OpenAIAssistantAgent.CreateAsync( + kernel, + config, + new(modelName) + { + Instructions = "Answer questions about the menu.", + }); + + AgentGroupChat chat = new(); + chat.AddChatMessage(new ChatMessageContent(AuthorRole.User, input)); + + // Act + StringBuilder builder = new(); + await foreach (var message in chat.InvokeStreamingAsync(agent)) + { + builder.Append(message.Content); + } + + // Assert + ChatMessageContent[] history = await chat.GetChatMessagesAsync().ToArrayAsync(); + Assert.Contains(expected, builder.ToString(), StringComparison.OrdinalIgnoreCase); + Assert.Contains(expected, history.First().Content, StringComparison.OrdinalIgnoreCase); + } + public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] From 1ca53733deed1f04a80a8dd9a7567cdfd4141365 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 14 Aug 2024 17:25:54 -0700 Subject: [PATCH 200/226] One more sample --- .../Concepts/Agents/MixedChat_Streaming.cs | 159 ++++++++++++++++++ 1 file changed, 159 insertions(+) create mode 100644 dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs new file mode 100644 index 000000000000..5447b04dc78a --- /dev/null +++ b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft. All rights reserved. +using System.ComponentModel; +using Microsoft.SemanticKernel; +using Microsoft.SemanticKernel.Agents; +using Microsoft.SemanticKernel.Agents.Chat; +using Microsoft.SemanticKernel.Agents.OpenAI; +using Microsoft.SemanticKernel.ChatCompletion; +using Microsoft.SemanticKernel.Connectors.OpenAI; + +namespace Agents; + +/// +/// Demonstrate creation of and +/// eliciting its response to three explicit user messages. +/// +public class MixedChat_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) +{ + private const string ReviewerName = "ArtDirector"; + private const string ReviewerInstructions = + """ + You are an art director who has opinions about copywriting born of a love for David Ogilvy. + The goal is to determine is the given copy is acceptable to print. + If so, state that it is approved. + If not, provide insight on how to refine suggested copy without example. + """; + + private const string CopyWriterName = "CopyWriter"; + private const string CopyWriterInstructions = + """ + You are a copywriter with ten years of experience and are known for brevity and a dry humor. + The goal is to refine and decide on the single best copy as an expert in the field. + Only provide a single proposal per response. + You're laser focused on the goal at hand. + Don't waste time with chit chat. + Consider suggestions when refining an idea. + """; + + [Fact] + public async Task UseStreamingAgentChatAsync() + { + // Define the agents: one of each type + ChatCompletionAgent agentReviewer = + new() + { + Instructions = ReviewerInstructions, + Name = ReviewerName, + Kernel = this.CreateKernelWithChatCompletion(), + }; + + OpenAIAssistantAgent agentWriter = + await OpenAIAssistantAgent.CreateAsync( + kernel: new(), + clientProvider: this.GetClientProvider(), + definition: new(this.Model) + { + Instructions = CopyWriterInstructions, + Name = CopyWriterName, + Metadata = AssistantSampleMetadata, + }); + + // Create a chat for agent interaction. + AgentGroupChat chat = + new(agentWriter, agentReviewer) + { + ExecutionSettings = + new() + { + // Here a TerminationStrategy subclass is used that will terminate when + // an assistant message contains the term "approve". + TerminationStrategy = + new ApprovalTerminationStrategy() + { + // Only the art-director may approve. + Agents = [agentReviewer], + // Limit total number of turns + MaximumIterations = 10, + } + } + }; + + // Invoke chat and display messages. + ChatMessageContent input = new(AuthorRole.User, "concept: maps made out of egg cartons."); + chat.AddChatMessage(input); + this.WriteAgentChatMessage(input); + + string lastAgent = string.Empty; + await foreach (StreamingChatMessageContent response in chat.InvokeStreamingAsync()) + { + if (string.IsNullOrEmpty(response.Content)) + { + continue; + } + + if (!lastAgent.Equals(response.AuthorName, StringComparison.Ordinal)) + { + Console.WriteLine($"\n# {response.Role} - {response.AuthorName ?? "*"}:"); + lastAgent = response.AuthorName ?? string.Empty; + } + + Console.WriteLine($"\t > streamed: '{response.Content}'"); + } + + // Display the chat history. + Console.WriteLine("================================"); + Console.WriteLine("CHAT HISTORY"); + Console.WriteLine("================================"); + + ChatMessageContent[] history = await chat.GetChatMessagesAsync().Reverse().ToArrayAsync(); + + for (int index = 0; index < history.Length; index++) + { + this.WriteAgentChatMessage(history[index]); + } + + Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); + } + + // Local function to invoke agent and display the conversation messages. + private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input) + { + ChatMessageContent message = new(AuthorRole.User, input); + chat.Add(message); + this.WriteAgentChatMessage(message); + + int historyCount = chat.Count; + + bool isFirst = false; + await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat)) + { + if (string.IsNullOrEmpty(response.Content)) + { + continue; + } + + if (!isFirst) + { + Console.WriteLine($"\n# {response.Role} - {response.AuthorName ?? "*"}:"); + isFirst = true; + } + + Console.WriteLine($"\t > streamed: '{response.Content}'"); + } + + if (historyCount <= chat.Count) + { + for (int index = historyCount; index < chat.Count; index++) + { + this.WriteAgentChatMessage(chat[index]); + } + } + } + + private sealed class ApprovalTerminationStrategy : TerminationStrategy + { + // Terminate when the final message contains the term "approve" + protected override Task ShouldAgentTerminateAsync(Agent agent, IReadOnlyList history, CancellationToken cancellationToken) + => Task.FromResult(history[history.Count - 1].Content?.Contains("approve", StringComparison.OrdinalIgnoreCase) ?? false); + } +} From 76fdb4c245d038aa39be66ce6cae001cd347fcd6 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 15 Aug 2024 09:13:29 -0700 Subject: [PATCH 201/226] Namespace --- dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs index 5447b04dc78a..e316c40211ac 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs @@ -1,11 +1,9 @@ // Copyright (c) Microsoft. All rights reserved. -using System.ComponentModel; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.Chat; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Agents; From 86e1df611aa1e9f1f8d68b9f18e60bd4ffebd1b7 Mon Sep 17 00:00:00 2001 From: Roger Barreto <19890735+RogerBarreto@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:29:37 +0100 Subject: [PATCH 202/226] .Net: OpenAI V2 - Prompty UT Fix (#8277) ### Motivation and Context Settings are nullable in the Execution Settings V2 moving forward, the top_p is not by default 1.0 if not set, setting the value in the yml to comply with the test. --- .../Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty index a6be798dbf1a..ba095afeebfc 100644 --- a/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty +++ b/dotnet/src/Functions/Functions.Prompty.UnitTests/TestData/chatJsonObject.prompty @@ -11,6 +11,7 @@ model: parameters: temperature: 0.0 max_tokens: 3000 + top_p: 1.0 response_format: type: json_object From 5774c73e2b290d14e4bd9bcf1be759f7d204c1a9 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 19 Aug 2024 11:41:59 -0700 Subject: [PATCH 203/226] Sync new sample --- .../Concepts/Agents/MixedChat_Images.cs | 2 +- .../Concepts/Agents/MixedChat_Reset.cs | 22 +++++++++---------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs index 142706e8506c..437643e25574 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Images.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Images.cs @@ -84,7 +84,7 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) if (!string.IsNullOrWhiteSpace(input)) { ChatMessageContent message = new(AuthorRole.User, input); - chat.AddChatMessage(new(AuthorRole.User, input)); + chat.AddChatMessage(message); this.WriteAgentChatMessage(message); } diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs index 92aa8a9ce9d4..e8ba13f089ad 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using Azure; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; @@ -10,7 +11,7 @@ namespace Agents; /// /// Demonstrate the use of . /// -public class MixedChat_Reset(ITestOutputHelper output) : BaseTest(output) +public class MixedChat_Reset(ITestOutputHelper output) : BaseAgentsTest(output) { private const string AgentInstructions = """ @@ -21,18 +22,17 @@ The user may either provide information or query on information previously provi [Fact] public async Task ResetChatAsync() { - OpenAIFileService fileService = new(TestConfiguration.OpenAI.ApiKey); + OpenAIClientProvider provider = this.GetClientProvider(); // Define the agents OpenAIAssistantAgent assistantAgent = await OpenAIAssistantAgent.CreateAsync( kernel: new(), - config: new(this.ApiKey, this.Endpoint), - new() + provider, + new(this.Model) { Name = nameof(OpenAIAssistantAgent), Instructions = AgentInstructions, - ModelId = this.Model, }); ChatCompletionAgent chatAgent = @@ -74,16 +74,14 @@ async Task InvokeAgentAsync(Agent agent, string? input = null) { if (!string.IsNullOrWhiteSpace(input)) { - chat.AddChatMessage(new(AuthorRole.User, input)); - Console.WriteLine($"\n# {AuthorRole.User}: '{input}'"); + ChatMessageContent message = new(AuthorRole.User, input); + chat.AddChatMessage(message); + this.WriteAgentChatMessage(message); } - await foreach (ChatMessageContent message in chat.InvokeAsync(agent)) + await foreach (ChatMessageContent response in chat.InvokeAsync(agent)) { - if (!string.IsNullOrWhiteSpace(message.Content)) - { - Console.WriteLine($"\n# {message.Role} - {message.AuthorName ?? "*"}: '{message.Content}'"); - } + this.WriteAgentChatMessage(response); } } } From c93a56a713cc3bc77c8becd34bb957762b31ecc1 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Mon, 19 Aug 2024 11:45:02 -0700 Subject: [PATCH 204/226] Namespace in sample --- dotnet/samples/Concepts/Agents/MixedChat_Reset.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs index e8ba13f089ad..f9afcc55b7f5 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Reset.cs @@ -1,10 +1,8 @@ // Copyright (c) Microsoft. All rights reserved. -using Azure; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; -using Microsoft.SemanticKernel.Connectors.OpenAI; namespace Agents; From c80aa0ff922145456f4ef5e29885d47738a5a2cc Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Tue, 3 Sep 2024 20:04:11 -0700 Subject: [PATCH 205/226] Checkpoint --- .../Step11_AssistantTool_FileSearch.cs | 2 +- .../OpenAI/Internal/AssistantThreadActions.cs | 24 +++++++-------- .../src/Agents/OpenAI/OpenAIAssistantAgent.cs | 29 ++++--------------- dotnet/src/Agents/UnitTests/MockAgent.cs | 2 +- 4 files changed, 18 insertions(+), 39 deletions(-) diff --git a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs index d34cadaf3707..581cfd355995 100644 --- a/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs +++ b/dotnet/samples/GettingStartedWithAgents/Step11_AssistantTool_FileSearch.cs @@ -64,7 +64,7 @@ await agent.CreateThreadAsync( await agent.DeleteThreadAsync(threadId); await agent.DeleteAsync(); await vectorStoreClient.DeleteVectorStoreAsync(vectorStore); - await fileClient.DeleteFileAsync(fileInfo); + await fileClient.DeleteFileAsync(fileInfo.Id); } // Local function to invoke agent and display the conversation messages. diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 70127a3c9996..e3b8e7b4c464 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System; +using System.ClientModel; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -114,7 +115,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist await foreach (PageResult page in client.GetMessagesAsync(threadId, new() { Order = ListOrder.NewestFirst }, cancellationToken).ConfigureAwait(false)) { - foreach (var message in page.Values) + foreach (ThreadMessage message in page.Values) { AuthorRole role = new(message.Role.ToString()); @@ -201,7 +202,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist } List steps = []; - await foreach (var page in client.GetRunStepsAsync(run).ConfigureAwait(false)) + await foreach (PageResult page in client.GetRunStepsAsync(run).ConfigureAwait(false)) { steps.AddRange(page.Values); }; @@ -275,7 +276,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist else if (completedStep.Type == RunStepType.MessageCreation) { // Retrieve the message - ThreadMessage? message = await RetrieveMessageAsync(completedStep.Details.CreatedMessageId, cancellationToken).ConfigureAwait(false); + ThreadMessage? message = await RetrieveMessageAsync(client, threadId, completedStep.Details.CreatedMessageId, agent.PollingOptions.MessageSynchronizationDelay, cancellationToken).ConfigureAwait(false); if (message is not null) { @@ -339,8 +340,6 @@ IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, R FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - var content = new FunctionCallContent(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - functionSteps.Add(toolCall.ToolCallId, content); yield return content; @@ -375,7 +374,7 @@ public static async IAsyncEnumerable InvokeStreamin Kernel kernel, KernelArguments? arguments, [EnumeratorCancellation] CancellationToken cancellationToken) - { + { if (agent.IsDeleted) { throw new KernelException($"Agent Failure - {nameof(OpenAIAssistantAgent)} agent is deleted: {agent.Id}."); @@ -398,19 +397,19 @@ public static async IAsyncEnumerable InvokeStreamin ThreadRun? run = null; IAsyncEnumerable asyncUpdates = client.CreateRunStreamingAsync(threadId, agent.Id, options, cancellationToken); - do - { + do + { functionCalls.Clear(); functionResultTasks.Clear(); messageIds.Clear(); if (run != null) - { + { Debugger.Break(); - } + } await foreach (StreamingUpdate update in asyncUpdates.ConfigureAwait(false)) - { + { if (update is RunUpdate runUpdate) { run = runUpdate.Value; @@ -469,8 +468,7 @@ public static async IAsyncEnumerable InvokeStreamin ChatMessageContent content = GenerateMessageContent(agent.GetName(), message); messages.Add(content); } - } - while (retry); + } logger.LogOpenAIAssistantProcessedRunMessages(nameof(InvokeAsync), messageIds.Count, run!.Id, threadId); } diff --git a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs index b2b0368d0aa0..a7c579ffc272 100644 --- a/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs +++ b/dotnet/src/Agents/OpenAI/OpenAIAssistantAgent.cs @@ -1,4 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. +using System.ClientModel; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,6 +9,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.Agents.OpenAI.Internal; +using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; using OpenAI.Files; @@ -108,7 +110,7 @@ public static async IAsyncEnumerable ListDefinitionsA AssistantClient client = CreateClient(provider); // Query and enumerate assistant definitions - await foreach (var page in client.GetAssistantsAsync(new AssistantCollectionOptions() { Order = ListOrder.NewestFirst }, cancellationToken).ConfigureAwait(false)) + await foreach (PageResult page in client.GetAssistantsAsync(new AssistantCollectionOptions() { Order = ListOrder.NewestFirst }, cancellationToken).ConfigureAwait(false)) { foreach (Assistant model in page.Values) { @@ -135,7 +137,7 @@ public static async Task RetrieveAsync( AssistantClient client = CreateClient(provider); // Retrieve the assistant - Assistant model = await client.GetAssistantAsync(id).ConfigureAwait(false); // SDK BUG - CANCEL TOKEN (https://github.com/microsoft/semantic-kernel/issues/7431) + Assistant model = await client.GetAssistantAsync(id, cancellationToken).ConfigureAwait(false); // Instantiate the agent return @@ -259,25 +261,6 @@ public IAsyncEnumerable InvokeAsync( CancellationToken cancellationToken = default) => this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken); - /// - /// Invoke the assistant on the specified thread. - /// - /// The thread identifier - /// Optional invocation options - /// Optional arguments to pass to the agents's invocation, including any . - /// The containing services, plugins, and other state for use by the agent. - /// The to monitor for cancellation requests. The default is . - /// Asynchronous enumeration of messages. - /// - /// The `arguments` parameter is not currently used by the agent, but is provided for future extensibility. - /// - public IAsyncEnumerable InvokeAsync( - string threadId, - KernelArguments? arguments = null, - Kernel? kernel = null, - CancellationToken cancellationToken = default) - => this.InvokeAsync(threadId, options: null, arguments, kernel, cancellationToken); - /// /// Invoke the assistant on the specified thread. /// @@ -469,9 +452,7 @@ private static AssistantCreationOptions CreateAssistantCreationOptions(OpenAIAss NucleusSamplingFactor = definition.TopP, }; - assistantCreationOptions.FileIds.AddRange(definition.FileIds ?? []); - - if (definition.EnableCodeInterpreter) + if (definition.Metadata != null) { foreach (KeyValuePair item in definition.Metadata) { diff --git a/dotnet/src/Agents/UnitTests/MockAgent.cs b/dotnet/src/Agents/UnitTests/MockAgent.cs index 6e20c0434b93..b8b7f295e02b 100644 --- a/dotnet/src/Agents/UnitTests/MockAgent.cs +++ b/dotnet/src/Agents/UnitTests/MockAgent.cs @@ -11,7 +11,7 @@ namespace SemanticKernel.Agents.UnitTests; /// /// Mock definition of with a contract. /// -internal class MockAgent : ChatHistoryKernelAgent +internal sealed class MockAgent : ChatHistoryKernelAgent { public int InvokeCount { get; private set; } From 230fc795865755815a78c6de234a90900b6a2a5d Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Sep 2024 10:46:09 -0700 Subject: [PATCH 206/226] Checkpoint --- .../Agents/ChatCompletion_Streaming.cs | 20 +++ .../OpenAI/Internal/AssistantThreadActions.cs | 130 ++++++++++-------- 2 files changed, 89 insertions(+), 61 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 334b7f53496b..9085d9f8b6f0 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; +using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; @@ -34,6 +35,9 @@ public async Task UseStreamingChatCompletionAgentAsync() await InvokeAgentAsync(agent, chat, "Fortune favors the bold."); await InvokeAgentAsync(agent, chat, "I came, I saw, I conquered."); await InvokeAgentAsync(agent, chat, "Practice makes perfect."); + + // Output the entire chat history + DisplayChatHistory(chat); } [Fact] @@ -60,6 +64,9 @@ public async Task UseStreamingChatCompletionAgentWithPluginAsync() // Respond to user input await InvokeAgentAsync(agent, chat, "What is the special soup?"); await InvokeAgentAsync(agent, chat, "What is the special drink?"); + + // Output the entire chat history + DisplayChatHistory(chat); } // Local function to invoke agent and display the conversation messages. @@ -97,6 +104,19 @@ private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, } } + private void DisplayChatHistory(ChatHistory history) + { + // Display the chat history. + Console.WriteLine("================================"); + Console.WriteLine("CHAT HISTORY"); + Console.WriteLine("================================"); + + foreach (ChatMessageContent message in history) + { + this.WriteAgentChatMessage(message); + } + } + public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index e3b8e7b4c464..a234e156e564 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -201,11 +201,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } - List steps = []; - await foreach (PageResult page in client.GetRunStepsAsync(run).ConfigureAwait(false)) - { - steps.AddRange(page.Values); - }; + IReadOnlyList steps = await GetRunStepsAsync(client, run).ConfigureAwait(false); // Is tool action required? if (run.Status == RunStatus.RequiresAction) @@ -213,14 +209,19 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); // Execute functions in parallel and post results at once. - FunctionCallContent[] activeFunctionSteps = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); - if (activeFunctionSteps.Length > 0) + FunctionCallContent[] functionCalls = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + // Capture function-call for message processing + foreach (FunctionCallContent functionCall in functionCalls) + { + functionSteps.Add(functionCall.Id!, functionCall); + } + if (functionCalls.Length > 0) { // Emit function-call content - yield return (IsVisible: false, Message: GenerateFunctionCallContent(agent.GetName(), activeFunctionSteps)); + yield return (IsVisible: false, Message: GenerateFunctionCallContent(agent.GetName(), functionCalls)); // Invoke functions for each tool-step - IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, activeFunctionSteps, cancellationToken); + IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, functionCalls, cancellationToken); // Block for function results FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); @@ -231,7 +232,7 @@ public static async IAsyncEnumerable GetMessagesAsync(Assist await client.SubmitToolOutputsToRunAsync(threadId, run.Id, toolOutputs, cancellationToken).ConfigureAwait(false); } - logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), activeFunctionSteps.Length, run.Id, threadId); + logger.LogOpenAIAssistantProcessedRunSteps(nameof(InvokeAsync), functionCalls.Length, run.Id, threadId); } // Enumerate completed messages @@ -328,24 +329,6 @@ async Task PollRunStatusAsync() logger.LogOpenAIAssistantPolledRunStatus(nameof(PollRunStatusAsync), run.Status, run.Id, threadId); } - - // Local function to capture kernel function state for further processing (participates in method closure). - IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) - { - if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls) - { - foreach (RunStepToolCall toolCall in step.Details.ToolCalls) - { - (FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(toolCall.FunctionName, toolCall.FunctionArguments); - - FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - - functionSteps.Add(toolCall.ToolCallId, content); - - yield return content; - } - } - } } /// @@ -390,24 +373,14 @@ public static async IAsyncEnumerable InvokeStreamin // Evaluate status and process steps and messages, as encountered. HashSet processedStepIds = []; - List functionCalls = []; - List> functionResultTasks = []; HashSet messageIds = []; ThreadRun? run = null; IAsyncEnumerable asyncUpdates = client.CreateRunStreamingAsync(threadId, agent.Id, options, cancellationToken); - do { - functionCalls.Clear(); - functionResultTasks.Clear(); messageIds.Clear(); - if (run != null) - { - Debugger.Break(); - } - await foreach (StreamingUpdate update in asyncUpdates.ConfigureAwait(false)) { if (update is RunUpdate runUpdate) @@ -416,15 +389,6 @@ public static async IAsyncEnumerable InvokeStreamin logger.LogOpenAIAssistantCreatedRun(nameof(InvokeAsync), run.Id, threadId); } - else if (update is RequiredActionUpdate actionUpdate) - { - (FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(actionUpdate.FunctionName, actionUpdate.FunctionArguments); - FunctionCallContent functionCall = new(nameParts.Name, nameParts.PluginName, actionUpdate.ToolCallId, functionArguments); - functionCalls.Add(functionCall); - - Task functionResultTask = ExecuteFunctionStep(agent, functionCall, cancellationToken); - functionResultTasks.Add(functionResultTask); - } else if (update is MessageContentUpdate contentUpdate) { messageIds.Add(contentUpdate.MessageId); @@ -432,27 +396,42 @@ public static async IAsyncEnumerable InvokeStreamin } } - if (run != null) + if (run == null) { - // Is in terminal state? - if (s_terminalStatuses.Contains(run.Status)) - { - throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); - } + throw new KernelException($"Agent Failure - Run not created for thread: ${threadId}"); + } + + // Is in terminal state? + if (s_terminalStatuses.Contains(run.Status)) + { + throw new KernelException($"Agent Failure - Run terminated: {run.Status} [{run.Id}]: {run.LastError?.Message ?? "Unknown"}"); } - if (functionCalls.Count > 0) + if (run.Status == RunStatus.RequiresAction) { - messages.Add(GenerateFunctionCallContent(agent.GetName(), functionCalls)); + IReadOnlyList steps = await GetRunStepsAsync(client, run).ConfigureAwait(false); + + //logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); - // Block for function results - FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); + // Execute functions in parallel and post results at once. + FunctionCallContent[] functionCalls = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); + if (functionCalls.Length > 0) + { + // Emit function-call content + messages.Add(GenerateFunctionCallContent(agent.GetName(), functionCalls)); - // Process tool output - ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); - asyncUpdates = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); + // Invoke functions for each tool-step + IEnumerable> functionResultTasks = ExecuteFunctionSteps(agent, functionCalls, cancellationToken); - messages.Add(GenerateFunctionResultsContent(agent.GetName(), functionResults)); + // Block for function results + FunctionResultContent[] functionResults = await Task.WhenAll(functionResultTasks).ConfigureAwait(false); + + // Process tool output + ToolOutput[] toolOutputs = GenerateToolOutputs(functionResults); + asyncUpdates = client.SubmitToolOutputsToRunStreamingAsync(run, toolOutputs); + + messages.Add(GenerateFunctionResultsContent(agent.GetName(), functionResults)); + } } if (messageIds.Count > 0) @@ -478,6 +457,18 @@ public static async IAsyncEnumerable InvokeStreamin logger.LogOpenAIAssistantCompletedRun(nameof(InvokeAsync), run?.Id ?? "Failed", threadId); } + private static async Task> GetRunStepsAsync(AssistantClient client, ThreadRun run) + { + List steps = []; + + await foreach (PageResult page in client.GetRunStepsAsync(run).ConfigureAwait(false)) + { + steps.AddRange(page.Values); + }; + + return steps; + } + private static ChatMessageContent GenerateMessageContent(string? assistantName, ThreadMessage message) { AuthorRole role = new(message.Role.ToString()); @@ -601,6 +592,23 @@ private static ChatMessageContent GenerateCodeInterpreterContent(string agentNam }; } + private static IEnumerable ParseFunctionStep(OpenAIAssistantAgent agent, RunStep step) + { + if (step.Status == RunStepStatus.InProgress && step.Type == RunStepType.ToolCalls) + { + foreach (RunStepToolCall toolCall in step.Details.ToolCalls) + { + (FunctionName nameParts, KernelArguments functionArguments) = ParseFunctionCall(toolCall.FunctionName, toolCall.FunctionArguments); + + FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); + + //functionSteps.Add(toolCall.ToolCallId, content); + + yield return content; + } + } + } + private static (FunctionName functionName, KernelArguments arguments) ParseFunctionCall(string functionName, string? functionArguments) { FunctionName nameParts = FunctionName.Parse(functionName); From 2b4e0b832719757f403c5b046724d9f1c782f807 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Sep 2024 10:51:42 -0700 Subject: [PATCH 207/226] Tune samples --- .../Agents/OpenAIAssistant_Streaming.cs | 21 +++++++++++++++++++ .../Agents/Abstractions/AggregatorChannel.cs | 7 +++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs index 72a28d6dded8..3f75f9044268 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -1,5 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; +using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; @@ -38,6 +39,9 @@ await OpenAIAssistantAgent.CreateAsync( await InvokeAgentAsync(agent, threadId, "Fortune favors the bold."); await InvokeAgentAsync(agent, threadId, "I came, I saw, I conquered."); await InvokeAgentAsync(agent, threadId, "Practice makes perfect."); + + // Output the entire chat history + await DisplayChatHistoryAsync(agent, threadId); } [Fact] @@ -67,6 +71,9 @@ await OpenAIAssistantAgent.CreateAsync( // Respond to user input await InvokeAgentAsync(agent, threadId, "What is the special soup?"); await InvokeAgentAsync(agent, threadId, "What is the special drink?"); + + // Output the entire chat history + await DisplayChatHistoryAsync(agent, threadId); } // Local function to invoke agent and display the conversation messages. @@ -101,6 +108,20 @@ private async Task InvokeAgentAsync(OpenAIAssistantAgent agent, string threadId, } } + private async Task DisplayChatHistoryAsync(OpenAIAssistantAgent agent, string threadId) + { + + Console.WriteLine("================================"); + Console.WriteLine("CHAT HISTORY"); + Console.WriteLine("================================"); + + ChatMessageContent[] messages = await agent.GetThreadMessagesAsync(threadId).ToArrayAsync(); + for (int index = messages.Length - 1; index >= 0; --index) + { + this.WriteAgentChatMessage(messages[index]); + } + } + public sealed class MenuPlugin { [KernelFunction, Description("Provides a list of specials from the menu.")] diff --git a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs index 1cd7e8c242fe..05adb1e2af04 100644 --- a/dotnet/src/Agents/Abstractions/AggregatorChannel.cs +++ b/dotnet/src/Agents/Abstractions/AggregatorChannel.cs @@ -55,9 +55,12 @@ protected internal override async IAsyncEnumerable { int messageCount = await this._chat.GetChatMessagesAsync(cancellationToken).CountAsync(cancellationToken).ConfigureAwait(false); - await foreach (StreamingChatMessageContent message in this._chat.InvokeStreamingAsync(cancellationToken).ConfigureAwait(false)) // %%% NOISY / NEEDED ??? + if (agent.Mode == AggregatorMode.Flat) { - yield return message; + await foreach (StreamingChatMessageContent message in this._chat.InvokeStreamingAsync(cancellationToken).ConfigureAwait(false)) + { + yield return message; + } } ChatMessageContent[] history = await this._chat.GetChatMessagesAsync(cancellationToken).ToArrayAsync(cancellationToken).ConfigureAwait(false); From 9cb5ecf83a5edacecd1d74979971e781aee3f257 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Sep 2024 10:54:39 -0700 Subject: [PATCH 208/226] Namespace --- dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs | 1 - dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 9085d9f8b6f0..5c512fb34147 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; -using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.ChatCompletion; diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs index 3f75f9044268..ffbc43c2ad75 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; -using Microsoft.Graph; using Microsoft.SemanticKernel; using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; From f4c5609c882e9519d2f23ec7ee72eb47d871429f Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Sep 2024 11:08:00 -0700 Subject: [PATCH 209/226] whitespace --- dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs index ffbc43c2ad75..0f89258bad4d 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -109,7 +109,6 @@ private async Task InvokeAgentAsync(OpenAIAssistantAgent agent, string threadId, private async Task DisplayChatHistoryAsync(OpenAIAssistantAgent agent, string threadId) { - Console.WriteLine("================================"); Console.WriteLine("CHAT HISTORY"); Console.WriteLine("================================"); From 9d7ef2201f4914765172db0de8963c87901bc2ba Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Wed, 4 Sep 2024 11:12:42 -0700 Subject: [PATCH 210/226] namespace --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index a234e156e564..3eb2eb28b5e2 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -2,7 +2,6 @@ using System; using System.ClientModel; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Net; using System.Runtime.CompilerServices; From d6bf539f3e5cdcf4c49b02ff3e90cad47408cdc8 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 5 Sep 2024 09:34:36 -0700 Subject: [PATCH 211/226] Remove demo --- .../AssistantStreaming.csproj | 24 -------- .../Demos/AssistantStreaming/Program.cs | 57 ------------------- 2 files changed, 81 deletions(-) delete mode 100644 dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj delete mode 100644 dotnet/samples/Demos/AssistantStreaming/Program.cs diff --git a/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj b/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj deleted file mode 100644 index e4926c923e6e..000000000000 --- a/dotnet/samples/Demos/AssistantStreaming/AssistantStreaming.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - Exe - net8.0 - enable - enable - 5ee045b0-aea3-4f08-8d31-32d1a6f8fed0 - $(NoWarn);CS8618,IDE0009,CA1051,CA1050,CA1707,CA1054,CA2007,VSTHRD111,CS1591,RCS1110,RCS1243,CA5394,SKEXP0001,SKEXP0010,SKEXP0020,SKEXP0040,SKEXP0050,SKEXP0060,SKEXP0070,SKEXP0101,SKEXP0110,OPENAI001 - - - - - - - - - - - - - - - diff --git a/dotnet/samples/Demos/AssistantStreaming/Program.cs b/dotnet/samples/Demos/AssistantStreaming/Program.cs deleted file mode 100644 index f8a6347cd987..000000000000 --- a/dotnet/samples/Demos/AssistantStreaming/Program.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) Microsoft. All rights reserved. - -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Logging.Abstractions; -using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents.OpenAI; -using Microsoft.SemanticKernel.ChatCompletion; - -var configuration = new ConfigurationBuilder() - .AddUserSecrets() - .AddEnvironmentVariables() - .Build(); - -string? apiKey = configuration["OpenAI:ApiKey"]; -string? modelId = configuration["OpenAI:ChatModelId"]; - -// Logger for program scope -ILogger logger = NullLogger.Instance; - -ArgumentNullException.ThrowIfNull(apiKey); -ArgumentNullException.ThrowIfNull(modelId); - -OpenAIAssistantAgent agent = - await OpenAIAssistantAgent.CreateAsync( - kernel: new(), - OpenAIClientProvider.ForOpenAI(apiKey), - new(modelId)); - -string threadId = await agent.CreateThreadAsync(); - -try -{ - ChatHistory messages = []; - while (true) - { - Console.Write("\nUser: "); - var input = Console.ReadLine(); - if (string.IsNullOrWhiteSpace(input)) { break; } - - await agent.AddChatMessageAsync(threadId, new(AuthorRole.User, input)); - - Console.Write("\nAssistant: "); - - await foreach (StreamingChatMessageContent content in agent.InvokeStreamingAsync(threadId, messages)) - { - Console.Write(content.Content); - } - - Console.WriteLine(); - } -} -finally -{ - await agent.DeleteThreadAsync(threadId); - await agent.DeleteAsync(); -} From 57850e26d92576620ccfcdab180e8309bda8dbda Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:46:54 -0700 Subject: [PATCH 212/226] Update dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs index 6417a7cac10e..6c94c22316d6 100644 --- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs +++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs @@ -103,6 +103,7 @@ public static partial void LogAgentChatInvokedAgentMessage( logLevel: LogLevel.Debug, eventId: 0, "[{MethodName}] Agent message {AgentType}/{AgentId}: {Message}."); + public static void LogAgentChatInvokedStreamingAgentMessages( this ILogger logger, string methodName, From 995ace965e1b8edab448805a7e47d3312c0c1ea9 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:47:37 -0700 Subject: [PATCH 213/226] Update dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingAnnotationContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs index 80619445b780..ccbb0e41d1b8 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs @@ -41,7 +41,7 @@ public StreamingAnnotationContent() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The source text being referenced. /// The model ID used to generate the content. From 647f7c3a944458b2e1e6589b242416caf046ee0a Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:47:45 -0700 Subject: [PATCH 214/226] Update dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingFileReferenceContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs index d9c44fb205b1..bdbe5a062e7e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs @@ -25,7 +25,7 @@ public StreamingFileReferenceContent() { } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// /// The identifier of the referenced file. /// The model ID used to generate the content. From 3c4e5169d879469cb0a0b93ead43e5391e389e1c Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:47:55 -0700 Subject: [PATCH 215/226] Update dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingFileReferenceContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs index bdbe5a062e7e..83b76946ef8e 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingFileReferenceContent.cs @@ -18,7 +18,7 @@ public class StreamingFileReferenceContent : StreamingKernelContent public string FileId { get; init; } = string.Empty; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// [JsonConstructor] public StreamingFileReferenceContent() From 9ae1dc62fb8a987ae2bc79da14ed6c7b2795f7e2 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:48:09 -0700 Subject: [PATCH 216/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 3eb2eb28b5e2..43f5d76dba32 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -12,7 +12,6 @@ using Microsoft.Extensions.Logging; using Microsoft.SemanticKernel.ChatCompletion; -//using Microsoft.SemanticKernel.ChatCompletion; using OpenAI; using OpenAI.Assistants; From ab8f9c1620ee8bc62b34d3b0d77f932f3afcb96d Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:48:22 -0700 Subject: [PATCH 217/226] Update dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingAnnotationContentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs index 14ab8f4a7dc9..eb954752ce4b 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingAnnotationContentTests.cs @@ -8,7 +8,7 @@ namespace SemanticKernel.UnitTests.Contents; #pragma warning disable SKEXP0110 /// -/// Unit testing of . +/// Unit testing of . /// public class StreamingAnnotationContentTests { From 87a8fa6c37f67db7bb132987eaaa32b90c024192 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:48:31 -0700 Subject: [PATCH 218/226] Update dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingFileReferenceContentTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs index 507980bdb2d2..a5105bf48ae5 100644 --- a/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs +++ b/dotnet/src/SemanticKernel.UnitTests/Contents/StreamingFileReferenceContentTests.cs @@ -8,7 +8,7 @@ namespace SemanticKernel.UnitTests.Contents; #pragma warning disable SKEXP0110 /// -/// Unit testing of . +/// Unit testing of . /// public class StreamingFileReferenceContentTests { From f29cb68aebb608b3ec415d741b6a163ba827215d Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 15:54:13 -0700 Subject: [PATCH 219/226] Update dotnet/src/Agents/Core/AgentGroupChat.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Agents/Core/AgentGroupChat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Core/AgentGroupChat.cs b/dotnet/src/Agents/Core/AgentGroupChat.cs index 3dc83aa0c5e1..09c2aa959a81 100644 --- a/dotnet/src/Agents/Core/AgentGroupChat.cs +++ b/dotnet/src/Agents/Core/AgentGroupChat.cs @@ -156,7 +156,7 @@ public async IAsyncEnumerable InvokeAsync( /// Asynchronous enumeration of messages. /// /// Specified agent joins the chat. - /// > + /// public async IAsyncEnumerable InvokeStreamingAsync( Agent agent, [EnumeratorCancellation] CancellationToken cancellationToken = default) From 6473623fcd250f9361a9b61160e910487b18eec7 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 5 Sep 2024 15:57:36 -0700 Subject: [PATCH 220/226] Sample comments --- dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs | 3 +-- dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs | 4 ++-- dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs | 3 +-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs index 5c512fb34147..e7860fa78a88 100644 --- a/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/ChatCompletion_Streaming.cs @@ -8,8 +8,7 @@ namespace Agents; /// -/// Demonstrate creation of and -/// eliciting its response to three explicit user messages. +/// Demonstrate consuming "streaming" message for . /// public class ChatCompletion_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) { diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs index e316c40211ac..bd50e200a760 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs @@ -8,8 +8,8 @@ namespace Agents; /// -/// Demonstrate creation of and -/// eliciting its response to three explicit user messages. +/// Demonstrate consuming "streaming" message for and +/// both participating in an . /// public class MixedChat_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) { diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs index 0f89258bad4d..3e1148a30941 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -8,8 +8,7 @@ namespace Agents; /// -/// Demonstrate creation of and -/// eliciting its response to three explicit user messages. +/// Demonstrate consuming "streaming" message for . /// public class OpenAIAssistant_Streaming(ITestOutputHelper output) : BaseAgentsTest(output) { From afe87ae7cd5b25f4890f078ff4b5ff2432a1599e Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 5 Sep 2024 16:05:56 -0700 Subject: [PATCH 221/226] Updated from PR comments --- .../Concepts/Agents/MixedChat_Streaming.cs | 35 ------------------- .../Agents/OpenAIAssistant_Streaming.cs | 1 - 2 files changed, 36 deletions(-) diff --git a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs index bd50e200a760..de88a242ee03 100644 --- a/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/MixedChat_Streaming.cs @@ -113,41 +113,6 @@ await OpenAIAssistantAgent.CreateAsync( Console.WriteLine($"\n[IS COMPLETED: {chat.IsComplete}]"); } - // Local function to invoke agent and display the conversation messages. - private async Task InvokeAgentAsync(ChatCompletionAgent agent, ChatHistory chat, string input) - { - ChatMessageContent message = new(AuthorRole.User, input); - chat.Add(message); - this.WriteAgentChatMessage(message); - - int historyCount = chat.Count; - - bool isFirst = false; - await foreach (StreamingChatMessageContent response in agent.InvokeStreamingAsync(chat)) - { - if (string.IsNullOrEmpty(response.Content)) - { - continue; - } - - if (!isFirst) - { - Console.WriteLine($"\n# {response.Role} - {response.AuthorName ?? "*"}:"); - isFirst = true; - } - - Console.WriteLine($"\t > streamed: '{response.Content}'"); - } - - if (historyCount <= chat.Count) - { - for (int index = historyCount; index < chat.Count; index++) - { - this.WriteAgentChatMessage(chat[index]); - } - } - } - private sealed class ApprovalTerminationStrategy : TerminationStrategy { // Terminate when the final message contains the term "approve" diff --git a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs index 3e1148a30941..b070b8cf6ef8 100644 --- a/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs +++ b/dotnet/samples/Concepts/Agents/OpenAIAssistant_Streaming.cs @@ -1,7 +1,6 @@ // Copyright (c) Microsoft. All rights reserved. using System.ComponentModel; using Microsoft.SemanticKernel; -using Microsoft.SemanticKernel.Agents; using Microsoft.SemanticKernel.Agents.OpenAI; using Microsoft.SemanticKernel.ChatCompletion; From 9adf4798a0f2b9d1de46021ce785be689bcd9c87 Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 5 Sep 2024 16:09:15 -0700 Subject: [PATCH 222/226] Whitespace --- dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs index 6c94c22316d6..ebd9e83b42ce 100644 --- a/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs +++ b/dotnet/src/Agents/Abstractions/Logging/AgentChatLogMessages.cs @@ -103,7 +103,7 @@ public static partial void LogAgentChatInvokedAgentMessage( logLevel: LogLevel.Debug, eventId: 0, "[{MethodName}] Agent message {AgentType}/{AgentId}: {Message}."); - + public static void LogAgentChatInvokedStreamingAgentMessages( this ILogger logger, string methodName, From e71e2ead4ecb9275cb2464c00478c43ed6b08fe7 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:19:11 -0700 Subject: [PATCH 223/226] Update dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- .../Contents/StreamingAnnotationContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs index ccbb0e41d1b8..609f94a87180 100644 --- a/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs +++ b/dotnet/src/SemanticKernel.Abstractions/Contents/StreamingAnnotationContent.cs @@ -34,7 +34,7 @@ public class StreamingAnnotationContent : StreamingKernelContent public int EndIndex { get; init; } /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// [JsonConstructor] public StreamingAnnotationContent() From 8967a1ef6b4c3c96cdb141134ad842f513af1aed Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:22:28 -0700 Subject: [PATCH 224/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index 43f5d76dba32..bf56144ed6fe 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -409,7 +409,6 @@ public static async IAsyncEnumerable InvokeStreamin { IReadOnlyList steps = await GetRunStepsAsync(client, run).ConfigureAwait(false); - //logger.LogOpenAIAssistantProcessingRunSteps(nameof(InvokeAsync), run.Id, threadId); // Execute functions in parallel and post results at once. FunctionCallContent[] functionCalls = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); From a6e686563b851777166876875b86b52429ec66c2 Mon Sep 17 00:00:00 2001 From: Chris <66376200+crickman@users.noreply.github.com> Date: Thu, 5 Sep 2024 16:22:55 -0700 Subject: [PATCH 225/226] Update dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs Co-authored-by: Dmytro Struk <13853051+dmytrostruk@users.noreply.github.com> --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index bf56144ed6fe..a441f222adca 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -599,7 +599,6 @@ private static IEnumerable ParseFunctionStep(OpenAIAssistan FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - //functionSteps.Add(toolCall.ToolCallId, content); yield return content; } From 9ca049ac1a8517bf6576885cce638d3414179a4b Mon Sep 17 00:00:00 2001 From: Chris Rickman Date: Thu, 5 Sep 2024 16:40:18 -0700 Subject: [PATCH 226/226] Whitespace --- dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs index a441f222adca..933ed120ae2e 100644 --- a/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs +++ b/dotnet/src/Agents/OpenAI/Internal/AssistantThreadActions.cs @@ -409,7 +409,6 @@ public static async IAsyncEnumerable InvokeStreamin { IReadOnlyList steps = await GetRunStepsAsync(client, run).ConfigureAwait(false); - // Execute functions in parallel and post results at once. FunctionCallContent[] functionCalls = steps.SelectMany(step => ParseFunctionStep(agent, step)).ToArray(); if (functionCalls.Length > 0) @@ -599,7 +598,6 @@ private static IEnumerable ParseFunctionStep(OpenAIAssistan FunctionCallContent content = new(nameParts.Name, nameParts.PluginName, toolCall.ToolCallId, functionArguments); - yield return content; } }